v0.3.13-pre.009
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
# file: Cargo.toml
|
||||
# version: 555
|
||||
# version: 556
|
||||
|
||||
[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.8.fix.2"
|
||||
version = "0.3.13-pre.9"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: crates/ksp-worker-raw-transaction-ingest-lib/README.md -->
|
||||
<!-- version: 8 -->
|
||||
<!-- version: 9 -->
|
||||
|
||||
# ksp-worker-raw-transaction-ingest-lib
|
||||
|
||||
@@ -171,14 +171,19 @@ Le shutdown est borné par `shutdown_drain_timeout`. Le supervisor multi-source
|
||||
|
||||
La queue centrale est un `tokio::sync::mpsc` privé borné par `admission_queue_capacity`. Les sources internes subissent la backpressure ; aucune queue non bornée ni silent drop n'est autorisé.
|
||||
|
||||
Le Worker possède des coordinateurs source-neutral Yellowstone, Standard Logs et Helius Transaction branchés sur un registre global d'hydration partagé. Standard Block et HTTP Block Polling n'entrent pas dans ce registre lorsqu'une transaction est direct-qualified :
|
||||
Le Worker possède des coordinateurs source-neutral Yellowstone, Standard Logs et Helius Transaction branchés sur un registre global d'hydration partagé. Standard Block et HTTP Block Polling n'entrent pas dans ce registre lorsqu'une transaction est direct-qualified.
|
||||
|
||||
Les sources reference-bearing reçoivent des quotas déterministes dont la somme reste exactement dans les bornes techniques configurées :
|
||||
|
||||
```text
|
||||
in-flight hydration <= persistence_concurrency
|
||||
pending source signals <= admission_queue_capacity
|
||||
somme pending source signals <= admission_queue_capacity
|
||||
somme hydration tasks in flight <= persistence_concurrency
|
||||
source reference-bearing active => quota pending >= 1 et quota in-flight >= 1
|
||||
```
|
||||
|
||||
Les signaux partageant le même `(network, signature, commitment)` sont coalescés cross-source avant le fan-out HTTP. Après canonicalisation, une cache run-local bornée sérialise les acquisitions de même `(network, signature)` : la première passe par l'écriture atomique entity + observation, les suivantes de contenu identique ajoutent uniquement leur observation déterministe. Un hash canonique divergent devient un content conflict terminal.
|
||||
Pour garantir simultanément ces bornes et l'absence de starvation structurelle, le démarrage échoue avant spawn si le nombre de sources reference-bearing dépasse `admission_queue_capacity` ou `persistence_concurrency`. Un sémaphore global protège en plus l'ouverture effective des hydrations HTTP.
|
||||
|
||||
Les signaux partageant le même `(network, signature, commitment)` sont coalescés cross-source avant le fan-out HTTP. Après canonicalisation, une cache run-local bornée sérialise les acquisitions de même `(network, signature)` : la première passe par l'écriture atomique entity + observation, les suivantes de contenu canonique identique ajoutent uniquement leur observation déterministe. Une divergence de slot, block time, format ou hash canonique devient un content conflict terminal ; aucune majorité, préférence provider ou overwrite n'est appliqué. Le Store conserve son guard durable final.
|
||||
|
||||
Les retries/reroutages HTTP appartiennent à `ksp-onchain-transport-lib`. Le Worker ne possède pas une seconde boucle de retry autour de `getTransaction`.
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: crates/ksp-worker-raw-transaction-ingest-lib/USAGE.md -->
|
||||
<!-- version: 8 -->
|
||||
<!-- version: 9 -->
|
||||
|
||||
# Utilisation de ksp-worker-raw-transaction-ingest-lib
|
||||
|
||||
@@ -255,6 +255,19 @@ Yellowstone Account/Ping/Pong/Entry -> sans RAW Transaction dans cette verticale
|
||||
|
||||
Les signaux de même `(network, signature, commitment)` sont coalescés globalement avant l'hydration HTTP. Si plusieurs sources produisent ensuite la même transaction canonique, le Worker conserve une seule entité RAW et enregistre séparément les observations déterministes propres à chaque source. Le Worker ne possède pas une boucle de retry HTTP : reroutage/retry/backoff restent dans `ksp-onchain-transport-lib`.
|
||||
|
||||
Les sources qui nécessitent `getTransaction` partagent des quotas bornés et déterministes. Pour une composition valide :
|
||||
|
||||
```text
|
||||
nombre de sources avec hydration <= admission_queue_capacity
|
||||
nombre de sources avec hydration <= persistence_concurrency
|
||||
somme de leurs pending <= admission_queue_capacity
|
||||
somme de leurs tâches d'hydration actives <= persistence_concurrency
|
||||
```
|
||||
|
||||
Ces contraintes garantissent au moins une part à chaque source reference-bearing sans introduire de scheduler pondéré. Si les settings ne permettent pas cette répartition, le démarrage échoue avant spawn avec `runtime_invalid`; le caller doit augmenter la capacité concernée ou réduire le nombre de sources nécessitant une hydration.
|
||||
|
||||
Pour une même identité `(network, signature)`, une divergence canonique de slot, block time, format ou hash est un `content_conflict`. Le Worker ne choisit ni majorité ni provider préféré et n'écrase pas un contenu divergent ; le Store reste l'autorité durable finale du conflit.
|
||||
|
||||
## Observer le snapshot concret
|
||||
|
||||
```rust
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-worker-raw-transaction-ingest-lib/src/persistence.rs
|
||||
// version: 3
|
||||
// version: 4
|
||||
|
||||
/// Canonical entity disposition produced by one Worker Store persistence attempt.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
@@ -92,11 +92,23 @@ struct RawTransactionIngestPersistenceKey {
|
||||
signature: ksp_store_lib::RawTransactionSignature,
|
||||
}
|
||||
|
||||
#[derive(Clone, Eq, PartialEq)]
|
||||
struct RawTransactionIngestCanonicalState {
|
||||
block_time: std::option::Option<ksp_store_lib::RawTimestamp>,
|
||||
content_hash: ksp_store_lib::RawContentHash,
|
||||
format_id: ksp_store_lib::RawFormatId,
|
||||
format_version: u32,
|
||||
slot: u64,
|
||||
}
|
||||
|
||||
/// Private bounded run-local cache serializing repeated canonical identities before Store writes.
|
||||
pub(crate) struct RawTransactionIngestPersistenceConvergence {
|
||||
max_entries: usize,
|
||||
entries: std::sync::Mutex<
|
||||
std::collections::BTreeMap<RawTransactionIngestPersistenceKey, std::sync::Arc<tokio::sync::Mutex<std::option::Option<ksp_store_lib::RawContentHash>>>>,
|
||||
std::collections::BTreeMap<
|
||||
RawTransactionIngestPersistenceKey,
|
||||
std::sync::Arc<tokio::sync::Mutex<std::option::Option<RawTransactionIngestCanonicalState>>>,
|
||||
>,
|
||||
>,
|
||||
}
|
||||
|
||||
@@ -110,7 +122,7 @@ impl crate::RawTransactionIngestPersistenceConvergence {
|
||||
fn entry(
|
||||
&self,
|
||||
key: RawTransactionIngestPersistenceKey,
|
||||
) -> std::option::Option<std::sync::Arc<tokio::sync::Mutex<std::option::Option<ksp_store_lib::RawContentHash>>>> {
|
||||
) -> std::option::Option<std::sync::Arc<tokio::sync::Mutex<std::option::Option<RawTransactionIngestCanonicalState>>>> {
|
||||
let mut entries = match self.entries.lock() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(poisoned) => poisoned.into_inner(),
|
||||
@@ -177,16 +189,16 @@ where
|
||||
if acquisition.observation().transaction() != &reference {
|
||||
return std::result::Result::Err(crate::runtime_error("persistence.acquisition_reference_mismatch"));
|
||||
}
|
||||
let content_hash = acquisition.transaction().payload().content_hash();
|
||||
let canonical_state = canonical_state(acquisition.transaction());
|
||||
let key = RawTransactionIngestPersistenceKey { network: reference.network().clone(), signature: reference.signature() };
|
||||
let entry = convergence.entry(key);
|
||||
let entry = match entry {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return crate::persist_raw_transaction_ingest_acquisition(port, acquisition).await,
|
||||
};
|
||||
let mut known_hash = entry.lock().await;
|
||||
if let std::option::Option::Some(known_hash_value) = *known_hash {
|
||||
if known_hash_value != content_hash {
|
||||
let mut known_state = entry.lock().await;
|
||||
if let std::option::Option::Some(known_state_value) = known_state.as_ref() {
|
||||
if known_state_value != &canonical_state {
|
||||
return std::result::Result::Err(crate::content_conflict_error());
|
||||
}
|
||||
let (_transaction, observation) = acquisition.into_parts();
|
||||
@@ -202,11 +214,21 @@ where
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if outcome.entity() != crate::RawTransactionIngestEntityPersistence::SkippedPurged {
|
||||
*known_hash = std::option::Option::Some(content_hash);
|
||||
*known_state = std::option::Option::Some(canonical_state);
|
||||
}
|
||||
return std::result::Result::Ok(outcome);
|
||||
}
|
||||
|
||||
fn canonical_state(transaction: &ksp_store_lib::RawTransaction) -> RawTransactionIngestCanonicalState {
|
||||
return RawTransactionIngestCanonicalState {
|
||||
block_time: transaction.block_time(),
|
||||
content_hash: transaction.payload().content_hash(),
|
||||
format_id: transaction.payload().format_id().clone(),
|
||||
format_version: transaction.payload().format_version(),
|
||||
slot: transaction.slot(),
|
||||
};
|
||||
}
|
||||
|
||||
fn map_store_error(error: ksp_core_lib::Error) -> ksp_core_lib::Result<crate::RawTransactionIngestPersistenceOutcome> {
|
||||
if error.code() == ksp_store_lib::ERROR_CODE_RAW_CONFLICT {
|
||||
return std::result::Result::Err(crate::content_conflict_error());
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-worker-raw-transaction-ingest-lib/src/runtime_resources.rs
|
||||
// version: 23
|
||||
// version: 24
|
||||
|
||||
use sha2::Digest; // rust-rules: trait-import
|
||||
|
||||
@@ -85,12 +85,18 @@ impl RawTransactionIngestLiveSource {
|
||||
};
|
||||
}
|
||||
|
||||
fn uses_hydration(&self) -> bool {
|
||||
return matches!(self, Self::HeliusTransaction(_) | Self::StandardLogs(_) | Self::Yellowstone(_));
|
||||
}
|
||||
|
||||
async fn run(
|
||||
self,
|
||||
settings: crate::RawTransactionIngestSettings,
|
||||
stop_receiver: tokio::sync::watch::Receiver<bool>,
|
||||
admission_sender: tokio::sync::mpsc::Sender<crate::RawTransactionIngress>,
|
||||
global_hydration_registry: std::sync::Arc<RawTransactionIngestGlobalHydrationRegistry>,
|
||||
hydration_pending_limit: usize,
|
||||
hydration_in_flight_limit: usize,
|
||||
inventory_publisher: RawTransactionIngestSourceInventoryPublisher,
|
||||
) -> ksp_core_lib::Result<()> {
|
||||
let (source_frontier_sender, mut source_frontier_receiver) =
|
||||
@@ -98,12 +104,46 @@ impl RawTransactionIngestLiveSource {
|
||||
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, global_hydration_registry).await
|
||||
source
|
||||
.run(
|
||||
settings,
|
||||
stop_receiver,
|
||||
admission_sender,
|
||||
source_frontier_sender,
|
||||
global_hydration_registry,
|
||||
hydration_pending_limit,
|
||||
hydration_in_flight_limit,
|
||||
)
|
||||
.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, global_hydration_registry).await,
|
||||
Self::Yellowstone(source) => source.run(settings, stop_receiver, admission_sender, source_frontier_sender, global_hydration_registry).await,
|
||||
Self::StandardLogs(source) => {
|
||||
source
|
||||
.run(
|
||||
settings,
|
||||
stop_receiver,
|
||||
admission_sender,
|
||||
source_frontier_sender,
|
||||
global_hydration_registry,
|
||||
hydration_pending_limit,
|
||||
hydration_in_flight_limit,
|
||||
)
|
||||
.await
|
||||
},
|
||||
Self::Yellowstone(source) => {
|
||||
source
|
||||
.run(
|
||||
settings,
|
||||
stop_receiver,
|
||||
admission_sender,
|
||||
source_frontier_sender,
|
||||
global_hydration_registry,
|
||||
hydration_pending_limit,
|
||||
hydration_in_flight_limit,
|
||||
)
|
||||
.await
|
||||
},
|
||||
};
|
||||
});
|
||||
loop {
|
||||
@@ -628,6 +668,8 @@ impl crate::RawTransactionIngestYellowstoneSource {
|
||||
admission_sender: tokio::sync::mpsc::Sender<crate::RawTransactionIngress>,
|
||||
processing_frontier_sender: tokio::sync::watch::Sender<crate::RawTransactionIngestProcessingFrontierProjection>,
|
||||
global_hydration_registry: std::sync::Arc<RawTransactionIngestGlobalHydrationRegistry>,
|
||||
hydration_pending_limit: usize,
|
||||
hydration_in_flight_limit: usize,
|
||||
) -> ksp_core_lib::Result<()> {
|
||||
let opened = tokio::select! {
|
||||
biased;
|
||||
@@ -641,7 +683,8 @@ impl crate::RawTransactionIngestYellowstoneSource {
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(source_transport_error(error.code())),
|
||||
};
|
||||
let hydration = self.hydration_context();
|
||||
let mut coordinator = RawTransactionIngestHydrationCoordinator::with_global_registry(&settings, global_hydration_registry);
|
||||
let mut coordinator =
|
||||
RawTransactionIngestHydrationCoordinator::with_global_registry(global_hydration_registry, hydration_pending_limit, hydration_in_flight_limit);
|
||||
let mut processing_frontier = RawTransactionIngestProcessingFrontierReporter::new(processing_frontier_sender);
|
||||
let snapshot_source = session.snapshot_source();
|
||||
if let std::result::Result::Err(error) = processing_frontier.observe_session_snapshot(snapshot_source.current()) {
|
||||
@@ -865,6 +908,8 @@ impl crate::RawTransactionIngestHeliusTransactionSource {
|
||||
admission_sender: tokio::sync::mpsc::Sender<crate::RawTransactionIngress>,
|
||||
processing_frontier_sender: tokio::sync::watch::Sender<crate::RawTransactionIngestProcessingFrontierProjection>,
|
||||
global_hydration_registry: std::sync::Arc<RawTransactionIngestGlobalHydrationRegistry>,
|
||||
hydration_pending_limit: usize,
|
||||
hydration_in_flight_limit: usize,
|
||||
) -> ksp_core_lib::Result<()> {
|
||||
let connected = tokio::select! {
|
||||
biased;
|
||||
@@ -894,7 +939,8 @@ impl crate::RawTransactionIngestHeliusTransactionSource {
|
||||
},
|
||||
};
|
||||
let hydration = self.hydration_context();
|
||||
let mut coordinator = RawTransactionIngestHydrationCoordinator::with_global_registry(&settings, global_hydration_registry);
|
||||
let mut coordinator =
|
||||
RawTransactionIngestHydrationCoordinator::with_global_registry(global_hydration_registry, hydration_pending_limit, hydration_in_flight_limit);
|
||||
let mut processing_frontier = RawTransactionIngestProcessingFrontierReporter::new(processing_frontier_sender);
|
||||
processing_frontier.set_source_state(crate::RawTransactionIngestSourceState::Active);
|
||||
let mut fault = std::option::Option::None;
|
||||
@@ -1584,6 +1630,8 @@ impl crate::RawTransactionIngestStandardLogsSource {
|
||||
admission_sender: tokio::sync::mpsc::Sender<crate::RawTransactionIngress>,
|
||||
processing_frontier_sender: tokio::sync::watch::Sender<crate::RawTransactionIngestProcessingFrontierProjection>,
|
||||
global_hydration_registry: std::sync::Arc<RawTransactionIngestGlobalHydrationRegistry>,
|
||||
hydration_pending_limit: usize,
|
||||
hydration_in_flight_limit: usize,
|
||||
) -> ksp_core_lib::Result<()> {
|
||||
let connected = tokio::select! {
|
||||
biased;
|
||||
@@ -1613,7 +1661,8 @@ impl crate::RawTransactionIngestStandardLogsSource {
|
||||
},
|
||||
};
|
||||
let hydration = self.hydration_context();
|
||||
let mut coordinator = RawTransactionIngestHydrationCoordinator::with_global_registry(&settings, global_hydration_registry);
|
||||
let mut coordinator =
|
||||
RawTransactionIngestHydrationCoordinator::with_global_registry(global_hydration_registry, hydration_pending_limit, hydration_in_flight_limit);
|
||||
let mut processing_frontier = RawTransactionIngestProcessingFrontierReporter::new(processing_frontier_sender);
|
||||
processing_frontier.set_source_state(crate::RawTransactionIngestSourceState::Active);
|
||||
let mut fault = std::option::Option::None;
|
||||
@@ -1939,11 +1988,29 @@ impl crate::RawTransactionIngestRuntimeResources {
|
||||
return std::result::Result::Err(crate::runtime_error("runtime_resources.source_collection_invalid"));
|
||||
}
|
||||
let source_keys = self.sources.iter().map(RawTransactionIngestLiveSource::source_key).collect::<std::vec::Vec<_>>();
|
||||
let hydration_source_count = self.sources.iter().filter(|source| return source.uses_hydration()).count();
|
||||
if let std::result::Result::Err(error) =
|
||||
validate_hydration_fairness_capacity(settings.admission_queue_capacity(), settings.persistence_concurrency(), hydration_source_count)
|
||||
{
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let hydration_pending_budget = settings.admission_queue_capacity();
|
||||
let hydration_in_flight_budget = settings.persistence_concurrency();
|
||||
let inventory = std::sync::Arc::new(std::sync::Mutex::new(RawTransactionIngestSourceInventory::new(source_keys)));
|
||||
let global_hydration_registry = std::sync::Arc::new(RawTransactionIngestGlobalHydrationRegistry::new(&settings));
|
||||
let global_hydration_registry =
|
||||
std::sync::Arc::new(RawTransactionIngestGlobalHydrationRegistry::new(hydration_pending_budget, hydration_in_flight_budget));
|
||||
let (source_stop_sender, source_stop_receiver) = tokio::sync::watch::channel(false);
|
||||
let mut children = tokio::task::JoinSet::new();
|
||||
let mut hydration_entry_index = 0_usize;
|
||||
for (entry_index, source) in self.sources.into_iter().enumerate() {
|
||||
let (hydration_pending_limit, hydration_in_flight_limit) = if source.uses_hydration() {
|
||||
let pending_limit = hydration_pending_limit(hydration_pending_budget, hydration_source_count, hydration_entry_index);
|
||||
let in_flight_limit = hydration_in_flight_limit(hydration_in_flight_budget, hydration_source_count, hydration_entry_index);
|
||||
hydration_entry_index += 1;
|
||||
(pending_limit, in_flight_limit)
|
||||
} else {
|
||||
(0, 0)
|
||||
};
|
||||
let publisher = RawTransactionIngestSourceInventoryPublisher {
|
||||
aggregate_sender: processing_frontier_sender.clone(),
|
||||
entry_index,
|
||||
@@ -1955,7 +2022,17 @@ impl crate::RawTransactionIngestRuntimeResources {
|
||||
let source_stop_receiver = source_stop_receiver.clone();
|
||||
let source_global_hydration_registry = std::sync::Arc::clone(&global_hydration_registry);
|
||||
let _abort_handle = children.spawn(async move {
|
||||
return source.run(source_settings, source_stop_receiver, source_admission_sender, source_global_hydration_registry, publisher).await;
|
||||
return source
|
||||
.run(
|
||||
source_settings,
|
||||
source_stop_receiver,
|
||||
source_admission_sender,
|
||||
source_global_hydration_registry,
|
||||
hydration_pending_limit,
|
||||
hydration_in_flight_limit,
|
||||
publisher,
|
||||
)
|
||||
.await;
|
||||
});
|
||||
}
|
||||
std::mem::drop(admission_sender);
|
||||
@@ -1963,6 +2040,33 @@ impl crate::RawTransactionIngestRuntimeResources {
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_hydration_fairness_capacity(
|
||||
admission_queue_capacity: usize,
|
||||
persistence_concurrency: usize,
|
||||
hydration_source_count: usize,
|
||||
) -> ksp_core_lib::Result<()> {
|
||||
if hydration_source_count > admission_queue_capacity {
|
||||
return std::result::Result::Err(crate::runtime_error("runtime_resources.hydration_pending_capacity_insufficient"));
|
||||
}
|
||||
if hydration_source_count > persistence_concurrency {
|
||||
return std::result::Result::Err(crate::runtime_error("runtime_resources.hydration_concurrency_insufficient"));
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn hydration_pending_limit(total_budget: usize, hydration_source_count: usize, hydration_entry_index: usize) -> usize {
|
||||
if hydration_source_count == 0 || hydration_entry_index >= hydration_source_count {
|
||||
return 0;
|
||||
}
|
||||
let base = total_budget / hydration_source_count;
|
||||
let remainder = total_budget % hydration_source_count;
|
||||
return base + usize::from(hydration_entry_index < remainder);
|
||||
}
|
||||
|
||||
fn hydration_in_flight_limit(total_budget: usize, hydration_source_count: usize, hydration_entry_index: usize) -> usize {
|
||||
return hydration_pending_limit(total_budget, hydration_source_count, hydration_entry_index);
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for crate::RawTransactionIngestRuntimeResources {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
return formatter.debug_struct("RawTransactionIngestRuntimeResources").field("source_count", &self.sources.len()).finish();
|
||||
@@ -3351,6 +3455,7 @@ enum RawTransactionIngestSharedHydrationResult {
|
||||
}
|
||||
|
||||
struct RawTransactionIngestGlobalHydrationRegistry {
|
||||
hydration_permits: std::sync::Arc<tokio::sync::Semaphore>,
|
||||
max_pending: usize,
|
||||
pending: std::sync::Mutex<
|
||||
std::collections::BTreeMap<
|
||||
@@ -3361,9 +3466,10 @@ struct RawTransactionIngestGlobalHydrationRegistry {
|
||||
}
|
||||
|
||||
impl RawTransactionIngestGlobalHydrationRegistry {
|
||||
fn new(settings: &crate::RawTransactionIngestSettings) -> Self {
|
||||
fn new(max_pending: usize, max_in_flight: usize) -> Self {
|
||||
return Self {
|
||||
max_pending: settings.admission_queue_capacity(),
|
||||
hydration_permits: std::sync::Arc::new(tokio::sync::Semaphore::new(max_in_flight)),
|
||||
max_pending,
|
||||
pending: std::sync::Mutex::new(std::collections::BTreeMap::new()),
|
||||
};
|
||||
}
|
||||
@@ -3416,17 +3522,21 @@ struct RawTransactionIngestHydrationCoordinator {
|
||||
impl RawTransactionIngestHydrationCoordinator {
|
||||
#[cfg(test)]
|
||||
fn new(settings: &crate::RawTransactionIngestSettings) -> Self {
|
||||
return Self::with_global_registry(settings, std::sync::Arc::new(RawTransactionIngestGlobalHydrationRegistry::new(settings)));
|
||||
let pending_limit = settings.admission_queue_capacity();
|
||||
let in_flight_limit = settings.persistence_concurrency().min(pending_limit);
|
||||
let registry = std::sync::Arc::new(RawTransactionIngestGlobalHydrationRegistry::new(pending_limit, settings.persistence_concurrency()));
|
||||
return Self::with_global_registry(registry, pending_limit, in_flight_limit);
|
||||
}
|
||||
|
||||
fn with_global_registry(
|
||||
settings: &crate::RawTransactionIngestSettings,
|
||||
global_registry: std::sync::Arc<RawTransactionIngestGlobalHydrationRegistry>,
|
||||
max_pending_signals: usize,
|
||||
max_in_flight: usize,
|
||||
) -> Self {
|
||||
return Self {
|
||||
global_registry,
|
||||
max_in_flight: settings.persistence_concurrency(),
|
||||
max_pending_signals: settings.admission_queue_capacity(),
|
||||
max_in_flight,
|
||||
max_pending_signals,
|
||||
pending_signal_count: 0,
|
||||
pending: std::collections::BTreeMap::new(),
|
||||
tasks: RawTransactionIngestHydrationTasks::new(),
|
||||
@@ -3615,6 +3725,33 @@ async fn fetch_hydration(
|
||||
return std::result::Result::Ok(RawTransactionIngestHydrationFetch { key, observed });
|
||||
}
|
||||
|
||||
struct RawTransactionIngestHydrationLeaderGuard {
|
||||
armed: bool,
|
||||
key: RawTransactionIngestHydrationKey,
|
||||
registry: std::sync::Arc<RawTransactionIngestGlobalHydrationRegistry>,
|
||||
}
|
||||
|
||||
impl RawTransactionIngestHydrationLeaderGuard {
|
||||
fn new(registry: std::sync::Arc<RawTransactionIngestGlobalHydrationRegistry>, key: RawTransactionIngestHydrationKey) -> Self {
|
||||
return Self { armed: true, key, registry };
|
||||
}
|
||||
|
||||
fn disarm(&mut self) {
|
||||
self.armed = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Drop for RawTransactionIngestHydrationLeaderGuard {
|
||||
fn drop(&mut self) {
|
||||
if self.armed {
|
||||
self.registry
|
||||
.publish_and_remove(&self.key, RawTransactionIngestSharedHydrationResult::Failed(crate::ERROR_CODE_RAW_TRANSACTION_INGEST_SOURCE_FAILED));
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async fn fetch_hydration_shared(
|
||||
global_registry: std::sync::Arc<RawTransactionIngestGlobalHydrationRegistry>,
|
||||
http_pool: ksp_onchain_transport_lib::HttpTransportPool,
|
||||
@@ -3628,14 +3765,22 @@ async fn fetch_hydration_shared(
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if leader {
|
||||
let mut leader_guard = RawTransactionIngestHydrationLeaderGuard::new(std::sync::Arc::clone(&global_registry), key.clone());
|
||||
let permit = std::sync::Arc::clone(&global_registry.hydration_permits).acquire_owned().await;
|
||||
let _permit = match permit {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(crate::runtime_error("source.global_hydration_permit_closed")),
|
||||
};
|
||||
let fetched = fetch_hydration(http_pool, hydration_role, expected_network, key.clone(), commitment).await;
|
||||
return match fetched {
|
||||
std::result::Result::Ok(value) => {
|
||||
global_registry.publish_and_remove(&key, RawTransactionIngestSharedHydrationResult::Available(value.observed.clone()));
|
||||
leader_guard.disarm();
|
||||
std::result::Result::Ok(value)
|
||||
},
|
||||
std::result::Result::Err(error) => {
|
||||
global_registry.publish_and_remove(&key, RawTransactionIngestSharedHydrationResult::Failed(error.code()));
|
||||
leader_guard.disarm();
|
||||
std::result::Result::Err(error)
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-worker-raw-transaction-ingest-lib/tests/dependency_boundary.rs
|
||||
// version: 24
|
||||
// version: 25
|
||||
|
||||
//! Dependency firewall canaries for the RAW transaction ingest Worker foundation.
|
||||
|
||||
@@ -604,3 +604,41 @@ fn v0_3_13_pre_008_cross_source_convergence_reuses_store_and_transport_facades_o
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v0_3_13_pre_009_global_bounds_and_fairness_stay_inside_worker_facades() {
|
||||
let admission = include_str!("../src/admission.rs");
|
||||
let persistence = include_str!("../src/persistence.rs");
|
||||
let resources = include_str!("../src/runtime_resources.rs");
|
||||
for required in [
|
||||
"validate_hydration_fairness_capacity",
|
||||
"hydration_pending_limit",
|
||||
"hydration_in_flight_limit",
|
||||
"tokio::sync::Semaphore",
|
||||
"acquire_owned",
|
||||
"RawTransactionIngestHydrationLeaderGuard",
|
||||
"RawTransactionIngestCanonicalState",
|
||||
"content_conflict_error()",
|
||||
] {
|
||||
assert!(
|
||||
admission.contains(required) || persistence.contains(required) || resources.contains(required),
|
||||
"required pre.009 global-bound contract missing: {required}"
|
||||
);
|
||||
}
|
||||
for forbidden in [
|
||||
"unbounded_channel",
|
||||
"ksp_store_postgres_lib::",
|
||||
"ksp_config_lib::",
|
||||
"ksp_job_backfill_lib::",
|
||||
"reqwest::",
|
||||
"tokio_tungstenite::",
|
||||
"tonic::",
|
||||
"yellowstone_grpc_proto::",
|
||||
] {
|
||||
assert!(
|
||||
!admission.contains(forbidden) && !persistence.contains(forbidden) && !resources.contains(forbidden),
|
||||
"pre.009 crossed a Worker facade boundary: {forbidden}"
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-worker-raw-transaction-ingest-lib/tests/hardening.rs
|
||||
// version: 18
|
||||
// version: 19
|
||||
|
||||
//! External public, security, redaction and release-boundary hardening canaries for `pre.010`.
|
||||
|
||||
@@ -786,3 +786,42 @@ fn v0_3_13_pre_008_cross_source_convergence_is_bounded_conflict_checked_and_priv
|
||||
assert!(!resources.contains("unbounded_channel"));
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v0_3_13_pre_009_duplicate_storm_disagreement_and_starvation_guards_are_explicit() {
|
||||
let admission = include_str!("../src/admission.rs");
|
||||
let persistence = include_str!("../src/persistence.rs");
|
||||
let resources = include_str!("../src/runtime_resources.rs");
|
||||
for required in [
|
||||
"runtime_resources.hydration_pending_capacity_insufficient",
|
||||
"runtime_resources.hydration_concurrency_insufficient",
|
||||
"base + usize::from(hydration_entry_index < remainder)",
|
||||
"hydration_in_flight_limit",
|
||||
"tokio::sync::Semaphore::new(max_in_flight)",
|
||||
"RawTransactionIngestHydrationLeaderGuard",
|
||||
"source.global_hydration_pending_saturated",
|
||||
"block_time: transaction.block_time()",
|
||||
"slot: transaction.slot()",
|
||||
"content_hash: transaction.payload().content_hash()",
|
||||
] {
|
||||
assert!(
|
||||
admission.contains(required) || persistence.contains(required) || resources.contains(required),
|
||||
"required pre.009 adversarial guard missing: {required}"
|
||||
);
|
||||
}
|
||||
for forbidden in [
|
||||
"VecDeque::new()",
|
||||
"unbounded_channel",
|
||||
"first_provider_wins",
|
||||
"provider_priority",
|
||||
"majority_vote",
|
||||
"preferred_provider",
|
||||
"overwrite_conflict",
|
||||
] {
|
||||
assert!(
|
||||
!admission.contains(forbidden) && !persistence.contains(forbidden) && !resources.contains(forbidden),
|
||||
"pre.009 introduced forbidden unbounded/provider-preference behavior: {forbidden}"
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-worker-raw-transaction-ingest-lib/tests/public_api.rs
|
||||
// version: 18
|
||||
// version: 19
|
||||
|
||||
//! External public-surface proofs for the RAW transaction ingest Worker foundation.
|
||||
|
||||
@@ -351,3 +351,20 @@ fn v0_3_13_pre_008_convergence_cache_registry_and_source_keys_remain_private() {
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v0_3_13_pre_009_fairness_budgets_semaphore_and_canonical_state_remain_private() {
|
||||
let root = include_str!("../src/lib.rs");
|
||||
for private_name in [
|
||||
"RawTransactionIngestCanonicalState",
|
||||
"RawTransactionIngestGlobalHydrationRegistry",
|
||||
"RawTransactionIngestHydrationLeaderGuard",
|
||||
"validate_hydration_fairness_capacity",
|
||||
"hydration_pending_limit",
|
||||
"hydration_in_flight_limit",
|
||||
] {
|
||||
assert!(!root.contains(&std::format!("pub use self::runtime_resources::{private_name}")));
|
||||
assert!(!root.contains(&std::format!("pub use self::persistence::{private_name}")));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-worker-raw-transaction-ingest-lib/tests/release_completeness.rs
|
||||
// version: 16
|
||||
// version: 17
|
||||
|
||||
//! Release-completeness canaries through the `pre.010` public/release/security hardening tranche.
|
||||
|
||||
@@ -129,6 +129,7 @@ fn pre_010_external_hardening_suite_is_present_and_scoped() {
|
||||
"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",
|
||||
"v0_3_13_pre_008_cross_source_convergence_is_bounded_conflict_checked_and_private",
|
||||
"v0_3_13_pre_009_duplicate_storm_disagreement_and_starvation_guards_are_explicit",
|
||||
] {
|
||||
assert!(hardening.contains(required), "required pre.010 hardening canary missing: {required}");
|
||||
}
|
||||
@@ -148,6 +149,7 @@ fn pre_010_external_hardening_suite_is_present_and_scoped() {
|
||||
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"));
|
||||
assert!(dependency_boundary.contains("v0_3_13_pre_008_cross_source_convergence_reuses_store_and_transport_facades_only"));
|
||||
assert!(dependency_boundary.contains("v0_3_13_pre_009_global_bounds_and_fairness_stay_inside_worker_facades"));
|
||||
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"));
|
||||
@@ -160,6 +162,7 @@ fn pre_010_external_hardening_suite_is_present_and_scoped() {
|
||||
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_13_pre_008_convergence_cache_registry_and_source_keys_remain_private"));
|
||||
assert!(public_api.contains("v0_3_13_pre_009_fairness_budgets_semaphore_and_canonical_state_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;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-worker-raw-transaction-ingest-lib/unit_tests/admission.rs
|
||||
// version: 5
|
||||
// version: 6
|
||||
|
||||
fn material(
|
||||
network: &str,
|
||||
@@ -311,3 +311,104 @@ async fn v0_3_13_pre_008_distinct_source_keys_produce_distinct_idempotent_observ
|
||||
assert_ne!(first.observation().observation_key(), second.observation().observation_key());
|
||||
return;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn v0_3_13_pre_009_duplicate_storm_cannot_starve_second_ready_source_on_bounded_admission() {
|
||||
let (network, initial_material) = match material("mainnet", 60, "AQID") {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
let (_, peer_material) = match material("mainnet", 99, "BwgJ") {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
let initial_provenance = match provenance() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
let storm_provenance = match provenance() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
let peer_provenance = match provenance() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
let (mut admission, sender) = crate::RawTransactionAdmission::new(1);
|
||||
assert!(
|
||||
sender
|
||||
.send(crate::RawTransactionIngress {
|
||||
material: initial_material,
|
||||
network: network.clone(),
|
||||
provenance: initial_provenance,
|
||||
source_key: [60_u8; 32],
|
||||
})
|
||||
.await
|
||||
.is_ok()
|
||||
);
|
||||
let storm_sender = sender.clone();
|
||||
let storm_network = network.clone();
|
||||
let storm_task = tokio::spawn(async move {
|
||||
for _ in 0..8 {
|
||||
let material = ksp_raw_transaction_lib::RawTransactionMaterial::binary_base64(
|
||||
storm_network.clone(),
|
||||
ksp_store_lib::RawTransactionSignature::new([61_u8; 64]),
|
||||
42,
|
||||
std::option::Option::Some(1_700_000_000),
|
||||
"BAUG",
|
||||
ksp_raw_transaction_lib::RawTransactionWireField::Omitted,
|
||||
ksp_raw_transaction_lib::RawTransactionWireField::Omitted,
|
||||
ksp_raw_transaction_lib::RawTransactionWireField::Omitted,
|
||||
);
|
||||
if storm_sender
|
||||
.send(crate::RawTransactionIngress {
|
||||
material,
|
||||
network: storm_network.clone(),
|
||||
provenance: storm_provenance.clone(),
|
||||
source_key: [61_u8; 32],
|
||||
})
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
tokio::task::yield_now().await;
|
||||
let peer_sender = sender.clone();
|
||||
let peer_network = network.clone();
|
||||
let peer_task = tokio::spawn(async move {
|
||||
return peer_sender
|
||||
.send(crate::RawTransactionIngress { material: peer_material, network: peer_network, provenance: peer_provenance, source_key: [99_u8; 32] })
|
||||
.await
|
||||
.is_ok();
|
||||
});
|
||||
tokio::task::yield_now().await;
|
||||
assert_eq!(admission.queue_depth(), 1);
|
||||
let first = match admission.receive(&network).await {
|
||||
std::result::Result::Ok(std::option::Option::Some(value)) => value,
|
||||
_ => return,
|
||||
};
|
||||
assert_eq!(first.transaction().reference().signature().as_bytes(), &[60_u8; 64]);
|
||||
let second = match admission.receive(&network).await {
|
||||
std::result::Result::Ok(std::option::Option::Some(value)) => value,
|
||||
_ => return,
|
||||
};
|
||||
assert_eq!(second.transaction().reference().signature().as_bytes(), &[61_u8; 64]);
|
||||
let third = match admission.receive(&network).await {
|
||||
std::result::Result::Ok(std::option::Option::Some(value)) => value,
|
||||
_ => return,
|
||||
};
|
||||
assert_eq!(third.transaction().reference().signature().as_bytes(), &[99_u8; 64]);
|
||||
for _ in 0..7 {
|
||||
let next = admission.receive(&network).await;
|
||||
assert!(matches!(next, std::result::Result::Ok(std::option::Option::Some(_))));
|
||||
assert!(admission.queue_depth() <= 1);
|
||||
}
|
||||
let peer_joined = peer_task.await;
|
||||
assert!(matches!(peer_joined, std::result::Result::Ok(true)));
|
||||
let storm_joined = storm_task.await;
|
||||
assert!(matches!(storm_joined, std::result::Result::Ok(true)));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-worker-raw-transaction-ingest-lib/unit_tests/persistence.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum PortResponse {
|
||||
@@ -92,6 +92,17 @@ fn acquisition_with_observation_key(
|
||||
network_name: &str,
|
||||
signature_byte: u8,
|
||||
observation_byte: u8,
|
||||
) -> std::option::Option<ksp_raw_transaction_lib::RawTransactionAcquisition> {
|
||||
return acquisition_with_shape(network_name, signature_byte, observation_byte, 42, std::option::Option::Some(1_700_000_000), "AQID");
|
||||
}
|
||||
|
||||
fn acquisition_with_shape(
|
||||
network_name: &str,
|
||||
signature_byte: u8,
|
||||
observation_byte: u8,
|
||||
slot: u64,
|
||||
block_time: std::option::Option<i64>,
|
||||
transaction_data: &str,
|
||||
) -> std::option::Option<ksp_raw_transaction_lib::RawTransactionAcquisition> {
|
||||
let network = match ksp_store_lib::RawNetworkId::new(network_name) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
@@ -101,9 +112,9 @@ fn acquisition_with_observation_key(
|
||||
let material = ksp_raw_transaction_lib::RawTransactionMaterial::binary_base64(
|
||||
network,
|
||||
signature,
|
||||
42,
|
||||
std::option::Option::Some(1_700_000_000),
|
||||
"AQID",
|
||||
slot,
|
||||
block_time,
|
||||
transaction_data,
|
||||
ksp_raw_transaction_lib::RawTransactionWireField::Omitted,
|
||||
ksp_raw_transaction_lib::RawTransactionWireField::Omitted,
|
||||
ksp_raw_transaction_lib::RawTransactionWireField::Omitted,
|
||||
@@ -357,3 +368,41 @@ async fn v0_3_13_pre_008_replayed_same_observation_is_idempotent_without_raw_res
|
||||
assert_eq!(second_outcome.observation(), crate::RawTransactionIngestObservationPersistence::AlreadyPresent);
|
||||
return;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn v0_3_13_pre_009_source_skew_and_provider_disagreement_fail_before_additional_observation() {
|
||||
let cases = [
|
||||
(43_u64, std::option::Option::Some(1_700_000_000_i64), "AQID"),
|
||||
(42_u64, std::option::Option::Some(1_700_000_001_i64), "AQID"),
|
||||
(42_u64, std::option::Option::Some(1_700_000_000_i64), "BAUG"),
|
||||
];
|
||||
for (index, (slot, block_time, transaction_data)) in cases.into_iter().enumerate() {
|
||||
let network = match network("mainnet") {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
let port = FakePersistencePort::new(
|
||||
network,
|
||||
PortResponse::Outcome(ksp_store_lib::RawEntityWriteOutcome::Inserted, ksp_store_lib::RawObservationWriteOutcome::Inserted),
|
||||
);
|
||||
let convergence = crate::RawTransactionIngestPersistenceConvergence::new(8);
|
||||
let first = match acquisition_with_shape("mainnet", 51, 10, 42, std::option::Option::Some(1_700_000_000), "AQID") {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
let divergent = match acquisition_with_shape("mainnet", 51, index as u8 + 20, slot, block_time, transaction_data) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
assert!(crate::persist_raw_transaction_ingest_converged_acquisition(&port, first, &convergence).await.is_ok());
|
||||
let conflict = crate::persist_raw_transaction_ingest_converged_acquisition(&port, divergent, &convergence).await;
|
||||
let error = match conflict {
|
||||
std::result::Result::Ok(_) => return,
|
||||
std::result::Result::Err(value) => value,
|
||||
};
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_RAW_TRANSACTION_INGEST_CONTENT_CONFLICT);
|
||||
assert_eq!(port.calls.load(std::sync::atomic::Ordering::Acquire), 1);
|
||||
assert_eq!(port.observation_calls.load(std::sync::atomic::Ordering::Acquire), 0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-worker-raw-transaction-ingest-lib/unit_tests/runtime_resources.rs
|
||||
// version: 20
|
||||
// version: 21
|
||||
|
||||
fn grpc_endpoint(cluster: &str) -> std::option::Option<ksp_onchain_transport_lib::YellowstoneGrpcEndpointSettings> {
|
||||
return grpc_endpoint_with_identity(cluster, "yellowstone-fixture", "fixture-provider");
|
||||
@@ -3008,7 +3008,7 @@ fn v0_3_13_pre_008_global_hydration_registry_coalesces_cross_source_key_to_one_l
|
||||
network,
|
||||
signature: ksp_store_lib::RawTransactionSignature::new([61_u8; 64]),
|
||||
};
|
||||
let registry = super::RawTransactionIngestGlobalHydrationRegistry::new(&settings);
|
||||
let registry = super::RawTransactionIngestGlobalHydrationRegistry::new(settings.admission_queue_capacity(), settings.persistence_concurrency());
|
||||
let (first_leader, _first_receiver) = match registry.subscribe_or_lead(&key) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
@@ -3034,3 +3034,107 @@ fn v0_3_13_pre_008_global_hydration_registry_coalesces_cross_source_key_to_one_l
|
||||
assert!(third_leader);
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v0_3_13_pre_009_hydration_quotas_partition_exact_bounds_and_reject_underprovision() {
|
||||
assert!(super::validate_hydration_fairness_capacity(8, 5, 3).is_ok());
|
||||
let pending_error = match super::validate_hydration_fairness_capacity(2, 5, 3) {
|
||||
std::result::Result::Ok(()) => return,
|
||||
std::result::Result::Err(value) => value,
|
||||
};
|
||||
assert_eq!(pending_error.context()[0].value(), "runtime_resources.hydration_pending_capacity_insufficient");
|
||||
let concurrency_error = match super::validate_hydration_fairness_capacity(8, 2, 3) {
|
||||
std::result::Result::Ok(()) => return,
|
||||
std::result::Result::Err(value) => value,
|
||||
};
|
||||
assert_eq!(concurrency_error.context()[0].value(), "runtime_resources.hydration_concurrency_insufficient");
|
||||
let pending_limits = [super::hydration_pending_limit(8, 3, 0), super::hydration_pending_limit(8, 3, 1), super::hydration_pending_limit(8, 3, 2)];
|
||||
assert_eq!(pending_limits, [3, 3, 2]);
|
||||
assert_eq!(pending_limits.iter().copied().sum::<usize>(), 8);
|
||||
assert!(pending_limits.iter().copied().all(|limit| return limit >= 1));
|
||||
let in_flight_limits = [super::hydration_in_flight_limit(5, 3, 0), super::hydration_in_flight_limit(5, 3, 1), super::hydration_in_flight_limit(5, 3, 2)];
|
||||
assert_eq!(in_flight_limits, [2, 2, 1]);
|
||||
assert_eq!(in_flight_limits.iter().copied().sum::<usize>(), 5);
|
||||
assert!(in_flight_limits.iter().copied().all(|limit| return limit >= 1));
|
||||
assert!(super::validate_hydration_fairness_capacity(1, 1, 0).is_ok());
|
||||
assert_eq!(super::hydration_pending_limit(1, 0, 0), 0);
|
||||
assert_eq!(super::hydration_in_flight_limit(1, 0, 0), 0);
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v0_3_13_pre_009_duplicate_hydration_storm_keeps_one_global_key_and_distinct_keys_bounded() {
|
||||
let network = match ksp_store_lib::RawNetworkId::new("devnet") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let registry = super::RawTransactionIngestGlobalHydrationRegistry::new(2, 1);
|
||||
let first_key = super::RawTransactionIngestHydrationKey {
|
||||
commitment: ksp_onchain_transport_lib::SolanaCommitment::Confirmed.as_str(),
|
||||
network: network.clone(),
|
||||
signature: ksp_store_lib::RawTransactionSignature::new([71_u8; 64]),
|
||||
};
|
||||
for index in 0..32 {
|
||||
let subscribed = registry.subscribe_or_lead(&first_key);
|
||||
let (leader, _receiver) = match subscribed {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
assert_eq!(leader, index == 0);
|
||||
}
|
||||
let second_key = super::RawTransactionIngestHydrationKey {
|
||||
commitment: ksp_onchain_transport_lib::SolanaCommitment::Confirmed.as_str(),
|
||||
network: network.clone(),
|
||||
signature: ksp_store_lib::RawTransactionSignature::new([72_u8; 64]),
|
||||
};
|
||||
let third_key = super::RawTransactionIngestHydrationKey {
|
||||
commitment: ksp_onchain_transport_lib::SolanaCommitment::Confirmed.as_str(),
|
||||
network,
|
||||
signature: ksp_store_lib::RawTransactionSignature::new([73_u8; 64]),
|
||||
};
|
||||
assert!(matches!(registry.subscribe_or_lead(&second_key), std::result::Result::Ok((true, _))));
|
||||
let saturated = registry.subscribe_or_lead(&third_key);
|
||||
let error = match saturated {
|
||||
std::result::Result::Ok(_) => return,
|
||||
std::result::Result::Err(value) => value,
|
||||
};
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_RAW_TRANSACTION_INGEST_RUNTIME_INVALID);
|
||||
assert_eq!(error.context()[0].value(), "source.global_hydration_pending_saturated");
|
||||
return;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn v0_3_13_pre_009_global_hydration_permit_and_leader_cleanup_are_bounded_and_fail_closed() {
|
||||
let network = match ksp_store_lib::RawNetworkId::new("devnet") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let registry = std::sync::Arc::new(super::RawTransactionIngestGlobalHydrationRegistry::new(4, 1));
|
||||
let first_permit = std::sync::Arc::clone(®istry.hydration_permits).try_acquire_owned();
|
||||
let first_permit = match first_permit {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
assert!(std::sync::Arc::clone(®istry.hydration_permits).try_acquire_owned().is_err());
|
||||
std::mem::drop(first_permit);
|
||||
assert!(std::sync::Arc::clone(®istry.hydration_permits).try_acquire_owned().is_ok());
|
||||
let key = super::RawTransactionIngestHydrationKey {
|
||||
commitment: ksp_onchain_transport_lib::SolanaCommitment::Confirmed.as_str(),
|
||||
network,
|
||||
signature: ksp_store_lib::RawTransactionSignature::new([74_u8; 64]),
|
||||
};
|
||||
let subscribed = registry.subscribe_or_lead(&key);
|
||||
let (leader, receiver) = match subscribed {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
assert!(leader);
|
||||
{
|
||||
let _guard = super::RawTransactionIngestHydrationLeaderGuard::new(std::sync::Arc::clone(®istry), key.clone());
|
||||
}
|
||||
let published = receiver.borrow().clone();
|
||||
assert!(matches!(published, std::option::Option::Some(super::RawTransactionIngestSharedHydrationResult::Failed(_))));
|
||||
let resubscribed = registry.subscribe_or_lead(&key);
|
||||
assert!(matches!(resubscribed, std::result::Result::Ok((true, _))));
|
||||
return;
|
||||
}
|
||||
|
||||
222
deltas/0.3.13/pre.009.md
Normal file
222
deltas/0.3.13/pre.009.md
Normal file
@@ -0,0 +1,222 @@
|
||||
<!-- file: deltas/0.3.13/pre.009.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta 0.3.13-pre.009 — disagreements, duplicate storms, bornes globales et fairness
|
||||
|
||||
## Base requise
|
||||
|
||||
```text
|
||||
livraison précédente : 0.3.13-pre.008-fix.002
|
||||
Cargo base : 0.3.13-pre.8.fix.2
|
||||
delta base : deltas/0.3.13/pre.008-fix.002.md
|
||||
archive delta base : ksp-general-0.3.13-pre.008-fix.002.zip
|
||||
```
|
||||
|
||||
Les gates opérateur communiqués le 10 septembre 2026 autorisent cette tranche. Le gate Worker est vert pour `cargo fmt`, `cargo fmt --check`, les audits Rust/Markdown, `cargo check --workspace`, Clippy strict et toutes les suites `ksp-worker-raw-transaction-ingest-lib` : `94` unit, `4` cross-layer, `15` dependency-boundary, `22` hardening, `16` public-api, `4` release-completeness et `0` doc-test.
|
||||
|
||||
Le gate étendu complète cette qualification avec `cargo test -p ksp-onchain-transport-lib` : `389` unit, `52` public-api, `44` release-completeness et `4` doc-tests passent. Les cinq smokes réseau opt-in affichés restent ignorés comme prévu. Les commandes `cargo tree -p ksp-worker-raw-transaction-ingest-lib --edges normal`, `cargo tree -p ksp-worker-raw-transaction-ingest-lib -e features` et `cargo tree --duplicates` sont exécutées sans erreur ; `cargo tree --duplicates` liste les doublons existants du workspace sans introduire de promesse de graphe sans doublon.
|
||||
|
||||
## Objectif
|
||||
|
||||
Fermer les risques adversariaux qui restent après la convergence cross-source de `pre.008` :
|
||||
|
||||
```text
|
||||
disagreement entre sources
|
||||
storms de références dupliquées
|
||||
borne globale des pending hydration signals
|
||||
borne globale des tâches/hydrations HTTP
|
||||
cleanup d'un leader global annulé
|
||||
fairness minimale entre sources reference-bearing
|
||||
absence de starvation structurelle
|
||||
```
|
||||
|
||||
La tranche ne modifie pas encore la projection publique de health/snapshots multi-source ; ce point reste réservé à `pre.010`.
|
||||
|
||||
## Version
|
||||
|
||||
```text
|
||||
livraison : 0.3.13-pre.009
|
||||
workspace.package.version : 0.3.13-pre.9
|
||||
archive : ksp-general-0.3.13-pre.009.zip
|
||||
```
|
||||
|
||||
`Cargo.toml` passe de la version de fichier `555` à `556`.
|
||||
|
||||
## Budgets globaux et fairness minimale
|
||||
|
||||
Yellowstone, Standard Logs et Helius Transaction sont les sources reference-bearing qui nécessitent une hydration `getTransaction`. Avant tout spawn, `run_live_sources` vérifie :
|
||||
|
||||
```text
|
||||
hydration_source_count <= admission_queue_capacity
|
||||
hydration_source_count <= persistence_concurrency
|
||||
```
|
||||
|
||||
Si une de ces relations est fausse, le démarrage échoue fail-closed avec un `runtime_invalid` stable. Aucune capacité configurée n'est agrandie silencieusement.
|
||||
|
||||
Lorsque la composition est valide, les deux budgets sont partitionnés statiquement par quotient/reste suivant l'ordre déjà validé des sources :
|
||||
|
||||
```text
|
||||
somme hydration pending quotas = admission_queue_capacity
|
||||
somme hydration in-flight quotas = persistence_concurrency
|
||||
quota de chaque source reference-bearing >= 1
|
||||
```
|
||||
|
||||
Une source ne peut donc pas consommer la part réservée aux autres sources reference-bearing. Ce mécanisme n'introduit ni scheduler pondéré, ni priorité provider, ni primary/standby.
|
||||
|
||||
Standard Block et HTTP Block Polling restent RAW-direct et continuent à utiliser l'admission centrale bornée sans passer par ces quotas d'hydration.
|
||||
|
||||
## Borne globale d'hydration
|
||||
|
||||
Le `RawTransactionIngestGlobalHydrationRegistry` reste privé et borné à `admission_queue_capacity` clés distinctes. Un duplicate storm sur une clé déjà présente rejoint le résultat global existant et ne crée ni nouvelle entrée ni second leader HTTP.
|
||||
|
||||
Un `tokio::sync::Semaphore` global, dimensionné à `persistence_concurrency`, protège en plus le nombre de leaders HTTP effectivement ouverts. Les coordinators locaux conservent simultanément leurs quotas `max_pending_signals` et `max_in_flight`.
|
||||
|
||||
Une clé distincte au-delà de la borne globale produit `source.global_hydration_pending_saturated` au lieu d'une croissance mémoire non bornée.
|
||||
|
||||
## Cleanup d'un leader global
|
||||
|
||||
Chaque leader de la registry possède un `RawTransactionIngestHydrationLeaderGuard` privé. Si la tâche est annulée ou droppée avant la publication normale du résultat, le guard publie une faute source sûre aux followers et retire la clé du registre.
|
||||
|
||||
Après cleanup, la même clé peut redevenir leader lors d'une acquisition ultérieure. Aucun leader orphelin ne doit survivre à un abort/shutdown.
|
||||
|
||||
## Disagreement canonique
|
||||
|
||||
Le cache run-local de convergence persistence ne compare plus uniquement `RawContentHash`. Pour une même identité durable `(network, signature)`, il mémorise maintenant un état canonique borné :
|
||||
|
||||
```text
|
||||
slot
|
||||
block_time
|
||||
format_id
|
||||
format_version
|
||||
content_hash
|
||||
```
|
||||
|
||||
Une divergence sur l'une de ces dimensions produit immédiatement le content conflict terminal existant avant toute observation additionnelle. Aucun byte de transaction n'est recopié dans le cache ; le hash provient de la canonicalisation Common RAW et le Store conserve son contrôle durable final sur le contenu exact.
|
||||
|
||||
La politique reste conservatrice : aucune majorité de sources, aucun first-provider-wins, aucune préférence provider/tier et aucun overwrite de conflit.
|
||||
|
||||
## Fairness et duplicate storms
|
||||
|
||||
La fairness minimale est prouvée sur le canal d'admission commun borné à capacité `1`. Après qu'une source en storm a rejoint la file d'attente, une seconde source déjà prête rejoint la même file et progresse avant la répétition suivante de la storm.
|
||||
|
||||
Cette preuve ne revendique pas une QoS pondérée. Elle verrouille uniquement l'absence de starvation indéfinie d'une source déjà prête sous le contrat FIFO du canal borné commun, tandis que les quotas statiques empêchent la starvation structurelle des sources reference-bearing en amont.
|
||||
|
||||
## Preuves ajoutées
|
||||
|
||||
Tests unitaires déterministes :
|
||||
|
||||
```text
|
||||
budgets 8/5 répartis sur 3 sources -> [3,3,2] pending et [2,2,1] in-flight
|
||||
composition sous-provisionnée -> refus avant spawn
|
||||
32 duplicats d'une même clé -> un seul leader global
|
||||
clé distincte au-delà de max_pending -> saturation sûre
|
||||
sémaphore capacité 1 -> une seule permit simultanée
|
||||
leader guard droppé -> Failed publié + clé réouvrable
|
||||
queue admission capacité 1 -> seconde source prête progresse sous storm
|
||||
même signature + slot divergent -> content conflict
|
||||
même signature + block_time divergent -> content conflict
|
||||
même signature + contenu/hash divergent -> content conflict
|
||||
aucune observation additionnelle après disagreement
|
||||
```
|
||||
|
||||
Canaris externes :
|
||||
|
||||
```text
|
||||
dependency_boundary : bornes/fairness restent dans Worker + façades existantes
|
||||
hardening : quotas, sémaphore, cleanup, disagreement et absence de provider preference
|
||||
public_api : registry, leader guard, quotas et canonical state restent privés
|
||||
release_completeness : présence obligatoire des trois canaris pre.009
|
||||
```
|
||||
|
||||
## Frontière de tranche
|
||||
|
||||
`pre.009` ne ferme pas encore :
|
||||
|
||||
```text
|
||||
nouveaux compteurs/health publics multi-source
|
||||
projection détaillée par source
|
||||
politique de dégradation non terminale
|
||||
failover provider
|
||||
scheduler/QoS pondéré
|
||||
source primary/standby
|
||||
majority vote
|
||||
```
|
||||
|
||||
Les snapshots/health appartiennent à `pre.010`.
|
||||
|
||||
## 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/persistence.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/admission.rs
|
||||
crates/ksp-worker-raw-transaction-ingest-lib/unit_tests/persistence.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.009.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.009` restent donc `NON EXÉCUTÉ LOCAL`. Les audits statiques KSP, le scan normatif, le contrôle exhaustif du diff, des headers et du ZIP sont exécutés avant livraison.
|
||||
|
||||
## Résultats statiques finaux
|
||||
|
||||
```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, 845 files)
|
||||
Normative rule definitions: 489
|
||||
Unique normative IDs: 489
|
||||
Duplicates: 0
|
||||
```
|
||||
|
||||
Contrôle ciblé :
|
||||
|
||||
```text
|
||||
14 fichiers existants modifiés
|
||||
1 fichier ajouté
|
||||
0 suppression
|
||||
14/14 headers existants : +1
|
||||
nouvelle dépendance : aucune
|
||||
backend Store physique direct : absent
|
||||
client HTTP/gRPC direct : absent
|
||||
unbounded_channel : absent
|
||||
registry/quotas/canonical state publics : absents
|
||||
```
|
||||
|
||||
## Gate opérateur requis avant pre.010
|
||||
|
||||
```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
|
||||
```
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/plans/034-V0_3_13_MULTI_SOURCE_LIVE_CONVERGENCE_PLAN.md -->
|
||||
<!-- version: 8 -->
|
||||
<!-- version: 9 -->
|
||||
|
||||
# Plan v0.3.13 — WS standard / Helius / HTTP live + convergence multi-source RawTransaction
|
||||
|
||||
@@ -361,6 +361,8 @@ shutdown timeout : bornes 0.3.12 conservées
|
||||
|
||||
Le coordinator doit utiliser la capacité effective configurée plutôt que la constante maximale comme borne opérationnelle de pending.
|
||||
|
||||
Pour matérialiser simultanément cette borne et la fairness minimale sans scheduler pondéré, `pre.009` partitionne statiquement les deux capacités entre les seules sources reference-bearing. Une composition est refusée avant spawn si leur nombre dépasse `admission_queue_capacity` ou `persistence_concurrency`; sinon chaque source reçoit au moins une part et les sommes des quotas restent exactement dans les capacités configurées. Un sémaphore global conserve en plus un guard défensif sur les hydrations HTTP effectivement ouvertes.
|
||||
|
||||
### 9.4 Fairness minimale
|
||||
|
||||
Aucune source ne reçoit une queue Worker non bornée. Tous les producteurs attendent sur le même canal borné et subissent le même backpressure.
|
||||
@@ -404,7 +406,7 @@ même identité + payload/hash divergent = content conflict explicite
|
||||
|
||||
Aucune majorité de providers, préférence de tier ou overwrite n'est autorisé.
|
||||
|
||||
Le Store reste l'autorité durable finale du conflit. Le coordinator peut détecter plus tôt une divergence lorsque plusieurs matériaux complets de même identité coexistent dans le même lot, mais cette optimisation ne remplace jamais le guard Store.
|
||||
Le Store reste l'autorité durable finale du conflit. Le coordinator peut détecter plus tôt une divergence lorsque plusieurs matériaux complets de même identité coexistent dans le même lot, mais cette optimisation ne remplace jamais le guard Store. La comparaison précoce `pre.009` couvre les dimensions durables disponibles sans recopier le payload dans le cache : slot, block time, format id/version et hash canonique ; les bytes restent protégés par la canonicalisation Common RAW et le guard Store.
|
||||
|
||||
Un content conflict reste terminal pour le Worker en `0.3.13`.
|
||||
|
||||
@@ -1283,3 +1285,125 @@ 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
|
||||
```
|
||||
|
||||
## 67. Gate opérateur pre.008-fix.002 reçu avant pre.009
|
||||
|
||||
Le gate complet a été fermé en deux exécutions opérateur consécutives le 10 septembre 2026.
|
||||
|
||||
La première exécution confirme :
|
||||
|
||||
```text
|
||||
fmt : PASS
|
||||
audits Rust : clean / export completeness 0
|
||||
Markdown : clean, 340 tables / 844 files
|
||||
cargo check --workspace : PASS
|
||||
clippy workspace all-targets all-features -D warnings : PASS
|
||||
worker unit : 94/94 PASS
|
||||
cross-layer : 4/4 PASS
|
||||
dependency boundary : 15/15 PASS
|
||||
hardening : 22/22 PASS
|
||||
public API : 16/16 PASS
|
||||
release completeness : 4/4 PASS
|
||||
worker doc-tests : 0/0 PASS
|
||||
```
|
||||
|
||||
La seconde exécution ferme les commandes étendues qui manquaient jusque-là :
|
||||
|
||||
```text
|
||||
ksp-onchain-transport-lib unit : 389/389 PASS
|
||||
transport public API : 52/52 PASS
|
||||
transport release completeness : 44/44 PASS
|
||||
transport doc-tests : 4/4 PASS
|
||||
smokes live opt-in : 5 ignored comme prévu
|
||||
cargo tree worker --edges normal : exécuté sans erreur
|
||||
cargo tree worker -e features : exécuté sans erreur
|
||||
cargo tree --duplicates : exécuté sans erreur ; doublons workspace existants listés, aucune promesse de graphe sans doublon
|
||||
```
|
||||
|
||||
Ce gate autorise `pre.009` sans réouvrir Transport ni modifier les dépendances du Worker.
|
||||
|
||||
## 68. Implémentation pre.009 — bornes globales et fairness minimale
|
||||
|
||||
Les seules sources reference-bearing sont Yellowstone, Standard Logs et Helius Transaction. `run_live_sources` compte ces sources avant tout spawn et vérifie deux compatibilités de capacité :
|
||||
|
||||
```text
|
||||
hydration_source_count <= admission_queue_capacity
|
||||
hydration_source_count <= persistence_concurrency
|
||||
```
|
||||
|
||||
Une violation est fail-closed avant spawn avec un `runtime_invalid` stable et sans exposer de valeur sensible. Lorsque la composition est valide, les quotas sont répartis de façon déterministe suivant l'ordre déjà validé des sources :
|
||||
|
||||
```text
|
||||
somme hydration pending quotas = admission_queue_capacity
|
||||
somme hydration in-flight quotas = persistence_concurrency
|
||||
chaque source reference-bearing reçoit au moins 1 pending et 1 in-flight
|
||||
```
|
||||
|
||||
Le partage utilise quotient + reste et n'introduit ni priorité provider, ni scheduler pondéré, ni queue privée non bornée. Une source hostile ne peut donc pas consommer la part réservée aux autres sources reference-bearing. Standard Block et HTTP Block Polling continuent à passer directement par l'admission centrale bornée lorsqu'ils produisent du Common RAW.
|
||||
|
||||
Le registre global d'hydration conserve `max_pending = admission_queue_capacity`. Un sémaphore Tokio global de `persistence_concurrency` constitue un second guard défensif : même si un invariant local régressait, le nombre d'hydrations HTTP leader effectivement ouvertes resterait borné.
|
||||
|
||||
## 69. Duplicate storms, cleanup et starvation
|
||||
|
||||
La coalescence globale de `pre.008` reste `(network, signature, commitment)`. Un duplicate storm sur une clé existante ne crée pas de nouvelle entrée globale et ne déclenche pas de second leader HTTP. Les clés distinctes restent bornées par le budget global ; saturation produit une faute runtime sûre plutôt qu'une croissance mémoire.
|
||||
|
||||
Chaque leader global possède désormais un guard privé de cleanup. Si sa tâche est annulée ou droppée avant publication normale, le guard retire la clé du registre et publie une faute source sûre aux followers. Aucune entrée leader orpheline ne doit survivre à un abort/shutdown.
|
||||
|
||||
La fairness de l'admission centrale est prouvée sur une queue bornée de capacité `1` : après qu'une source storm a rejoint la file d'attente, une seconde source prête rejoint la même queue et est admise avant la réitération suivante de la storm. La preuve ne revendique pas de QoS pondérée ; elle verrouille seulement l'absence de starvation indéfinie d'une source déjà prête sous le contrat FIFO du canal borné utilisé.
|
||||
|
||||
## 70. Disagreement et content conflict pre.009
|
||||
|
||||
Le cache de convergence persistence ne se limite plus au seul `RawContentHash`. Pour une même identité durable `(network, signature)`, il conserve un état canonique borné contenant :
|
||||
|
||||
```text
|
||||
slot
|
||||
block_time
|
||||
format_id
|
||||
format_version
|
||||
content_hash
|
||||
```
|
||||
|
||||
Une divergence sur l'une de ces dimensions devient immédiatement le content conflict terminal existant, avant toute observation additionnelle. Les bytes ne sont pas recopiés dans le cache ; ils sont déjà représentés par le hash issu de la canonicalisation Common RAW, et le Store conserve son contrôle durable final incluant les bytes exacts.
|
||||
|
||||
La politique reste strictement conservatrice : aucune majorité de sources, aucun first-provider-wins, aucune préférence de tier/provider et aucun overwrite ne sont introduits.
|
||||
|
||||
## 71. Preuves déterministes pre.009
|
||||
|
||||
Les preuves ajoutées couvrent au minimum :
|
||||
|
||||
```text
|
||||
partition exacte des budgets pending et in-flight
|
||||
refus des compositions sous-provisionnées avant spawn
|
||||
chaque source reference-bearing reçoit une part non nulle
|
||||
storm de 32 duplicats -> une seule clé globale leader
|
||||
clé globale distincte au-delà de la borne -> saturation sûre
|
||||
sémaphore global -> une seule permit active pour capacité 1
|
||||
drop du leader guard -> publication Failed + clé réouvrable
|
||||
storm admission -> seconde source prête progresse avant la répétition suivante
|
||||
même signature + slot divergent -> content conflict
|
||||
même signature + block_time divergent -> content conflict
|
||||
même signature + contenu/hash divergent -> content conflict
|
||||
aucune observation additionnelle après disagreement
|
||||
```
|
||||
|
||||
Les canaris externes verrouillent en plus l'absence de backend Store direct, client réseau direct, queue non bornée, politique provider-preference et exposition publique des quotas/registry/canonical state.
|
||||
|
||||
## 72. Frontière de tranche pre.009
|
||||
|
||||
`pre.009` ferme disagreements, duplicate storms, bornes globales d'hydration et fairness minimale. Elle ne modifie pas les compteurs/snapshots publics pour exposer une health multi-source détaillée : cette projection appartient à `pre.010`. Elle n'introduit pas non plus de failover, de dégradation non terminale, de priorité provider ni de scheduling pondéré.
|
||||
|
||||
## 73. Gate opérateur requis avant pre.010
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/validation/030-V0_3_13_MULTI_SOURCE_LIVE_CONVERGENCE.md -->
|
||||
<!-- version: 8 -->
|
||||
<!-- version: 9 -->
|
||||
|
||||
# Validation v0.3.13 — WS standard / Helius / HTTP live + convergence multi-source
|
||||
|
||||
@@ -1288,3 +1288,127 @@ pas de failover provider implicite
|
||||
pas de préférence silencieuse entre sources divergentes
|
||||
pas de nouveaux compteurs/health publics multi-source avant pre.010
|
||||
```
|
||||
|
||||
## 68. Gate complet pre.008-fix.002 reçu
|
||||
|
||||
Le 10 septembre 2026, le gate Worker puis son complément Transport/graphe ont été fournis et sont verts pour toutes les commandes exécutées :
|
||||
|
||||
```text
|
||||
fmt : PASS
|
||||
audits Rust : clean / export completeness 0
|
||||
Markdown : clean, 340 tables / 844 files
|
||||
cargo check --workspace : PASS
|
||||
clippy strict : PASS
|
||||
Worker : 94 unit + 4 cross-layer + 15 dependency-boundary + 22 hardening + 16 public-api + 4 release-completeness, 0 échec
|
||||
Transport : 389 unit + 52 public-api + 44 release-completeness + 4 doc-tests, 0 échec
|
||||
smokes Transport live opt-in : 5 ignored comme prévu
|
||||
cargo tree worker normal/features : exécutés sans erreur
|
||||
cargo tree --duplicates : exécuté sans erreur et liste les doublons existants du workspace
|
||||
```
|
||||
|
||||
Aucun smoke live ignoré n'est requalifié en preuve réseau réelle.
|
||||
|
||||
## 69. Surface pre.009 matérialisée
|
||||
|
||||
```text
|
||||
validate_hydration_fairness_capacity privé
|
||||
hydration_pending_limit privé
|
||||
hydration_in_flight_limit privé
|
||||
quotas statiques par source reference-bearing
|
||||
RawTransactionIngestGlobalHydrationRegistry max_pending exact
|
||||
sémaphore global hydration
|
||||
RawTransactionIngestHydrationLeaderGuard privé
|
||||
RawTransactionIngestCanonicalState privé
|
||||
comparaison slot/block_time/format/hash avant observation additionnelle
|
||||
```
|
||||
|
||||
Aucune API publique ni dépendance supplémentaire n'est requise.
|
||||
|
||||
## 70. Invariants globaux pre.009
|
||||
|
||||
Pour une composition productive valide :
|
||||
|
||||
```text
|
||||
hydration_source_count <= admission_queue_capacity
|
||||
hydration_source_count <= persistence_concurrency
|
||||
somme quotas pending = admission_queue_capacity
|
||||
somme quotas hydration tasks = persistence_concurrency
|
||||
quota de chaque source reference-bearing >= 1
|
||||
global registry distinct keys <= admission_queue_capacity
|
||||
HTTP leader effectivement ouvert <= persistence_concurrency
|
||||
source count <= 32
|
||||
```
|
||||
|
||||
Une composition sous-provisionnée échoue avant spawn. Il n'existe aucune extension silencieuse des bornes configurées, aucune queue provider privée et aucune `unbounded_channel`.
|
||||
|
||||
## 71. Duplicate storm, fairness et cleanup
|
||||
|
||||
Les tests déterministes ajoutés vérifient :
|
||||
|
||||
```text
|
||||
32 subscriptions identiques -> une seule entrée/leader global
|
||||
clés distinctes au-delà de max_pending -> runtime_invalid sûr
|
||||
sémaphore capacité 1 -> seconde acquisition simultanée refusée
|
||||
leader guard droppé -> follower reçoit Failed et la clé peut redevenir leader
|
||||
queue admission capacité 1 -> une seconde source déjà prête n'est pas affamée par la répétition d'une storm
|
||||
```
|
||||
|
||||
Le test de fairness ne revendique ni priorité métier ni scheduler pondéré. Il vérifie le comportement minimal attendu du canal borné commun et les quotas réservent une capacité aux autres sources reference-bearing.
|
||||
|
||||
## 72. Disagreement/content conflict pre.009
|
||||
|
||||
Pour une même identité `(network, signature)`, le cache run-local compare maintenant :
|
||||
|
||||
```text
|
||||
slot
|
||||
block_time
|
||||
format_id
|
||||
format_version
|
||||
content_hash
|
||||
```
|
||||
|
||||
Les cas slot divergent, block time divergent et contenu/hash divergent sont testés comme content conflicts terminaux avant `record_raw_transaction_observation`. Le Store reste l'autorité durable finale et aucune politique de majority vote, provider priority, preferred provider ou overwrite conflict n'est admise.
|
||||
|
||||
## 73. Canaris externes pre.009
|
||||
|
||||
Les suites externes sont étendues pour verrouiller :
|
||||
|
||||
```text
|
||||
dependency-boundary : bornes/fairness restent dans Worker + façades existantes
|
||||
hardening : quotas, sémaphore, cleanup, disagreement et absence de provider preference
|
||||
public-api : registry, leader guard, quotas et canonical state restent privés
|
||||
release-completeness : présence obligatoire des trois nouveaux canaris
|
||||
```
|
||||
|
||||
## 74. Validation locale d'assemblage pre.009
|
||||
|
||||
Le toolchain Cargo/Rustfmt n'est pas disponible dans l'environnement d'assemblage. Les commandes Cargo post-modification restent `NON EXÉCUTÉ LOCAL`; seules les validations statiques effectivement exécutées et les contrôles de packaging pourront être déclarés PASS dans le delta.
|
||||
|
||||
## 75. Non-claims pre.009
|
||||
|
||||
```text
|
||||
pas de nouveaux compteurs/health publics avant pre.010
|
||||
pas de QoS/scheduler pondéré
|
||||
pas de failover provider implicite
|
||||
pas de source primary/standby
|
||||
pas de majority vote
|
||||
pas de préférence provider/tier
|
||||
pas de overwrite d'un conflit canonique
|
||||
pas de preuve live réseau nouvelle
|
||||
```
|
||||
|
||||
## 76. Gate opérateur requis avant pre.010
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user