diff --git a/Cargo.toml b/Cargo.toml index e0d8bc0..e7639ca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ # file: Cargo.toml -# version: 551 +# version: 552 [workspace] resolver = "3" members = ["crates/ksp-app-backfill-desk", "crates/ksp-app-config-desk", "crates/ksp-app-solprices-desk", "crates/ksp-app-store-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-interface-lib", "crates/ksp-job-api", "crates/ksp-job-backfill-lib", "crates/ksp-logging-lib", "crates/ksp-offchain-transport-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-program-api", "crates/ksp-raw-transaction-lib", "crates/ksp-store-api", "crates/ksp-store-lib", "crates/ksp-store-postgres-lib", "crates/ksp-wallet-lib", "crates/ksp-worker-api", "crates/ksp-worker-raw-transaction-ingest-lib"] [workspace.package] -version = "0.3.13-pre.6.fix.1" +version = "0.3.13-pre.7" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" diff --git a/crates/ksp-worker-raw-transaction-ingest-lib/README.md b/crates/ksp-worker-raw-transaction-ingest-lib/README.md index 1de4a11..9b6403c 100644 --- a/crates/ksp-worker-raw-transaction-ingest-lib/README.md +++ b/crates/ksp-worker-raw-transaction-ingest-lib/README.md @@ -1,5 +1,5 @@ - + # ksp-worker-raw-transaction-ingest-lib @@ -12,8 +12,8 @@ RawTransactionIngestWorker::start -> fondation source-neutral, sans source productive RawTransactionIngestWorker::start_with_runtime_resources - -> même runtime + une source productive supervisée - Yellowstone, WS standard logsSubscribe, WS standard blockSubscribe, Helius transactionSubscribe ou HTTP block polling + -> 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 ``` @@ -82,7 +82,11 @@ La construction est sans I/O et refuse notamment : - une provenance provider/endpoint non représentable ; - l'absence d'une route HTTP compatible pour `getTransaction` sur le même réseau. -Le runtime-resource aggregate public accepte une collection validée de 1 à 32 sources logiques. `pre.006` sait exécuter une source unique Yellowstone, Standard Logs, Standard Block, Helius Transaction ou HTTP Block Polling ; une activation simultanée de plusieurs sources reste rejetée avant spawn jusqu'au supervisor dédié. La collection interne, les `source_key`, les URLs, les filtres et les clients inférieurs ne sont pas exposés. +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 source qui échoue est terminale pour le Worker et déclenche l'arrêt coopératif puis le join des autres sources, car cette release ne suppose aucune équivalence de coverage. Une fermeture propre d'une source alors que le Worker n'est pas en arrêt est également traitée comme une perte de source configurée et devient terminale. + +Chaque source conserve provisoirement ses mécanismes de production/hydration existants. 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. La convergence/coalescence cross-source des mêmes transactions reste une tranche séparée. ## Contrat de source Standard Logs + HTTP @@ -161,7 +165,7 @@ Le Worker s'exécute sur le runtime Tokio courant du caller. Il ne crée pas de - 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`. Les tâches source, hydration et persistence possédées sont drainées ou abort+join avant publication terminale. L'abandon terminal d'une hydration retire son pending run-local sans le convertir artificiellement en travail `settled`. +Le shutdown est borné par `shutdown_drain_timeout`. 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. L'abandon terminal d'une hydration retire son pending run-local sans le convertir artificiellement en travail `settled`. ## Admission, coalescence et backpressure diff --git a/crates/ksp-worker-raw-transaction-ingest-lib/USAGE.md b/crates/ksp-worker-raw-transaction-ingest-lib/USAGE.md index 7f0045d..47d3336 100644 --- a/crates/ksp-worker-raw-transaction-ingest-lib/USAGE.md +++ b/crates/ksp-worker-raw-transaction-ingest-lib/USAGE.md @@ -1,5 +1,5 @@ - + # Utilisation de ksp-worker-raw-transaction-ingest-lib @@ -101,7 +101,23 @@ let handle = match ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestWo }; ``` -Les deux entrées exigent un runtime Tokio courant et un `Store` portant exactement le même `RawNetworkId` que les settings. Le démarrage avec ressources exige également que chaque source composée cible ce même réseau. Une exécution productive accepte une source unique Yellowstone, Standard Logs, Standard Block, Helius Transaction ou HTTP Block Polling ; une collection de plusieurs sources est validable à la composition mais reste rejetée avant spawn tant que la supervision simultanée n'est pas disponible. +Les deux entrées exigent un runtime Tokio courant et un `Store` portant exactement le même `RawNetworkId` que les settings. Le démarrage avec ressources exige également que chaque source composée cible ce même réseau. L'agrégat accepte 1 à 32 sources logiques et les lance simultanément ; les doublons d'identité et les mélanges de réseaux sont refusés avant spawn. Il n'existe pas de source primaire, standby ou fallback implicite : toute source configurée fait partie du run. + +### Composer plusieurs sources simultanées + +Une fois un premier `RawTransactionIngestRuntimeResources` construit, le caller ajoute les autres sources avec les méthodes `try_push_*` correspondant à leur capability. Exemple conceptuel : + +```rust +let mut resources = ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestRuntimeResources::new(yellowstone_source); +if let std::result::Result::Err(error) = resources.try_push_standard_logs_source(standard_logs_source) { + return std::result::Result::Err(error); +} +if let std::result::Result::Err(error) = resources.try_push_http_block_polling_source(http_polling_source) { + return std::result::Result::Err(error); +} +``` + +La validation est transactionnelle à chaque ajout : limite globale 32, réseau unique et `source_key` logique unique. Au démarrage, toutes les sources présentes sont supervisées ensemble. La défaillance d'une source est terminale pour le Worker ; les autres sources sont arrêtées et jointes, car le Worker ne suppose pas qu'elles couvrent les mêmes filtres ou le même univers de transactions. ### Source Standard Logs productive diff --git a/crates/ksp-worker-raw-transaction-ingest-lib/src/runtime.rs b/crates/ksp-worker-raw-transaction-ingest-lib/src/runtime.rs index e8e634e..d1e4a31 100644 --- a/crates/ksp-worker-raw-transaction-ingest-lib/src/runtime.rs +++ b/crates/ksp-worker-raw-transaction-ingest-lib/src/runtime.rs @@ -1,5 +1,5 @@ // file: crates/ksp-worker-raw-transaction-ingest-lib/src/runtime.rs -// version: 13 +// version: 14 type PersistencePort = std::sync::Arc; type PersistenceTasks = tokio::task::JoinSet>; @@ -99,8 +99,8 @@ impl crate::RawTransactionIngestWorker { /// Starts one Worker with caller-composed runtime resources. /// - /// `pre.003` activates either the existing Yellowstone source or one standard Solana `logsSubscribe` source through the same bounded Worker foundation. - /// Aggregates with more than one source still fail closed before any source task is spawned until the dedicated simultaneous supervisor tranche. + /// Every validated source in the bounded runtime-resource aggregate is started concurrently under one private source supervisor. Any configured source + /// failure remains terminal for the Worker because this release does not infer equivalent coverage or failover between source families. pub fn start_with_runtime_resources( settings: crate::RawTransactionIngestSettings, store: std::sync::Arc, @@ -117,9 +117,6 @@ impl crate::RawTransactionIngestWorker { if let std::result::Result::Err(error) = runtime_resources.validate_network(settings.network()) { return std::result::Result::Err(error); } - if let std::result::Result::Err(error) = runtime_resources.validate_single_source_activation() { - return std::result::Result::Err(error); - } let source_settings = settings.clone(); let (processing_frontier_sender, processing_frontier_receiver) = tokio::sync::watch::channel(crate::RawTransactionIngestProcessingFrontierProjection::empty()); @@ -131,7 +128,7 @@ impl crate::RawTransactionIngestWorker { std::option::Option::Some(processing_frontier_receiver), move |children, stop_receiver, admission_sender| { let _abort_handle = children.spawn(async move { - return runtime_resources.run_single_live_source(source_settings, stop_receiver, admission_sender, processing_frontier_sender).await; + return runtime_resources.run_live_sources(source_settings, stop_receiver, admission_sender, processing_frontier_sender).await; }); }, ); diff --git a/crates/ksp-worker-raw-transaction-ingest-lib/src/runtime_resources.rs b/crates/ksp-worker-raw-transaction-ingest-lib/src/runtime_resources.rs index 1d398b9..86587ef 100644 --- a/crates/ksp-worker-raw-transaction-ingest-lib/src/runtime_resources.rs +++ b/crates/ksp-worker-raw-transaction-ingest-lib/src/runtime_resources.rs @@ -1,5 +1,5 @@ // file: crates/ksp-worker-raw-transaction-ingest-lib/src/runtime_resources.rs -// version: 20 +// version: 21 use sha2::Digest; // rust-rules: trait-import @@ -51,6 +51,19 @@ enum RawTransactionIngestLiveSource { Yellowstone(crate::RawTransactionIngestYellowstoneSource), } +struct RawTransactionIngestSourceInventory { + source_keys: std::vec::Vec<[u8; 32]>, + source_projections: std::vec::Vec, +} + +#[derive(Clone)] +struct RawTransactionIngestSourceInventoryPublisher { + aggregate_sender: tokio::sync::watch::Sender, + entry_index: usize, + inventory: std::sync::Arc>, + source_key: [u8; 32], +} + impl RawTransactionIngestLiveSource { fn network(&self) -> &ksp_store_lib::RawNetworkId { return match self { @@ -77,15 +90,148 @@ impl RawTransactionIngestLiveSource { settings: crate::RawTransactionIngestSettings, stop_receiver: tokio::sync::watch::Receiver, admission_sender: tokio::sync::mpsc::Sender, - processing_frontier_sender: tokio::sync::watch::Sender, + inventory_publisher: RawTransactionIngestSourceInventoryPublisher, ) -> ksp_core_lib::Result<()> { - return match self { - Self::HeliusTransaction(source) => source.run(settings, stop_receiver, admission_sender, processing_frontier_sender).await, - Self::HttpBlockPolling(source) => source.run(settings, stop_receiver, admission_sender, processing_frontier_sender).await, - Self::StandardBlock(source) => source.run(settings, stop_receiver, admission_sender, processing_frontier_sender).await, - Self::StandardLogs(source) => source.run(settings, stop_receiver, admission_sender, processing_frontier_sender).await, - Self::Yellowstone(source) => source.run(settings, stop_receiver, admission_sender, processing_frontier_sender).await, + let (source_frontier_sender, mut source_frontier_receiver) = + tokio::sync::watch::channel(crate::RawTransactionIngestProcessingFrontierProjection::empty()); + let mut source_future = std::boxed::Box::pin(async move { + return match self { + Self::HeliusTransaction(source) => source.run(settings, stop_receiver, admission_sender, source_frontier_sender).await, + Self::HttpBlockPolling(source) => source.run(settings, stop_receiver, admission_sender, source_frontier_sender).await, + Self::StandardBlock(source) => source.run(settings, stop_receiver, admission_sender, source_frontier_sender).await, + Self::StandardLogs(source) => source.run(settings, stop_receiver, admission_sender, source_frontier_sender).await, + Self::Yellowstone(source) => source.run(settings, stop_receiver, admission_sender, source_frontier_sender).await, + }; + }); + loop { + tokio::select! { + biased; + result = &mut source_future => { + let latest = *source_frontier_receiver.borrow_and_update(); + let terminal_state = if result.is_ok() { + crate::RawTransactionIngestSourceState::Closed + } else { + crate::RawTransactionIngestSourceState::Failed + }; + inventory_publisher.publish(source_projection_with_state(latest, terminal_state)); + return result; + } + changed = source_frontier_receiver.changed() => { + if changed.is_err() { + return std::result::Result::Err(crate::runtime_error("source.frontier_channel_closed")); + } + inventory_publisher.publish(*source_frontier_receiver.borrow_and_update()); + } + } + } + } +} + +impl RawTransactionIngestSourceInventory { + fn aggregate(&self) -> crate::RawTransactionIngestProcessingFrontierProjection { + let mut hydration_pending = 0_usize; + let mut oldest_pending_slot = std::option::Option::None; + let mut processing_frontier_slot = std::option::Option::None; + let mut all_frontiers_present = !self.source_projections.is_empty(); + let mut source_continuity_gap_total = 0_u64; + let mut source_reconnect_total = 0_u64; + let mut source_replay_attempt_total = 0_u64; + let mut any_active = false; + let mut any_closing = false; + let mut any_failed = false; + let mut any_reconnecting = false; + let mut all_closed = !self.source_projections.is_empty(); + for projection in &self.source_projections { + hydration_pending = hydration_pending.saturating_add(projection.hydration_pending()); + oldest_pending_slot = minimum_optional_slot(oldest_pending_slot, projection.oldest_pending_slot()); + match projection.processing_frontier_slot() { + std::option::Option::Some(slot) => { + processing_frontier_slot = minimum_optional_slot(processing_frontier_slot, std::option::Option::Some(slot)); + }, + std::option::Option::None => { + all_frontiers_present = false; + }, + } + source_continuity_gap_total = source_continuity_gap_total.saturating_add(projection.source_continuity_gap_total()); + source_reconnect_total = source_reconnect_total.saturating_add(projection.source_reconnect_total()); + source_replay_attempt_total = source_replay_attempt_total.saturating_add(projection.source_replay_attempt_total()); + match projection.source_state() { + std::option::Option::Some(crate::RawTransactionIngestSourceState::Active) => { + any_active = true; + all_closed = false; + }, + std::option::Option::Some(crate::RawTransactionIngestSourceState::Closing) => { + any_closing = true; + all_closed = false; + }, + std::option::Option::Some(crate::RawTransactionIngestSourceState::Closed) => {}, + std::option::Option::Some(crate::RawTransactionIngestSourceState::Failed) => { + any_failed = true; + all_closed = false; + }, + std::option::Option::Some(crate::RawTransactionIngestSourceState::Reconnecting) => { + any_reconnecting = true; + all_closed = false; + }, + std::option::Option::None => { + all_closed = false; + }, + } + } + if !all_frontiers_present { + processing_frontier_slot = std::option::Option::None; + } + let source_state = if any_failed { + std::option::Option::Some(crate::RawTransactionIngestSourceState::Failed) + } else if any_reconnecting { + std::option::Option::Some(crate::RawTransactionIngestSourceState::Reconnecting) + } else if any_closing { + std::option::Option::Some(crate::RawTransactionIngestSourceState::Closing) + } else if any_active { + std::option::Option::Some(crate::RawTransactionIngestSourceState::Active) + } else if all_closed { + std::option::Option::Some(crate::RawTransactionIngestSourceState::Closed) + } else { + std::option::Option::None }; + return crate::RawTransactionIngestProcessingFrontierProjection::new(hydration_pending, processing_frontier_slot, oldest_pending_slot) + .with_source_continuity(source_state, source_reconnect_total, source_replay_attempt_total, source_continuity_gap_total); + } + + fn new(source_keys: std::vec::Vec<[u8; 32]>) -> Self { + let source_projections = source_keys.iter().map(|_source_key| return crate::RawTransactionIngestProcessingFrontierProjection::empty()).collect(); + return Self { source_keys, source_projections }; + } + + fn update(&mut self, entry_index: usize, source_key: [u8; 32], projection: crate::RawTransactionIngestProcessingFrontierProjection) { + let expected_key = match self.source_keys.get(entry_index) { + std::option::Option::Some(value) => value, + std::option::Option::None => return, + }; + if expected_key != &source_key { + return; + } + let current = match self.source_projections.get_mut(entry_index) { + std::option::Option::Some(value) => value, + std::option::Option::None => return, + }; + *current = projection; + return; + } +} + +impl RawTransactionIngestSourceInventoryPublisher { + fn publish(&self, projection: crate::RawTransactionIngestProcessingFrontierProjection) { + let aggregate = { + let mut inventory = match self.inventory.lock() { + std::result::Result::Ok(value) => value, + std::result::Result::Err(poisoned) => poisoned.into_inner(), + }; + inventory.update(self.entry_index, self.source_key, projection); + inventory.aggregate() + }; + self.aggregate_sender.send_replace(aggregate); + return; } } @@ -1610,9 +1756,8 @@ impl std::fmt::Debug for crate::RawTransactionIngestYellowstoneSource { /// Caller-composed runtime resources accepted by the continuous RAW transaction ingest Worker. /// -/// The aggregate owns a bounded collection of capability-specific live-source contracts. `pre.006` supports Yellowstone, standard Solana logs/block, Helius -/// transaction and HTTP live block polling sources while deliberately keeping simultaneous multi-source supervision gated until the dedicated supervisor -/// tranche. +/// The aggregate owns a bounded collection of capability-specific live-source contracts. All validated sources are started together by one private supervisor; +/// source identities and per-source lifecycle remain private while the existing Worker snapshot receives only a conservative source-neutral aggregate. pub struct RawTransactionIngestRuntimeResources { sources: std::vec::Vec, } @@ -1776,34 +1921,37 @@ impl crate::RawTransactionIngestRuntimeResources { return std::result::Result::Ok(()); } - /// Validates that the current supervisor tranche can activate the aggregate without silently dropping configured sources. - pub(crate) fn validate_single_source_activation(&self) -> ksp_core_lib::Result<()> { - if self.sources.is_empty() { - return std::result::Result::Err(crate::runtime_error("runtime_resources.source_collection_empty")); - } - if self.sources.len() != 1 { - return std::result::Result::Err(crate::runtime_error("runtime_resources.multi_source_activation_pending")); - } - return std::result::Result::Ok(()); - } - - /// Runs the already validated single live source through the common Worker source-task contract. - pub(crate) async fn run_single_live_source( + /// Runs every validated source concurrently under one private bounded source supervisor. + pub(crate) async fn run_live_sources( self, settings: crate::RawTransactionIngestSettings, stop_receiver: tokio::sync::watch::Receiver, admission_sender: tokio::sync::mpsc::Sender, processing_frontier_sender: tokio::sync::watch::Sender, ) -> ksp_core_lib::Result<()> { - let mut sources = self.sources.into_iter(); - let source = match sources.next() { - std::option::Option::Some(value) => value, - std::option::Option::None => return std::result::Result::Err(crate::runtime_error("runtime_resources.source_collection_empty")), - }; - if sources.next().is_some() { - return std::result::Result::Err(crate::runtime_error("runtime_resources.multi_source_activation_pending")); + if self.sources.is_empty() || self.sources.len() > crate::MAX_RAW_TRANSACTION_INGEST_LIVE_SOURCES { + return std::result::Result::Err(crate::runtime_error("runtime_resources.source_collection_invalid")); } - return source.run(settings, stop_receiver, admission_sender, processing_frontier_sender).await; + let source_keys = self.sources.iter().map(RawTransactionIngestLiveSource::source_key).collect::>(); + let inventory = std::sync::Arc::new(std::sync::Mutex::new(RawTransactionIngestSourceInventory::new(source_keys))); + let (source_stop_sender, source_stop_receiver) = tokio::sync::watch::channel(false); + let mut children = tokio::task::JoinSet::new(); + for (entry_index, source) in self.sources.into_iter().enumerate() { + let publisher = RawTransactionIngestSourceInventoryPublisher { + aggregate_sender: processing_frontier_sender.clone(), + entry_index, + inventory: std::sync::Arc::clone(&inventory), + source_key: source.source_key(), + }; + let source_admission_sender = admission_sender.clone(); + let source_settings = settings.clone(); + let source_stop_receiver = source_stop_receiver.clone(); + let _abort_handle = children.spawn(async move { + return source.run(source_settings, source_stop_receiver, source_admission_sender, publisher).await; + }); + } + std::mem::drop(admission_sender); + return supervise_live_source_tasks(stop_receiver, source_stop_sender, children).await; } } @@ -1828,6 +1976,87 @@ impl std::hash::Hasher for RawTransactionIngestSourceKeyHashWriter<'_> { } } +fn minimum_optional_slot(first: std::option::Option, second: std::option::Option) -> std::option::Option { + return match (first, second) { + (std::option::Option::Some(left), std::option::Option::Some(right)) => std::option::Option::Some(left.min(right)), + (std::option::Option::Some(value), std::option::Option::None) | (std::option::Option::None, std::option::Option::Some(value)) => { + std::option::Option::Some(value) + }, + (std::option::Option::None, std::option::Option::None) => std::option::Option::None, + }; +} + +fn source_projection_with_state( + projection: crate::RawTransactionIngestProcessingFrontierProjection, + state: crate::RawTransactionIngestSourceState, +) -> crate::RawTransactionIngestProcessingFrontierProjection { + return crate::RawTransactionIngestProcessingFrontierProjection::new( + projection.hydration_pending(), + projection.processing_frontier_slot(), + projection.oldest_pending_slot(), + ) + .with_source_continuity( + std::option::Option::Some(state), + projection.source_reconnect_total(), + projection.source_replay_attempt_total(), + projection.source_continuity_gap_total(), + ); +} + +async fn supervise_live_source_tasks( + mut stop_receiver: tokio::sync::watch::Receiver, + source_stop_sender: tokio::sync::watch::Sender, + mut children: tokio::task::JoinSet>, +) -> ksp_core_lib::Result<()> { + loop { + if *stop_receiver.borrow() { + source_stop_sender.send_replace(true); + return drain_live_source_tasks(&mut children, std::option::Option::None).await; + } + let joined = tokio::select! { + biased; + changed = stop_receiver.changed() => { + if changed.is_err() || *stop_receiver.borrow() { + source_stop_sender.send_replace(true); + return drain_live_source_tasks(&mut children, std::option::Option::None).await; + } + continue; + } + value = children.join_next(), if !children.is_empty() => value, + }; + let first_fault = match joined { + std::option::Option::Some(std::result::Result::Ok(std::result::Result::Ok(()))) => { + std::option::Option::Some(crate::runtime_error("source.configured_source_closed")) + }, + std::option::Option::Some(std::result::Result::Ok(std::result::Result::Err(error))) => std::option::Option::Some(error), + std::option::Option::Some(std::result::Result::Err(_)) => std::option::Option::Some(crate::runtime_error("source.task_join_failed")), + std::option::Option::None => std::option::Option::Some(crate::runtime_error("source.task_set_empty")), + }; + source_stop_sender.send_replace(true); + return drain_live_source_tasks(&mut children, first_fault).await; + } +} + +async fn drain_live_source_tasks( + children: &mut tokio::task::JoinSet>, + mut first_fault: std::option::Option, +) -> ksp_core_lib::Result<()> { + while let std::option::Option::Some(joined) = children.join_next().await { + if first_fault.is_some() { + continue; + } + first_fault = match joined { + std::result::Result::Ok(std::result::Result::Ok(())) => std::option::Option::None, + std::result::Result::Ok(std::result::Result::Err(error)) => std::option::Option::Some(error), + std::result::Result::Err(_) => std::option::Option::Some(crate::runtime_error("source.task_join_failed")), + }; + } + return match first_fault { + std::option::Option::Some(error) => std::result::Result::Err(error), + std::option::Option::None => std::result::Result::Ok(()), + }; +} + fn http_block_polling_live_source_key( network: &ksp_store_lib::RawNetworkId, polling_role: &ksp_onchain_transport_lib::HttpRoleName, diff --git a/crates/ksp-worker-raw-transaction-ingest-lib/tests/dependency_boundary.rs b/crates/ksp-worker-raw-transaction-ingest-lib/tests/dependency_boundary.rs index 8f0dd7c..d256c53 100644 --- a/crates/ksp-worker-raw-transaction-ingest-lib/tests/dependency_boundary.rs +++ b/crates/ksp-worker-raw-transaction-ingest-lib/tests/dependency_boundary.rs @@ -1,5 +1,5 @@ // file: crates/ksp-worker-raw-transaction-ingest-lib/tests/dependency_boundary.rs -// version: 22 +// version: 23 //! Dependency firewall canaries for the RAW transaction ingest Worker foundation. @@ -158,11 +158,7 @@ fn v0_3_12_pre_005_block_and_continuity_adapters_remain_private_and_transport_fa fn v0_3_12_pre_006_productive_source_uses_transport_session_bounded_coalescence_and_existing_admission() { let runtime = include_str!("../src/runtime.rs"); let resources = include_str!("../src/runtime_resources.rs"); - for required in [ - "runtime_resources.validate_single_source_activation()", - "children.spawn", - ".run_single_live_source(source_settings, stop_receiver, admission_sender, processing_frontier_sender)", - ] { + for required in ["children.spawn", ".run_live_sources(source_settings, stop_receiver, admission_sender, processing_frontier_sender)"] { assert!(runtime.contains(required), "required pre.006 supervisor wiring missing: {required}"); } for required in [ @@ -260,7 +256,7 @@ fn v0_3_13_pre_003_standard_logs_source_reuses_transport_facades_and_common_hydr ] { assert!(resources.contains(required), "required pre.003 standard logs contract missing: {required}"); } - assert!(runtime.contains("run_single_live_source")); + assert!(runtime.contains("run_live_sources")); for forbidden in [ "ksp_config_lib::", "ksp_job_backfill_lib::", @@ -468,6 +464,46 @@ fn manifest_dependency_names(section: &str) -> std::vec::Vec<&str> { return names; } +#[test] +fn v0_3_13_pre_007_multi_source_supervisor_and_inventory_remain_private_bounded_and_source_neutral() { + let root = include_str!("../src/lib.rs"); + let runtime = include_str!("../src/runtime.rs"); + let resources = include_str!("../src/runtime_resources.rs"); + for required in [ + "RawTransactionIngestSourceInventory", + "RawTransactionIngestSourceInventoryPublisher", + "for (entry_index, source) in self.sources.into_iter().enumerate()", + "tokio::task::JoinSet::new()", + "supervise_live_source_tasks", + "drain_live_source_tasks", + "source_stop_sender.send_replace(true)", + "processing_frontier_sender.clone()", + "source.configured_source_closed", + ] { + assert!(resources.contains(required), "required pre.007 multi-source supervisor contract missing: {required}"); + } + assert!(runtime.contains("run_live_sources")); + assert!(!runtime.contains("validate_single_source_activation")); + assert!(!resources.contains("multi_source_activation_pending")); + for forbidden in [ + "pub struct RawTransactionIngestSourceInventory", + "pub struct RawTransactionIngestSourceInventoryPublisher", + "unbounded_channel", + "ksp_config_lib::", + "ksp_job_backfill_lib::", + "ksp_store_postgres_lib::", + "reqwest::", + "tonic::", + "yellowstone_grpc_proto::", + ] { + assert!( + !root.contains(forbidden) && !runtime.contains(forbidden) && !resources.contains(forbidden), + "pre.007 crossed a private/bounded boundary: {forbidden}" + ); + } + return; +} + #[test] fn v0_3_12_pre_007_processing_frontier_remains_run_local_and_backend_neutral_after_continuity_extension() { let runtime = include_str!("../src/runtime.rs"); diff --git a/crates/ksp-worker-raw-transaction-ingest-lib/tests/hardening.rs b/crates/ksp-worker-raw-transaction-ingest-lib/tests/hardening.rs index e3f08ba..a6fc04c 100644 --- a/crates/ksp-worker-raw-transaction-ingest-lib/tests/hardening.rs +++ b/crates/ksp-worker-raw-transaction-ingest-lib/tests/hardening.rs @@ -1,5 +1,5 @@ // file: crates/ksp-worker-raw-transaction-ingest-lib/tests/hardening.rs -// version: 16 +// version: 17 //! External public, security, redaction and release-boundary hardening canaries for `pre.010`. @@ -397,7 +397,7 @@ fn v0_3_12_pre_006_runtime_resource_contract_opens_one_supervised_transport_sour assert!(resources.contains(required), "productive runtime-resource source behavior missing: {required}"); } assert!(runtime.contains("start_with_runtime_resources")); - assert!(runtime.contains("run_single_live_source(source_settings, stop_receiver, admission_sender, processing_frontier_sender)")); + assert!(runtime.contains("run_live_sources(source_settings, stop_receiver, admission_sender, processing_frontier_sender)")); for forbidden in ["ksp_config_lib::", "ksp_store_postgres_lib::", "reqwest::", "tonic::", "yellowstone_grpc_proto::"] { assert!(!resources.contains(forbidden) && !runtime.contains(forbidden), "pre.006 runtime source crossed a forbidden boundary: {forbidden}"); } @@ -718,3 +718,36 @@ fn v0_3_12_pre_009_hydration_retry_ownership_and_no_orphan_cleanup_are_explicit( assert!(!resources.contains("unbounded_channel"), "Worker introduced an unbounded channel"); return; } + +#[test] +fn v0_3_13_pre_007_multi_source_supervisor_is_fail_closed_joined_and_does_not_publish_source_identity() { + let root = include_str!("../src/lib.rs"); + let resources = include_str!("../src/runtime_resources.rs"); + for required in [ + "MAX_RAW_TRANSACTION_INGEST_LIVE_SOURCES", + "RawTransactionIngestSourceInventory", + "std::sync::Mutex", + "tokio::task::JoinSet", + "source.configured_source_closed", + "source.task_join_failed", + "source_stop_sender.send_replace(true)", + "drain_live_source_tasks", + "processing_frontier_slot = std::option::Option::None", + "saturating_add", + ] { + assert!(resources.contains(required), "required pre.007 supervisor hardening guard missing: {required}"); + } + for forbidden in [ + "pub struct RawTransactionIngestSourceInventory", + "pub struct RawTransactionIngestSourceInventoryPublisher", + "pub fn source_key(", + "source_keys:", + "unbounded_channel", + "primary_source", + "standby_source", + "first_provider_wins", + ] { + assert!(!root.contains(forbidden), "private source identity/scheduling policy leaked into public root: {forbidden}"); + } + return; +} diff --git a/crates/ksp-worker-raw-transaction-ingest-lib/tests/public_api.rs b/crates/ksp-worker-raw-transaction-ingest-lib/tests/public_api.rs index 4b0070d..7081d80 100644 --- a/crates/ksp-worker-raw-transaction-ingest-lib/tests/public_api.rs +++ b/crates/ksp-worker-raw-transaction-ingest-lib/tests/public_api.rs @@ -1,5 +1,5 @@ // file: crates/ksp-worker-raw-transaction-ingest-lib/tests/public_api.rs -// version: 16 +// version: 17 //! External public-surface proofs for the RAW transaction ingest Worker foundation. @@ -327,3 +327,12 @@ fn v0_3_12_pre_007_processing_frontier_snapshot_getters_are_public_and_processin } return; } + +#[test] +fn v0_3_13_pre_007_source_inventory_and_logical_keys_remain_private() { + let root = include_str!("../src/lib.rs"); + for forbidden in ["RawTransactionIngestSourceInventory", "RawTransactionIngestSourceInventoryPublisher", "RawTransactionIngestLiveSource", "source_key"] { + assert!(!root.contains(forbidden), "pre.007 private multi-source implementation leaked through crate root: {forbidden}"); + } + return; +} diff --git a/crates/ksp-worker-raw-transaction-ingest-lib/tests/release_completeness.rs b/crates/ksp-worker-raw-transaction-ingest-lib/tests/release_completeness.rs index e42ce72..8dac106 100644 --- a/crates/ksp-worker-raw-transaction-ingest-lib/tests/release_completeness.rs +++ b/crates/ksp-worker-raw-transaction-ingest-lib/tests/release_completeness.rs @@ -1,5 +1,5 @@ // file: crates/ksp-worker-raw-transaction-ingest-lib/tests/release_completeness.rs -// version: 14 +// version: 15 //! Release-completeness canaries through the `pre.010` public/release/security hardening tranche. @@ -127,6 +127,7 @@ fn pre_010_external_hardening_suite_is_present_and_scoped() { "v0_3_13_pre_004_standard_block_redaction_version_and_null_guards_are_explicit", "v0_3_13_pre_005_helius_transaction_redaction_full_reference_and_tier_neutrality_are_explicit", "v0_3_13_pre_006_http_block_polling_is_bounded_run_local_stop_preemptible_and_redacted", + "v0_3_13_pre_007_multi_source_supervisor_is_fail_closed_joined_and_does_not_publish_source_identity", ] { assert!(hardening.contains(required), "required pre.010 hardening canary missing: {required}"); } @@ -144,6 +145,7 @@ fn pre_010_external_hardening_suite_is_present_and_scoped() { assert!(dependency_boundary.contains("v0_3_13_pre_004_standard_block_source_is_direct_raw_and_transport_facade_only")); assert!(dependency_boundary.contains("v0_3_13_pre_005_helius_transaction_source_reuses_transport_facade_and_common_hydration_only")); assert!(dependency_boundary.contains("v0_3_13_pre_006_http_block_polling_reuses_transport_facades_without_becoming_backfill")); + assert!(dependency_boundary.contains("v0_3_13_pre_007_multi_source_supervisor_and_inventory_remain_private_bounded_and_source_neutral")); let public_api = include_str!("public_api.rs"); assert!(public_api.contains("pre_003_kind_code_and_settings_are_consumable_from_crate_root")); assert!(public_api.contains("pre_004_start_handle_and_terminal_future_are_consumable_without_public_join_handle")); @@ -154,6 +156,7 @@ fn pre_010_external_hardening_suite_is_present_and_scoped() { assert!(public_api.contains("v0_3_13_pre_003_standard_logs_runtime_resource_surface_is_typed_and_transport_owned")); assert!(public_api.contains("v0_3_13_pre_004_standard_block_runtime_resource_surface_is_typed_and_transport_owned")); assert!(public_api.contains("v0_3_13_pre_006_http_block_polling_runtime_resource_surface_is_bounded_and_transport_owned")); + assert!(public_api.contains("v0_3_13_pre_007_source_inventory_and_logical_keys_remain_private")); assert!(public_api.contains("v0_3_12_pre_007_processing_frontier_snapshot_getters_are_public_and_processing_only")); assert!(public_api.contains("pre_008_snapshot_surface_and_common_projection_are_public_and_stable")); return; diff --git a/crates/ksp-worker-raw-transaction-ingest-lib/unit_tests/runtime_resources.rs b/crates/ksp-worker-raw-transaction-ingest-lib/unit_tests/runtime_resources.rs index 758afb6..cccc5f9 100644 --- a/crates/ksp-worker-raw-transaction-ingest-lib/unit_tests/runtime_resources.rs +++ b/crates/ksp-worker-raw-transaction-ingest-lib/unit_tests/runtime_resources.rs @@ -1,5 +1,5 @@ // file: crates/ksp-worker-raw-transaction-ingest-lib/unit_tests/runtime_resources.rs -// version: 18 +// version: 19 fn grpc_endpoint(cluster: &str) -> std::option::Option { return grpc_endpoint_with_identity(cluster, "yellowstone-fixture", "fixture-provider"); @@ -959,24 +959,118 @@ async fn v0_3_13_pre_002_runtime_resources_reject_duplicate_and_cross_network_so return; } +#[test] +fn v0_3_13_pre_007_source_inventory_aggregates_frontier_and_lifecycle_conservatively() { + let source_keys = std::vec![[1_u8; 32], [2_u8; 32]]; + let inventory = std::sync::Arc::new(std::sync::Mutex::new(super::RawTransactionIngestSourceInventory::new(source_keys))); + let (aggregate_sender, aggregate_receiver) = tokio::sync::watch::channel(crate::RawTransactionIngestProcessingFrontierProjection::empty()); + let first = super::RawTransactionIngestSourceInventoryPublisher { + aggregate_sender: aggregate_sender.clone(), + entry_index: 0, + inventory: std::sync::Arc::clone(&inventory), + source_key: [1_u8; 32], + }; + let second = super::RawTransactionIngestSourceInventoryPublisher { aggregate_sender, entry_index: 1, inventory, source_key: [2_u8; 32] }; + first.publish( + crate::RawTransactionIngestProcessingFrontierProjection::new(1, std::option::Option::Some(50), std::option::Option::Some(45)).with_source_continuity( + std::option::Option::Some(crate::RawTransactionIngestSourceState::Active), + 1, + 2, + 0, + ), + ); + let first_aggregate = *aggregate_receiver.borrow(); + assert_eq!(first_aggregate.hydration_pending(), 1); + assert_eq!(first_aggregate.processing_frontier_slot(), std::option::Option::None); + assert_eq!(first_aggregate.oldest_pending_slot(), std::option::Option::Some(45)); + assert_eq!(first_aggregate.source_state(), std::option::Option::Some(crate::RawTransactionIngestSourceState::Active)); + second.publish( + crate::RawTransactionIngestProcessingFrontierProjection::new(2, std::option::Option::Some(42), std::option::Option::Some(40)).with_source_continuity( + std::option::Option::Some(crate::RawTransactionIngestSourceState::Reconnecting), + 3, + 4, + 1, + ), + ); + let aggregate = *aggregate_receiver.borrow(); + assert_eq!(aggregate.hydration_pending(), 3); + assert_eq!(aggregate.processing_frontier_slot(), std::option::Option::Some(42)); + assert_eq!(aggregate.oldest_pending_slot(), std::option::Option::Some(40)); + assert_eq!(aggregate.source_state(), std::option::Option::Some(crate::RawTransactionIngestSourceState::Reconnecting)); + assert_eq!(aggregate.source_reconnect_total(), 4); + assert_eq!(aggregate.source_replay_attempt_total(), 6); + assert_eq!(aggregate.source_continuity_gap_total(), 1); + return; +} + #[tokio::test(flavor = "current_thread")] -async fn v0_3_13_pre_002_multi_source_activation_fails_closed_until_supervisor_tranche() { - let first = match source_with_identity("devnet", "source-a", "tx", std::option::Option::None) { - std::option::Option::Some(value) => value, - std::option::Option::None => return, +async fn v0_3_13_pre_007_source_supervisor_joins_all_children_on_stop() { + let active = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0)); + let (stop_sender, stop_receiver) = tokio::sync::watch::channel(false); + let (source_stop_sender, source_stop_receiver) = tokio::sync::watch::channel(false); + let mut children = tokio::task::JoinSet::new(); + for _ in 0..3 { + let active = std::sync::Arc::clone(&active); + let mut source_stop_receiver = source_stop_receiver.clone(); + let _abort_handle = children.spawn(async move { + active.fetch_add(1, std::sync::atomic::Ordering::AcqRel); + loop { + let changed = source_stop_receiver.changed().await; + if changed.is_err() || *source_stop_receiver.borrow() { + active.fetch_sub(1, std::sync::atomic::Ordering::AcqRel); + return std::result::Result::Ok(()); + } + } + }); + } + let supervisor = tokio::spawn(super::supervise_live_source_tasks(stop_receiver, source_stop_sender, children)); + for _ in 0..64 { + if active.load(std::sync::atomic::Ordering::Acquire) == 3 { + break; + } + tokio::task::yield_now().await; + } + assert_eq!(active.load(std::sync::atomic::Ordering::Acquire), 3); + stop_sender.send_replace(true); + let joined = supervisor.await; + let result = match joined { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, }; - let second = match source_with_identity("devnet", "source-b", "tx", std::option::Option::None) { - std::option::Option::Some(value) => value, - std::option::Option::None => return, - }; - let mut resources = crate::RawTransactionIngestRuntimeResources::new(first); - assert!(resources.try_push_yellowstone_source(second).is_ok()); - let error = match resources.validate_single_source_activation() { - std::result::Result::Ok(()) => return, - std::result::Result::Err(error) => error, - }; - assert_eq!(error.code(), crate::ERROR_CODE_RAW_TRANSACTION_INGEST_RUNTIME_INVALID); - assert!(error.context().iter().any(|context| return context.value() == "runtime_resources.multi_source_activation_pending")); + assert!(result.is_ok()); + assert_eq!(active.load(std::sync::atomic::Ordering::Acquire), 0); + return; +} + +#[tokio::test(flavor = "current_thread")] +async fn v0_3_13_pre_007_source_failure_stops_and_joins_sibling_sources() { + let barrier = std::sync::Arc::new(tokio::sync::Barrier::new(2)); + let sibling_active = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)); + let (_stop_sender, stop_receiver) = tokio::sync::watch::channel(false); + let (source_stop_sender, source_stop_receiver) = tokio::sync::watch::channel(false); + let mut children = tokio::task::JoinSet::new(); + let failing_barrier = std::sync::Arc::clone(&barrier); + let _failing_abort_handle = children.spawn(async move { + let _barrier_wait = failing_barrier.wait().await; + return std::result::Result::Err(crate::runtime_error("test.pre_007_source_failed")); + }); + let sibling_barrier = std::sync::Arc::clone(&barrier); + let sibling_active_for_task = std::sync::Arc::clone(&sibling_active); + let mut sibling_stop_receiver = source_stop_receiver.clone(); + let _sibling_abort_handle = children.spawn(async move { + sibling_active_for_task.store(true, std::sync::atomic::Ordering::Release); + let _barrier_wait = sibling_barrier.wait().await; + loop { + let changed = sibling_stop_receiver.changed().await; + if changed.is_err() || *sibling_stop_receiver.borrow() { + sibling_active_for_task.store(false, std::sync::atomic::Ordering::Release); + return std::result::Result::Ok(()); + } + } + }); + let result = super::supervise_live_source_tasks(stop_receiver, source_stop_sender, children).await; + assert!(result.is_err()); + assert!(!sibling_active.load(std::sync::atomic::Ordering::Acquire)); return; } diff --git a/deltas/0.3.13/pre.007.md b/deltas/0.3.13/pre.007.md new file mode 100644 index 0000000..f657c09 --- /dev/null +++ b/deltas/0.3.13/pre.007.md @@ -0,0 +1,218 @@ + + + +# Delta 0.3.13-pre.007 — supervision et inventaire multi-source + +## Base requise + +```text +livraison précédente : 0.3.13-pre.006-fix.001 +Cargo base : 0.3.13-pre.6.fix.1 +delta base : deltas/0.3.13/pre.006-fix.001.md +archive delta base : ksp-general-0.3.13-pre.006-fix.001.zip +``` + +Le gate opérateur communiqué le 10 septembre 2026 est vert sur toutes les commandes exécutées : `cargo fmt`, `cargo fmt --check`, audits Rust/Markdown, `cargo check --workspace`, Clippy strict et toutes les suites de `ksp-worker-raw-transaction-ingest-lib` (`88` unit, `4` cross-layer, `13` dependency-boundary, `20` hardening, `14` public-api, `4` release-completeness, `0` doc-test). Le log reçu ne contient pas les sorties séparées `cargo test -p ksp-onchain-transport-lib` ni les trois commandes `cargo tree`; elles ne sont donc pas déclarées PASS dans ce delta. + +## Objectif + +Activer réellement la collection `1..32` déjà composable depuis `pre.002` : + +```text +RawTransactionIngestRuntimeResources + -> validation complète réseau / doublons / borne + -> spawn de toutes les sources configurées + -> inventaire privé source_key -> latest state/frontier + -> projection frontier source-neutral agrégée + -> terminalité conservatrice si une source disparaît ou échoue +``` + +Aucun provider n'est traité comme primaire, fallback ou standby. + +## Version + +```text +livraison : 0.3.13-pre.007 +workspace.package.version : 0.3.13-pre.7 +archive : ksp-general-0.3.13-pre.007.zip +``` + +`Cargo.toml` passe de la version de fichier `551` à `552`. + +## Supervisor multi-source + +`RawTransactionIngestWorker::start_with_runtime_resources` ne vérifie plus `validate_single_source_activation`. Après validation du réseau, le runtime délègue à `RawTransactionIngestRuntimeResources::run_live_sources`. + +Le supervisor privé : + +```text +crée un stop channel source-owned +crée un JoinSet borné par la collection 1..32 +spawn chaque source configurée exactement une fois +clone uniquement les handles nécessaires +rejoint toutes les tasks avant de rendre son résultat +``` + +La vieille erreur `runtime_resources.multi_source_activation_pending` disparaît du code production. + +## Terminalité conservatrice + +La politique reste fail-closed : + +```text +stop Worker + -> fanout stop vers toutes les sources + -> join complet + -> Ok seulement si les sources ferment sans erreur + +source Err + -> stop immédiat des autres sources + -> join complet + -> faute source terminale + +source Ok avant stop Worker + -> fermeture inattendue d'une source configurée + -> stop des autres sources + -> faute terminale + +JoinError + -> faute terminale sûre +``` + +Le Worker ne suppose pas qu'une autre source couvre les mêmes filtres, le même provider universe ou la même complétude. + +## Inventaire privé et processing frontier + +`RawTransactionIngestSourceInventory` et `RawTransactionIngestSourceInventoryPublisher` restent privés au module runtime resources. L'inventaire est initialisé depuis les `source_key` déjà validées et possède exactement une entrée latest-value par source du run. + +Chaque source conserve son propre publisher local. Un wrapper privé reporte ses projections dans l'inventaire puis publie une projection globale conservative : + +```text +hydration_pending : somme saturante temporaire des sources +oldest_pending_slot : minimum global +processing_frontier_slot : None si une source n'a pas encore de frontier + sinon minimum de toutes les frontiers +source_state : Failed > Reconnecting > Closing > Active > Closed/None +reconnect/replay/gap : agrégation saturante, jamais wrap arithmétique +``` + +Le choix du minimum n'est pas une preuve de coverage blockchain. La frontier reste run-local et source-neutral. + +## Propriété d'ownership + +Les sources physiques continuent d'appartenir à leurs façades Transport existantes. Le Worker ne crée ni client reqwest, ni socket alternatif, ni SDK provider. Le supervisor ajoute uniquement l'ownership des tasks source et de leur lifecycle commun. + +Une source qui termine avant le stop est explicitement marquée `Closed` ou `Failed` dans l'inventaire avant le résultat du supervisor. Les identités `source_key` restent privées et ne sont jamais projetées dans `RawTransactionIngestSnapshot` ou le crate root. + +## Preuves ajoutées + +Unit tests déterministes : + +```text +inventaire 2 sources : pending/frontier/state/counters agrégés conservativement +stop supervisor : trois tasks source actives reçoivent le stop et sont toutes jointes +source failure : la source sœur reçoit le stop et est jointe avant retour de faute +``` + +Canaris externes : + +```text +dependency_boundary : run_live_sources, JoinSet, inventaire privé, vieille barrière absente +hardening : stop/fault/join fail-closed, aucune policy primary/standby, aucun canal unbounded +public_api : inventaire/live-source/source_key non exportés +``` + +Les canaris historiques `0.3.12` qui cherchaient `run_single_live_source` sont mis à jour vers le supervisor multi-source au lieu d'être supprimés. + +## Frontière de tranche + +`pre.007` active la simultanéité, mais ne ferme pas encore la convergence transactionnelle cross-source : + +```text +pas de coalescence globale de la même signature entre plusieurs sources +pas d'unique hydration cross-source garantie +pas de fusion de plusieurs provenance/observation seeds vers une acquisition canonique unique +pas encore de record_raw_transaction_observation pour les observations supplémentaires convergées +pas encore de fairness durcie duplicate-storm +pas encore de source_total/source_active/source_failed publics +``` + +Les coordinators d'hydration des sources reference-bearing restent donc source-local pendant cette tranche. La fermeture de la coalescence globale et des observations multiples appartient à `pre.008`, puis les bornes/fairness renforcées à `pre.009`. + +## Documentation + +Le README du crate décrit désormais le supervisor 1..32, la terminalité conservative et la frontier agrégée. `USAGE.md` reste version-neutral et montre comment composer plusieurs sources via les `try_push_*` existants sans mentionner le numéro de prerelease. + +Le plan et la validation consignent le gate opérateur réellement reçu et distinguent explicitement ce qui reste reporté à `pre.008+`. + +## Fichiers modifiés + +```text +Cargo.toml +crates/ksp-worker-raw-transaction-ingest-lib/README.md +crates/ksp-worker-raw-transaction-ingest-lib/USAGE.md +crates/ksp-worker-raw-transaction-ingest-lib/src/runtime.rs +crates/ksp-worker-raw-transaction-ingest-lib/src/runtime_resources.rs +crates/ksp-worker-raw-transaction-ingest-lib/tests/dependency_boundary.rs +crates/ksp-worker-raw-transaction-ingest-lib/tests/hardening.rs +crates/ksp-worker-raw-transaction-ingest-lib/tests/public_api.rs +crates/ksp-worker-raw-transaction-ingest-lib/tests/release_completeness.rs +crates/ksp-worker-raw-transaction-ingest-lib/unit_tests/runtime_resources.rs +docs/plans/034-V0_3_13_MULTI_SOURCE_LIVE_CONVERGENCE_PLAN.md +docs/validation/030-V0_3_13_MULTI_SOURCE_LIVE_CONVERGENCE.md +``` + +## Fichiers ajoutés + +```text +deltas/0.3.13/pre.007.md +``` + +## Fichiers supprimés + +```text +aucun +``` + +## Validation dans l'environnement d'assemblage + +Le toolchain Rust/Cargo/Rustfmt n'est pas disponible dans l'environnement d'assemblage. Les commandes Cargo post-`pre.007` restent donc `NON EXÉCUTÉ LOCAL`. Les audits statiques KSP, le scan normatif, les versions de fichiers et le diff exhaustif sont exécutés sur l'état exact avant packaging. + +## Résultats statiques finaux de l'assemblage + +```text +General Rust rule audit: clean +Rust export completeness audit: 0 candidate(s) +KSP workspace Rust rule audit: clean +Markdown table audit: clean (340 tables, 841 files) +Normative rule definitions: 489 +Unique normative IDs: 489 +Duplicates: 0 +``` + +Contrôle ciblé du supervisor : + +```text +ancienne barrière validate_single_source_activation : absente +runtime_resources.multi_source_activation_pending : absent du code production +inventaire source public : absent +source_key public : absent +unbounded_channel : absent +nouvelle dépendance : aucune +``` + +## Gate opérateur requis avant pre.008 + +```bash +cargo fmt --all +cargo fmt --all -- --check +python3 scripts/audit_rust_workspace_rules.py +python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas +cargo check --workspace +cargo clippy --workspace --all-targets --all-features -- -D warnings +cargo test -p ksp-onchain-transport-lib +cargo test -p ksp-worker-raw-transaction-ingest-lib +cargo tree -p ksp-worker-raw-transaction-ingest-lib --edges normal +cargo tree -p ksp-worker-raw-transaction-ingest-lib -e features +cargo tree --duplicates +``` diff --git a/docs/plans/034-V0_3_13_MULTI_SOURCE_LIVE_CONVERGENCE_PLAN.md b/docs/plans/034-V0_3_13_MULTI_SOURCE_LIVE_CONVERGENCE_PLAN.md index 7096fe1..160a8d2 100644 --- a/docs/plans/034-V0_3_13_MULTI_SOURCE_LIVE_CONVERGENCE_PLAN.md +++ b/docs/plans/034-V0_3_13_MULTI_SOURCE_LIVE_CONVERGENCE_PLAN.md @@ -1,5 +1,5 @@ - + # Plan v0.3.13 — WS standard / Helius / HTTP live + convergence multi-source RawTransaction @@ -1145,3 +1145,104 @@ L'empreinte de profil inclut les identités sûres endpoint/provider et les capa La supervision simultanée, le lifecycle par source et l'inventaire multi-source restent exclusivement `pre.007`. `pre.006` ne transforme pas le poller HTTP en Backfill et ne crée aucun checkpoint durable inter-run. +## 56. Gate opérateur pre.006-fix.001 reçu avant pre.007 + +Le gate opérateur communiqué le 10 septembre 2026 est vert sur le périmètre exécuté après `pre.006-fix.001` : + +```text +cargo fmt --all + --check : PASS +audit Rust général : clean +export completeness : 0 candidate +workspace Rust rules : clean +Markdown : clean, 340 tables / 840 files +cargo check --workspace : PASS +cargo clippy --workspace --all-targets --all-features -- -D warnings : PASS +worker unit : 88/88 PASS +cross_layer_completeness : 4/4 PASS +dependency_boundary : 13/13 PASS +hardening : 20/20 PASS +public_api : 14/14 PASS +release_completeness : 4/4 PASS +doc-tests Worker : 0/0 PASS +``` + +Le log communiqué ne contient pas les sorties séparées `cargo test -p ksp-onchain-transport-lib` ni les trois `cargo tree`; elles restent à reprendre dans un gate technique ultérieur et ne sont pas déclarées PASS ici. + +## 57. Implémentation pre.007 — supervisor et inventaire multi-source + +La barrière `runtime_resources.multi_source_activation_pending` disparaît. Après validation réseau/identité/borne de la collection, `RawTransactionIngestRuntimeResources::run_live_sources` consomme les `1..32` sources et lance chacune dans un `tokio::task::JoinSet` privé. + +Le supervisor source possède : + +```text +un stop receiver Worker +un stop sender privé partagé vers toutes les sources +un JoinSet de toutes les tasks source +un inventaire privé borné par source_key +le sender de frontier agrégée vers le supervisor Worker +``` + +Il n'existe aucun ordre de priorité, source primaire, standby ou first-provider-wins. Toutes les sources configurées sont démarrées dans le même run. + +## 58. Terminalité, stop et joins pre.007 + +Politique conservatrice : + +```text +stop Worker -> stop toutes les sources -> join toutes -> résultat supervisor +source Err -> stop des sœurs -> join toutes -> faute source terminale +source Ok avant stop Worker -> perte inattendue d'une source configurée -> faute terminale +join panic/cancel -> faute terminale sûre +``` + +Cette politique ne tente aucun failover implicite. KSP ne possède encore aucune preuve que deux sources couvrent le même ensemble de transactions ou de filtres. + +Le supervisor imbriqué retourne son résultat au supervisor Worker existant, qui conserve la sémantique globale `source_failed`, la transition Stopping et le drain borné admission/persistence déjà établis. + +## 59. Inventaire et processing frontier multi-source + +L'inventaire privé contient au plus 32 couples logiques `source_key -> latest RawTransactionIngestProcessingFrontierProjection`. Les clés ne sont jamais publiées par l'API ou les snapshots. + +Chaque source continue d'émettre sa projection latest-value locale. Un wrapper privé la reporte dans l'inventaire, qui publie ensuite une projection source-neutral agrégée : + +```text +hydration_pending = somme saturante temporaire des projections source +oldest_pending_slot = minimum des pending connus +processing_frontier_slot = None tant qu'une source n'a pas de frontier ; sinon minimum de toutes les frontiers +source_state = Failed > Reconnecting > Closing > Active > Closed/None +reconnect/replay/gap counters = agrégés sans wrap +``` + +Le choix du minimum est volontairement conservateur : une source plus avancée ne prouve jamais que les autres couvrent le même univers. Cette frontier reste run-local et ne devient ni checkpoint durable ni preuve de completeness blockchain. + +## 60. Frontière de tranche pre.007 + +`pre.007` active la simultanéité et le lifecycle source mais ne ferme pas encore la convergence transactionnelle cross-source. En particulier : + +```text +pas encore de coalescence globale entre signaux de sources différentes +pas encore d'unique hydration cross-source garantie +pas encore de lot canonique multi-observations +pas encore de persistence observation supplémentaire cross-source +pas encore de fairness durcie contre duplicate storm +pas encore de nouveaux compteurs publics source_total/source_active/source_failed +``` + +Les coordinators d'hydration des sources reference-bearing restent donc source-local pendant cette tranche. La borne absolue reste finie par source et par nombre de sources ; la borne globale/coalescence prévue par l'architecture sera fermée en `pre.008/pre.009` avant la release stable. + +## 61. Gate opérateur requis avant pre.008 + +```bash +cargo fmt --all +cargo fmt --all -- --check +python3 scripts/audit_rust_workspace_rules.py +python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas +cargo check --workspace +cargo clippy --workspace --all-targets --all-features -- -D warnings +cargo test -p ksp-onchain-transport-lib +cargo test -p ksp-worker-raw-transaction-ingest-lib +cargo tree -p ksp-worker-raw-transaction-ingest-lib --edges normal +cargo tree -p ksp-worker-raw-transaction-ingest-lib -e features +cargo tree --duplicates +``` + diff --git a/docs/validation/030-V0_3_13_MULTI_SOURCE_LIVE_CONVERGENCE.md b/docs/validation/030-V0_3_13_MULTI_SOURCE_LIVE_CONVERGENCE.md index 7c2ffee..7b6ca30 100644 --- a/docs/validation/030-V0_3_13_MULTI_SOURCE_LIVE_CONVERGENCE.md +++ b/docs/validation/030-V0_3_13_MULTI_SOURCE_LIVE_CONVERGENCE.md @@ -1,5 +1,5 @@ - + # Validation v0.3.13 — WS standard / Helius / HTTP live + convergence multi-source @@ -1155,3 +1155,90 @@ cargo tree -p ksp-worker-raw-transaction-ingest-lib -e features cargo tree --duplicates ``` +## 57. Gate opérateur pre.006-fix.001 reçu + +Preuve communiquée le 10 septembre 2026 sur les commandes exécutées : + +```text +fmt : PASS +audits Rust : clean / export completeness 0 +Markdown : clean, 340 tables / 840 files +cargo check --workspace : PASS +clippy workspace all-targets all-features -D warnings : PASS +worker unit : 88/88 PASS +cross-layer : 4/4 PASS +dependency boundary : 13/13 PASS +hardening : 20/20 PASS +public API : 14/14 PASS +release completeness : 4/4 PASS +``` + +Les sorties Transport ciblées et `cargo tree` demandées dans le gate complet ne figurent pas dans le log reçu et ne sont donc pas déclarées PASS. + +## 58. Surface pre.007 matérialisée + +```text +RawTransactionIngestRuntimeResources::run_live_sources +RawTransactionIngestSourceInventory privé +RawTransactionIngestSourceInventoryPublisher privé +JoinSet privé 1..32 sources +stop fanout privé +drain/join de toutes les sources +projection processing frontier agrégée +suppression de multi_source_activation_pending +``` + +Aucune nouvelle API publique n'est nécessaire : les méthodes `from_*`, `try_push_*`, `source_count` et `start_with_runtime_resources` existantes suffisent à composer puis lancer plusieurs sources. + +## 59. Invariants supervisor/source inventory vérifiés statiquement + +La tranche exige : + +```text +1..32 sources validées avant run +toutes les sources présentes sont spawnees +aucun sous-ensemble silencieux +aucune politique primary/standby/fallback +une source fautive arrête les autres et le supervisor les join +une fermeture source avant stop est terminale +source_key reste privée +inventaire borné initialisé depuis les source_key validées +frontier globale conservatrice = minimum seulement si toutes les sources ont une frontier +oldest pending global = minimum +état source agrégé sans provider material +``` + +Les tests déterministes ajoutés utilisent des tasks factices pour prouver le join de trois sources au stop et le stop/join d'une sœur après faute, sans I/O réseau. + +## 60. Validation locale d'assemblage pre.007 + +Le toolchain Cargo/Rustfmt n'est pas disponible dans l'environnement d'assemblage. Les commandes Cargo post-modification restent `NON EXÉCUTÉ LOCAL`; seuls les audits statiques effectivement exécutés sont déclarés PASS dans le delta. + +## 61. Non-claims pre.007 + +```text +pas de coalescence cross-source complète avant pre.008 +pas d'unique hydration globale cross-source garantie avant pre.008 +pas d'observations Store multiples convergées avant pre.008 +pas de fairness duplicate-storm fermée avant pre.009 +pas de nouveaux compteurs/health publics multi-source avant pre.010 +pas de failover/degraded non-terminal d'une source +pas de preuve de coverage entre sources +``` + +## 62. Gate opérateur requis avant pre.008 + +```bash +cargo fmt --all +cargo fmt --all -- --check +python3 scripts/audit_rust_workspace_rules.py +python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas +cargo check --workspace +cargo clippy --workspace --all-targets --all-features -- -D warnings +cargo test -p ksp-onchain-transport-lib +cargo test -p ksp-worker-raw-transaction-ingest-lib +cargo tree -p ksp-worker-raw-transaction-ingest-lib --edges normal +cargo tree -p ksp-worker-raw-transaction-ingest-lib -e features +cargo tree --duplicates +``` +