v0.3.13-pre.007

This commit is contained in:
2026-09-10 17:33:02 +02:00
parent 72df965a9c
commit 8bb530e9b4
13 changed files with 905 additions and 78 deletions

View File

@@ -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"

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-worker-raw-transaction-ingest-lib/README.md -->
<!-- version: 6 -->
<!-- version: 7 -->
# 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

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-worker-raw-transaction-ingest-lib/USAGE.md -->
<!-- version: 6 -->
<!-- version: 7 -->
# 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

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-worker-raw-transaction-ingest-lib/src/runtime.rs
// version: 13
// version: 14
type PersistencePort = std::sync::Arc<dyn crate::RawTransactionIngestPersistencePort + 'static>;
type PersistenceTasks = tokio::task::JoinSet<ksp_core_lib::Result<crate::RawTransactionIngestPersistenceOutcome>>;
@@ -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<ksp_store_lib::Store>,
@@ -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;
});
},
);

View File

@@ -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<crate::RawTransactionIngestProcessingFrontierProjection>,
}
#[derive(Clone)]
struct RawTransactionIngestSourceInventoryPublisher {
aggregate_sender: tokio::sync::watch::Sender<crate::RawTransactionIngestProcessingFrontierProjection>,
entry_index: usize,
inventory: std::sync::Arc<std::sync::Mutex<RawTransactionIngestSourceInventory>>,
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<bool>,
admission_sender: tokio::sync::mpsc::Sender<crate::RawTransactionIngress>,
processing_frontier_sender: tokio::sync::watch::Sender<crate::RawTransactionIngestProcessingFrontierProjection>,
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<RawTransactionIngestLiveSource>,
}
@@ -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<bool>,
admission_sender: tokio::sync::mpsc::Sender<crate::RawTransactionIngress>,
processing_frontier_sender: tokio::sync::watch::Sender<crate::RawTransactionIngestProcessingFrontierProjection>,
) -> 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::<std::vec::Vec<_>>();
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<u64>, second: std::option::Option<u64>) -> std::option::Option<u64> {
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<bool>,
source_stop_sender: tokio::sync::watch::Sender<bool>,
mut children: tokio::task::JoinSet<ksp_core_lib::Result<()>>,
) -> 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<ksp_core_lib::Result<()>>,
mut first_fault: std::option::Option<ksp_core_lib::Error>,
) -> 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,

View File

@@ -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");

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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<ksp_onchain_transport_lib::YellowstoneGrpcEndpointSettings> {
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;
}

218
deltas/0.3.13/pre.007.md Normal file
View File

@@ -0,0 +1,218 @@
<!-- file: deltas/0.3.13/pre.007.md -->
<!-- version: 1 -->
# 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
```

View File

@@ -1,5 +1,5 @@
<!-- file: docs/plans/034-V0_3_13_MULTI_SOURCE_LIVE_CONVERGENCE_PLAN.md -->
<!-- version: 6 -->
<!-- version: 7 -->
# 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
```

View File

@@ -1,5 +1,5 @@
<!-- file: docs/validation/030-V0_3_13_MULTI_SOURCE_LIVE_CONVERGENCE.md -->
<!-- version: 6 -->
<!-- version: 7 -->
# 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
```