# ksp-worker-raw-transaction-ingest-lib `ksp-worker-raw-transaction-ingest-lib` est le Worker concret KSP chargé de l'alimentation continue de la couche RAW Transaction. La crate possède deux niveaux publics complémentaires : ```text RawTransactionIngestWorker::start -> fondation source-neutral, sans source productive RawTransactionIngestWorker::start_with_runtime_resources -> même runtime + 1..32 sources productives supervisées simultanément Yellowstone, WS standard logsSubscribe, WS standard blockSubscribe, Helius transactionSubscribe et/ou HTTP block polling + hydration HTTP getTransaction lorsque la source produit une référence ``` Le Worker reste indépendant de Config et de tout backend Store physique. Le caller compose les ressources Transport et Store, puis les remet à la crate par ses façades publiques. ## Pipeline productif actuel Les verticales live productives sont : ```text Yellowstone standard subscribe -> Transaction / TransactionStatus / Block -> signal source-neutral (network, signature, slot, commitment, provenance sûre) Solana standard WS logsSubscribe -> context.slot + signature -> signal source-neutral (network, signature, slot, commitment, provenance sûre) -> coalescence bornée par (network, signature, commitment) -> HTTP getTransaction observed, Base64, maxSupportedTransactionVersion=1 Solana standard WS blockSubscribe -> Full + Base64 + maxSupportedTransactionVersion=1 + showRewards=false -> qualification explicite Legacy / V0 / V1 -> matériau Common RAW direct par transaction du bloc Helius transactionSubscribe -> Full + Base64 + maxSupportedTransactionVersion=1 + showRewards=false -> signature + slot + transactionIndex uniquement dans le signal Worker -> coalescence bornée par (network, signature, commitment) -> HTTP getTransaction observed avant Common RAW Solana HTTP live block polling -> getSlot borne le run courant -> getBlocksWithLimit découvre les blocs live -> getBlock observed matérialise Full/Base64 Legacy/V0/V1 -> matériau Common RAW direct par transaction du bloc les chemins productifs -> ksp-raw-transaction-lib -> admission centrale bornée -> ksp-store-lib -> RawTransaction + RawTransactionObservation ``` `BlockMeta` et `Slot` ne produisent pas de RAW directement. Ils servent uniquement à la projection de continuité du run. La qualification reste conservative : même lorsqu'une update Yellowstone contient une transaction complète côté protobuf, le Worker hydrate actuellement les signaux transactionnels par HTTP `getTransaction` avant de construire le RAW canonique. Il n'existe donc pas de second canonicaliseur Yellowstone. ## Contrat de source Yellowstone + HTTP `RawTransactionIngestYellowstoneSource::new` reçoit : ```text YellowstoneGrpcChannel YellowstoneSubscribeRequest HttpTransportPool HttpRoleName d'hydration ``` La construction est sans I/O et refuse notamment : - une requête Yellowstone invalide ; - l'absence de famille porteuse d'ingestion ; - un commitment `Processed` ou implicite ; - un réseau Yellowstone non représentable ; - une provenance provider/endpoint non représentable ; - l'absence d'une route HTTP compatible avec le mode retenu : `getTransaction` pour les signaux transactionnels, `getBlock` pour un abonnement Yellowstone Block pur, sur le même réseau. Le runtime-resource aggregate public accepte une collection validée de 1 à 32 sources logiques et les démarre simultanément sous un supervisor privé. La collection est validée entièrement avant spawn ; aucun sous-ensemble silencieux, source primaire implicite ou standby n'est choisi. La collection interne, les `source_key`, les URLs, les filtres et les clients inférieurs ne sont pas exposés. Le supervisor possède toutes les tâches source. Une perte de source n'est plus assimilée automatiquement à un fault : lorsqu'elle porte une plage de continuité bornée, le Worker l'inscrit dans son ledger run-local puis n'autorise la continuation des siblings que si la coverage passée de cette perte est réconciliée et si les sources encore actives couvrent explicitement tout le `TargetCoverage` futur. Une perte sans plage sûre, une coverage insuffisante ou un gap encore ouvert reste terminal. Le Worker ne respawn jamais lui-même une source Transport. Pour un abonnement Yellowstone `Block` pur, la notification gRPC est un trigger de slot et ne bloque plus la boucle de réception pendant l'hydration HTTP. Le slot entre dans un coordinateur privé borné ; les `getBlock` puis l'admission complète du bloc s'exécutent dans au plus le quota `in-flight` attribué à cette source, tandis que le nombre de slots en attente reste borné par son quota `pending`. La boucle Yellowstone continue donc à consommer `next_update()` tant qu'une capacité pending existe. Lorsque ce quota pending est plein, le Worker suspend temporairement `next_update()` au lieu d'allouer davantage. Transport conserve lui aussi une queue d'updates bornée ; lorsqu'elle est pleine, son acteur Yellowstone attend asynchronement de la capacité et arrête de poller le stream gRPC jusqu'à reprise du consumer. La pression remonte donc au flux HTTP/2 sans `unbounded_channel`, task détaché, silent drop ni `grpc_backpressure_overflow` local pour la seule saturation de la queue d'updates. Une rupture distante réelle reste soumise au reconnect/replay borné de Transport et ne constitue toujours pas une garantie lossless ou exactly-once. Un inventaire privé `source_key -> latest processing/source state`, borné à 32 entrées, agrège la projection run-local. La frontier agrégée reste conservative : elle n'expose un `processing_frontier_slot` que lorsque toutes les sources en possèdent un, choisit le minimum des frontiers connus et le plus ancien pending. Les sources reference-bearing partagent en plus un registre global d'hydration borné : une même clé `(network, signature, commitment)` ne déclenche qu'un leader HTTP, puis chaque signal source conserve sa propre provenance lors de la finalisation. ## Contrat de source Standard Logs + HTTP `RawTransactionIngestStandardLogsSource::new` reçoit : ```text WsEndpointSettings kind solana_standard avec capability Logs déclarée SolanaLogsSubscribeFilter SolanaCommitment Confirmed ou Finalized HttpTransportPool HttpRoleName d'hydration ``` La construction est sans I/O. Elle refuse un endpoint WS invalide ou non standard, une capability `Logs` absente ou non déclarée, `Processed`, un réseau/provenance non représentable et l'absence de route HTTP `getTransaction` compatible sur le même réseau. Le filtre `All`, `AllWithVotes` ou `Mentions(pubkey)` participe uniquement à une empreinte privée ; le pubkey d'un filtre `Mentions` n'est pas recopié dans `Debug`, snapshot ou provenance textuelle. Au runtime, le Worker ouvre `SolanaStandardWsSession::connect`, puis `logs_subscribe`. Les lignes de logs et `err` restent dans Transport et ne sont jamais stockées dans le signal Worker. Seuls `context.slot` et `signature` sont projetés vers l'hydration commune. Reconnect, resubscribe et backpressure de la subscription restent possédés par Transport. ## Contrat de source Standard Block direct RAW `RawTransactionIngestStandardBlockSource::new` reçoit : ```text WsEndpointSettings kind solana_standard avec capability Block déclarée SolanaBlockSubscribeFilter SolanaCommitment Confirmed ou Finalized ``` La construction est sans I/O et refuse un endpoint qui ne déclare pas explicitement la capability `Block`. Le runtime ouvre ensuite la `SolanaStandardWsSession` existante et demande exactement `Base64`, `Full`, `maxSupportedTransactionVersion = 1` et `showRewards = false`. Aucun `HttpTransportPool` n'est attaché à cette source : les transactions dont la version est explicitement `Legacy`, `0` ou `1` sont transformées directement en `RawTransactionMaterial` à partir du wire Base64 du bloc, avec signature embarquée, slot, block time, meta, version et index de transaction. La qualification est fermée : une version omise/nulle ou supérieure à `1`, une transaction non Base64, `block: null`, une erreur distante de notification, un slot de contexte incohérent ou un champ transactions absent/nul termine la source par une erreur sûre. Aucun de ces cas n'est converti en bloc vide, en succès silencieux ou en progression artificielle de frontier. Pour un bloc multi-transaction, le slot n'est projeté settled qu'après l'admission réussie de toutes ses transactions. Cette voie RAW-direct n'incrémente pas `hydration_pending`; un stop ou une erreur au milieu du bloc ne produit aucune progression artificielle. Reconnect, resubscribe et backpressure WebSocket restent possédés par Transport. ## Contrat de source Helius Transaction + HTTP `RawTransactionIngestHeliusTransactionSource::new` reçoit : ```text WsEndpointSettings kind helius_laserstream avec capability HeliusTransaction déclarée HeliusTransactionSubscribeFilter SolanaCommitment Confirmed ou Finalized HttpTransportPool HttpRoleName d'hydration ``` La construction est sans I/O. Elle exige la capability Transport `HeliusTransaction`, puis réutilise le contrat Transport existant et impose `Full`, `Base64`, `showRewards = false` et `maxSupportedTransactionVersion = 1`. Le Worker ne lit pas Config, ne lit pas `KSP_SECRET_HELIUS_API_KEY` et ne code aucun tier provider ; l'URL résolue et le credential restent dans l'endpoint Transport fourni par le caller. Au runtime, `HeliusLaserStreamWsSession::connect` puis `transaction_subscribe` sont utilisés. Seule une notification `Full` conforme au mode demandé est admise ; une forme `Signature`, `Unknown` ou future devient une faute source sûre. Le payload Helius `transaction` n'est jamais copié dans l'état Worker : la projection conserve uniquement signature, slot et transaction index, puis réutilise le coordinateur d'hydration commun `getTransaction observed`. La clé logique Helius inclut réseau, identités provider/endpoint sûres, commitment et empreinte privée du filtre. Les listes de pubkeys du filtre sont normalisées avant hash afin que leur ordre ne crée pas artificiellement deux sources logiques ; le rôle HTTP d'hydration reste exclu de l'identité live. ## Contrat de source HTTP Block Polling `RawTransactionIngestHttpBlockPollingSource::new` reçoit : ```text HttpTransportPool HttpRoleName de polling SolanaCommitment Confirmed ou Finalized ``` La construction est sans I/O et vérifie que le rôle HTTP possède, sur un seul réseau, les capacités `getSlot`, `getBlocksWithLimit` et `getBlock`. La cadence est bornée entre 100 ms et 30 s, avec 1 s par défaut ; la découverte est bornée entre 1 et 1024 blocs par cycle, avec 128 par défaut. Ces réglages de cadence ne font pas partie de l'identité logique de la source. Au démarrage, le premier `getSlot` fixe la borne inférieure du run. Le Worker ne demande aucun slot antérieur. Chaque cycle relit le tip, découvre les blocs disponibles avec `getBlocksWithLimit`, puis matérialise chaque slot listé par `getBlock observed` en `Full + Base64 + maxSupportedTransactionVersion = 1 + showRewards = false`. Seules les transactions Legacy/V0/V1 explicitement qualifiées entrent directement dans Common RAW. Un slot listé dont `getBlock` retourne `null` reste la tête de reprise du cycle suivant ; il n'est ni considéré vide ni marqué settled. Les slots absents de la liste de découverte sont traités comme non produits/skipped pour ce run. Le slot n'est settled qu'après admission réussie de toutes ses transactions. Le polling reste run-local : aucun checkpoint durable, aucun scan avant la borne initiale et aucun Backfill implicite ne sont créés. Les retries/reroutages HTTP restent possédés par `ksp-onchain-transport-lib`. ## Runtime et lifecycle Le Worker s'exécute sur le runtime Tokio courant du caller. Il ne crée pas de runtime global et n'expose aucun `JoinHandle` public. `RawTransactionIngestHandle` permet de : - demander un stop coopératif et idempotent ; - lire une source de snapshots concrets latest-value ; - utiliser la même source via `WorkerSnapshotSource` ; - attendre le terminal après drain et join des tâches possédées. Le shutdown est borné par `shutdown_drain_timeout` (10 s par défaut). Le supervisor multi-source relaie le stop à toutes les sources et les rejoint avant de rendre son résultat au supervisor Worker ; les tâches source, hydration et persistence possédées sont ensuite drainées ou abort+join avant publication terminale. Si la deadline expire, l'abort du wrapper source détruit aussi son `JoinSet` interne et annule ses tâches imbriquées avant le terminal. Une faute déjà observée n'est pas remplacée par un stop concurrent, sauf le `drain_timeout` terminal lorsqu'une récupération bornée dépasse sa deadline. Pour Yellowstone, un `Status` distant reçu après le half-close local explicitement engagé est classé `Closed` par Transport et ne remplace plus le Stop coopératif ; un timeout de fermeture Transport survenant après un Stop déjà demandé reste lui aussi accepté par le Worker comme fermeture coopérative bornée. En revanche, un `grpc_status` observé pendant une session active conserve la politique normale reconnect/failure. L'abandon terminal d'une hydration retire son pending run-local sans le convertir artificiellement en travail `settled`. ## Admission, coalescence et backpressure La queue centrale est un `tokio::sync::mpsc` privé borné par `admission_queue_capacity`. Les sources internes subissent la backpressure ; aucune queue non bornée ni silent drop n'est autorisé. Le Worker possède des coordinateurs bornés pour Yellowstone, Standard Logs et Helius Transaction. Les voies Yellowstone/Standard Logs/Helius basées sur des références partagent le registre global d'hydration `(network, signature, commitment)`. Le mode Yellowstone Block utilise un coordinateur de slots séparé mais reçoit sa part des mêmes budgets globaux pending/in-flight avant spawn ; Standard Block et HTTP Block Polling n'entrent pas dans ces quotas lorsqu'une transaction est direct-qualified. Les sources nécessitant une hydration HTTP asynchrone, y compris Yellowstone Block, reçoivent des quotas déterministes dont la somme reste exactement dans les bornes techniques configurées : ```text somme pending source signals <= admission_queue_capacity somme hydration tasks in flight <= persistence_concurrency source hydratante active => quota pending >= 1 et quota in-flight >= 1 ``` Pour garantir simultanément ces bornes et l'absence de starvation structurelle, le démarrage échoue avant spawn si le nombre de sources hydratantes dépasse `admission_queue_capacity` ou `persistence_concurrency`. Le registre des hydrations transactionnelles protège en plus l'ouverture effective des `getTransaction`; Yellowstone Block reste borné par sa partition déterministe et son `JoinSet` possédé. Les signaux partageant le même `(network, signature, commitment)` sont coalescés cross-source avant le fan-out HTTP. La publication du résultat partagé notifie les followers sous le verrou de registry avant de retirer la clé : une nouvelle génération de leader ne peut donc pas s'intercaler entre retrait et notification. Après canonicalisation, une cache run-local bornée sérialise les acquisitions de même `(network, signature)` : la première passe par l'écriture atomique entity + observation, les suivantes de contenu canonique identique ajoutent uniquement leur observation déterministe. Le Store conserve le guard durable final. En `0.3.15`, il accepte aussi un entrant strictement moins complet lorsque l'unique divergence prouvée est `meta.logMessages` avec exactement un marqueur `Log truncated` après un préfixe identique alors que le canonique stocké n'est pas tronqué ; le canonique complet reste inchangé et l'observation est conservée. Toute autre divergence, ainsi que le sens tronqué -> complet, reste un `content_conflict` terminal. Aucune majorité, préférence provider ou overwrite n'est appliqué. Les retries/reroutages HTTP appartiennent à `ksp-onchain-transport-lib`. Le Worker ne possède pas une seconde boucle de retry autour de `getTransaction`. Le trafic nominal et le trafic de réparation partagent le même registre global d'hydration, les mêmes permits, la même admission et la même persistence. Un gate de fairness privé alterne les deux classes lorsqu'elles attendent simultanément, sans réserver une fraction fixe de capacité ; une capacité existante de `1` doit donc encore permettre la progression des deux classes. ## Processing frontier run-local Le snapshot expose : ```text hydration_pending processing_frontier_slot oldest_pending_slot ``` Cette frontier mesure uniquement le traitement des signaux réellement observés pendant le run courant. Elle n'est ni un checkpoint durable, ni une preuve de complétude blockchain, ni un curseur de Backfill. Un signal transactionnel devient pending après validation de sa clé d'hydration et insertion dans le coordinateur. Il devient settled pour la source lorsque : ```text getTransaction -> Missing ou getTransaction -> Available puis ingress envoyé avec succès vers l'admission centrale ``` Un `BlockMeta` ou `Slot` continuity-only est settled localement sans produire de RAW. La frontier n'avance jamais à travers le plus ancien pending connu. ## Reconnect, replay, gaps et réparation run-local Le reconnect/replay Yellowstone appartient à Transport. Le Worker n'écrit pas `from_slot`, ne traite pas directement `SubscribeReplayInfo` et ne transforme jamais une simple reconnexion en preuve de continuité. Le Worker maintient séparément la processing frontier et une continuity frontier gap-aware. Les gaps sont des intervalles inclusifs bornés du run courant ; ils ne constituent ni une campagne historique ni une liste présumée de transactions manquantes. Les bornes internes sont : ```text open gaps <= 64 range d'un gap <= 4096 slots discovery HTTP par fenêtre <= 512 slots getBlock logiques concurrents <= 4 repair actif simultané <= 1 ``` Les mécanismes admissibles restent conservatifs : replay Transport lorsqu'il est réellement adressable, preuve de coverage d'une autre source compatible, scan HTTP borné, récupération de bloc produit et hydration d'une référence connue. Une réponse `getTransaction = null` reste une obligation manquante et `getBlock = null` pour un slot prouvé produit ne devient jamais une preuve d'absence. Les types publics source-neutral `RawTransactionIngestGapId`, `RawTransactionIngestGapState`, `RawTransactionIngestGapReason`, `RawTransactionIngestRepairMethod` et `RawTransactionIngestGapSnapshot` permettent d'observer les gaps sans exposer `source_key`, provider, endpoint, filtre, signature ou payload. Le snapshot concret expose notamment : ```text gaps() open_gap_count() repairing_gap_count() repaired_gap_total() unresolved_gap_total() replay_repair_total() redundant_coverage_repair_total() http_scan_repair_total() repair_block_fetch_total() repair_transaction_hydration_total() oldest_open_gap_start_slot() ``` La liste détaillée reste bornée ; tous les gaps ouverts sont retenus et les entrées réparées récentes peuvent occuper la capacité restante. Les compteurs utilisent une arithmétique checked. Le snapshot conserve également les informations source-neutral suivantes : ```text source_total source_active source_reconnecting source_failed source_state source_failure_total backpressure_wait_total source_reconnect_total source_replay_attempt_total source_continuity_gap_total ``` `RawTransactionIngestSourceState` distingue `Active`, `Reconnecting`, `Closing`, `Closed` et `Failed`. Un replay attempt, une redelivery de frontière et une coverage d'intervalle restent des preuves différentes. La health publique est volontairement stricte dès que la policy de continuité est active : reconnect en cours, gap ouvert, continuity frontier différente de la processing frontier ou `TargetCoverage` futur non couvert donnent `Unhealthy`. `Healthy` exige toutes les sources attendues actives et aucune lacune de continuité. `Degraded` n'est permis qu'après perte de source explicitement réconciliée, lorsque les sources restantes couvrent encore tout le `TargetCoverage` futur. Une source perdue peut donc rester absente uniquement si sa perte est bornée, si ses gaps sont fermés, si la continuity frontier rejoint la processing frontier et si la coverage future reste prouvée. Dans tous les autres cas, le Worker fault avec une erreur source sûre. Il ne lance jamais `ksp-job-backfill-lib` ni une campagne historique automatique. ## Persistence Store La persistance passe exclusivement par `ksp-store-lib` avec `default-features = false`. Aucun backend physique n'est importé directement. L'écriture utilise le mode normal atomique `RawTransaction + RawTransactionObservation`. Les outcomes distingués incluent : ```text entity inserted entity already present entity skipped purged observation inserted observation already present observation not recorded for purged entity content conflict store failure ``` Un content conflict réel reste terminal en `0.3.15` et n'est jamais converti en succès idempotent. L'exception `logMessages` tronqués décrite ci-dessus est classée avant le conflit comme observation compatible moins complète ; elle ne remplace pas le canonique et n'incrémente pas le terminal `content_conflict`. La conservation de variantes conflictuelles et leur résolution sans arrêt du Worker appartiennent à `0.3.16`. ## Snapshots et erreurs `RawTransactionIngestSnapshotSource` est latest-value : les lecteurs peuvent rater des transitions intermédiaires mais récupèrent toujours la dernière projection complète et monotone. Les codes Worker publics sont : ```text worker_raw_transaction_ingest.settings_invalid worker_raw_transaction_ingest.runtime_invalid worker_raw_transaction_ingest.store_failed worker_raw_transaction_ingest.content_conflict worker_raw_transaction_ingest.counter_exhausted worker_raw_transaction_ingest.source_failed worker_raw_transaction_ingest.drain_timeout ``` Les diagnostics et `Debug` ne recopient pas de payload RAW, signature, URL, credential, filtre provider, texte backend/provider arbitraire ou client inférieur. Les agrégations de compteurs de continuité multi-source utilisent une arithmétique vérifiée ; un overflow devient `counter_exhausted` au lieu d'être saturé silencieusement. ## Dépendances Les dépendances normales sont exactement : ```text ksp-core-lib ksp-logging-lib ksp-onchain-transport-lib ksp-raw-transaction-lib ksp-store-lib (default-features = false) ksp-worker-api sha2 tokio (macros, rt, sync, time) ``` La crate ne dépend pas de Config, Job, `ksp-store-api` directement, backend Store concret, `reqwest`, `tonic`, `yellowstone-grpc-proto` ou Tauri. ## Hors périmètre La verticale actuelle ne possède pas : - sélection Config interne, reconfiguration dynamique ou policy de failover entre sources ; - checkpoint persistent de processing frontier ; - campagne de réparation historique automatique ; - application Desk ou process autonome ; - décodage STRUCTURAL/DECODED/DOMAIN. Les futures extensions doivent conserver la séparation avec `ksp-job-backfill-lib` et réutiliser les mêmes contrats Common RAW/Store. ## Documentation - [`USAGE.md`](USAGE.md) — utilisation de la façade publique ; - [`../ksp-worker-api/README.md`](../ksp-worker-api/README.md) — contrats Worker génériques ; - [`../ksp-raw-transaction-lib/README.md`](../ksp-raw-transaction-lib/README.md) — canonicalisation RAW commune ; - [`../../docs/architecture/009-ACQUISITION_WORKERS_AND_JOBS.md`](../../docs/architecture/009-ACQUISITION_WORKERS_AND_JOBS.md) — séparation Worker/Job ; - [`../../docs/architecture/011-RAW_TRANSACTION_ACQUISITION.md`](../../docs/architecture/011-RAW_TRANSACTION_ACQUISITION.md) — architecture d'acquisition RawTransaction.