v0.3.14-pre.003

This commit is contained in:
2026-09-11 21:54:19 +02:00
parent 7bf938c19f
commit 6721a4142a
15 changed files with 872 additions and 22 deletions

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-worker-raw-transaction-ingest-lib/src/continuity.rs
// version: 3
// version: 4
/// Maximum number of simultaneously retained non-repaired run-local gaps.
const MAX_RAW_TRANSACTION_INGEST_OPEN_REPAIR_GAPS: usize = 64;
@@ -150,6 +150,85 @@ impl RawTransactionIngestGapRange {
}
}
/// Private run-local anchor for one WebSocket continuity incident.
///
/// The anchor never derives slots from wall-clock time. Its inclusive start is the latest slot actually observed by that source before Transport reported a
/// reconnect or notification overflow; the end remains absent until the first post-incident source slot is observed.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct RawTransactionIngestWebSocketIncidentAnchor {
end_slot: std::option::Option<u64>,
overflow_total: u64,
reconnect_total: u64,
saw_overflow: bool,
saw_reconnect: bool,
start_slot: u64,
}
impl crate::RawTransactionIngestWebSocketIncidentAnchor {
/// Creates one open incident anchor from monotone Transport counters and one actually observed source slot.
pub(crate) fn new(start_slot: u64, reconnect_total: u64, overflow_total: u64, saw_reconnect: bool, saw_overflow: bool) -> ksp_core_lib::Result<Self> {
if !saw_reconnect && !saw_overflow {
return std::result::Result::Err(crate::runtime_error("continuity.websocket_incident_reason_missing"));
}
return std::result::Result::Ok(Self {
end_slot: std::option::Option::None,
overflow_total,
reconnect_total,
saw_overflow,
saw_reconnect,
start_slot,
});
}
/// Extends one still-open incident with newer monotone Transport counters without changing its earliest start slot.
pub(crate) fn extend(&mut self, reconnect_total: u64, overflow_total: u64, saw_reconnect: bool, saw_overflow: bool) -> ksp_core_lib::Result<()> {
if self.end_slot.is_some() {
return std::result::Result::Err(crate::runtime_error("continuity.websocket_incident_already_closed"));
}
if reconnect_total < self.reconnect_total || overflow_total < self.overflow_total {
return std::result::Result::Err(crate::runtime_error("continuity.websocket_incident_counter_regression"));
}
self.reconnect_total = reconnect_total;
self.overflow_total = overflow_total;
self.saw_reconnect = self.saw_reconnect || saw_reconnect;
self.saw_overflow = self.saw_overflow || saw_overflow;
return std::result::Result::Ok(());
}
/// Closes the inclusive incident range at the first post-incident source slot.
pub(crate) fn close_at(&mut self, end_slot: u64) -> ksp_core_lib::Result<()> {
let range = match RawTransactionIngestGapRange::new(self.start_slot, end_slot) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
self.end_slot = std::option::Option::Some(range.end_slot());
return std::result::Result::Ok(());
}
/// Returns the first source slot that safely anchors the incident, inclusively.
#[cfg(test)]
pub(crate) const fn start_slot(self) -> u64 {
return self.start_slot;
}
/// Returns the first post-incident source slot when the incident has become range-bounded.
pub(crate) const fn end_slot(self) -> std::option::Option<u64> {
return self.end_slot;
}
/// Returns whether at least one physical WebSocket reconnect contributed to this incident.
#[cfg(test)]
pub(crate) const fn saw_reconnect(self) -> bool {
return self.saw_reconnect;
}
/// Returns whether at least one Transport notification overflow contributed to this incident.
#[cfg(test)]
pub(crate) const fn saw_overflow(self) -> bool {
return self.saw_overflow;
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
struct RawTransactionIngestGapId(u64);

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-worker-raw-transaction-ingest-lib/src/lib.rs
// version: 28
// version: 29
#![warn(missing_docs)]
#![deny(unreachable_pub)]
@@ -117,6 +117,8 @@ pub(crate) use self::continuity::RawTransactionIngestContinuityCapabilityDescrip
pub(crate) use self::continuity::RawTransactionIngestContinuityContracts;
/// Private provider-neutral coverage scope used by run-local continuity proof contracts.
pub(crate) use self::continuity::RawTransactionIngestCoverageScope;
/// Private run-local WebSocket incident anchor built only from observed source slots and safe Transport counters.
pub(crate) use self::continuity::RawTransactionIngestWebSocketIncidentAnchor;
/// Creates one terminal content-conflict error without copying conflicting material into diagnostics.
pub(crate) use self::error::content_conflict_error;
/// Creates one terminal counter-exhaustion error without exposing runtime material.

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-worker-raw-transaction-ingest-lib/src/runtime_resources.rs
// version: 29
// version: 30
use sha2::Digest; // rust-rules: trait-import
@@ -1055,12 +1055,17 @@ impl crate::RawTransactionIngestHeliusTransactionSource {
return std::result::Result::Err(source_transport_error(error.code()));
},
};
let mut session_snapshot_source = session.snapshot_source();
let hydration = self.hydration_context();
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);
if let std::result::Result::Err(error) = processing_frontier.observe_websocket_session_snapshot(session_snapshot_source.current()) {
let _closed = session.close().await;
return std::result::Result::Err(error);
}
let mut fault = std::option::Option::None;
let mut bounded_websocket_incident = false;
loop {
if *stop_receiver.borrow() {
break;
@@ -1069,7 +1074,11 @@ impl crate::RawTransactionIngestHeliusTransactionSource {
fault = std::option::Option::Some(error);
break;
}
let can_receive = coordinator.can_receive();
if bounded_websocket_incident && coordinator.pending_signal_count == 0 && coordinator.tasks.is_empty() {
fault = std::option::Option::Some(crate::runtime_error("source.continuity_gap_proven"));
break;
}
let can_receive = coordinator.can_receive() && !bounded_websocket_incident;
if !can_receive && coordinator.tasks.is_empty() {
fault = std::option::Option::Some(crate::runtime_error("source.hydration_stalled"));
break;
@@ -1079,6 +1088,19 @@ impl crate::RawTransactionIngestHeliusTransactionSource {
_ = stop_receiver.changed() => {
break;
}
source_snapshot = session_snapshot_source.wait_for_change() => {
let source_snapshot = match source_snapshot {
std::option::Option::Some(value) => value,
std::option::Option::None => {
fault = std::option::Option::Some(crate::runtime_error("source.websocket_snapshot_closed"));
break;
},
};
if let std::result::Result::Err(error) = processing_frontier.observe_websocket_session_snapshot(source_snapshot) {
fault = std::option::Option::Some(error);
break;
}
}
joined = coordinator.tasks.join_next(), if !coordinator.tasks.is_empty() => {
let joined = match joined {
std::option::Option::Some(value) => value,
@@ -1143,10 +1165,18 @@ impl crate::RawTransactionIngestHeliusTransactionSource {
break;
},
};
let incident_bounded = match processing_frontier.observe_websocket_post_incident_slot(signal.slot) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
fault = std::option::Option::Some(error);
break;
},
};
if let std::result::Result::Err(error) = coordinator.queue_signal(&hydration, signal, received_at, &mut processing_frontier) {
fault = std::option::Option::Some(error);
break;
}
bounded_websocket_incident = bounded_websocket_incident || incident_bounded;
}
}
}
@@ -1541,8 +1571,12 @@ impl crate::RawTransactionIngestStandardBlockSource {
return std::result::Result::Err(source_transport_error(error.code()));
},
};
let mut session_snapshot_source = session.snapshot_source();
let mut processing_frontier = RawTransactionIngestProcessingFrontierReporter::new(processing_frontier_sender);
processing_frontier.set_source_state(crate::RawTransactionIngestSourceState::Active);
if let std::result::Result::Err(error) = processing_frontier.observe_websocket_session_snapshot(session_snapshot_source.current()) {
let _closed = session.close().await;
return std::result::Result::Err(error);
}
let mut fault = std::option::Option::None;
'source: loop {
if *stop_receiver.borrow() {
@@ -1553,6 +1587,20 @@ impl crate::RawTransactionIngestStandardBlockSource {
_ = stop_receiver.changed() => {
break;
}
source_snapshot = session_snapshot_source.wait_for_change() => {
let source_snapshot = match source_snapshot {
std::option::Option::Some(value) => value,
std::option::Option::None => {
fault = std::option::Option::Some(crate::runtime_error("source.websocket_snapshot_closed"));
break;
},
};
if let std::result::Result::Err(error) = processing_frontier.observe_websocket_session_snapshot(source_snapshot) {
fault = std::option::Option::Some(error);
break;
}
continue;
}
value = subscription.recv() => value,
};
let notification = match notification {
@@ -1584,11 +1632,22 @@ impl crate::RawTransactionIngestStandardBlockSource {
break;
},
};
let incident_bounded = match processing_frontier.observe_websocket_post_incident_slot(slot) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
fault = std::option::Option::Some(error);
break;
},
};
if ingresses.is_empty() {
if let std::result::Result::Err(error) = processing_frontier.observe_settled(slot) {
fault = std::option::Option::Some(error);
break;
}
if incident_bounded {
fault = std::option::Option::Some(crate::runtime_error("source.continuity_gap_proven"));
break;
}
continue;
}
for ingress in ingresses {
@@ -1611,6 +1670,10 @@ impl crate::RawTransactionIngestStandardBlockSource {
fault = std::option::Option::Some(error);
break;
}
if incident_bounded {
fault = std::option::Option::Some(crate::runtime_error("source.continuity_gap_proven"));
break;
}
}
processing_frontier.discard_all_pending();
processing_frontier.set_source_state(crate::RawTransactionIngestSourceState::Closing);
@@ -1777,12 +1840,17 @@ impl crate::RawTransactionIngestStandardLogsSource {
return std::result::Result::Err(source_transport_error(error.code()));
},
};
let mut session_snapshot_source = session.snapshot_source();
let hydration = self.hydration_context();
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);
if let std::result::Result::Err(error) = processing_frontier.observe_websocket_session_snapshot(session_snapshot_source.current()) {
let _closed = session.close().await;
return std::result::Result::Err(error);
}
let mut fault = std::option::Option::None;
let mut bounded_websocket_incident = false;
loop {
if *stop_receiver.borrow() {
break;
@@ -1791,7 +1859,11 @@ impl crate::RawTransactionIngestStandardLogsSource {
fault = std::option::Option::Some(error);
break;
}
let can_receive = coordinator.can_receive();
if bounded_websocket_incident && coordinator.pending_signal_count == 0 && coordinator.tasks.is_empty() {
fault = std::option::Option::Some(crate::runtime_error("source.continuity_gap_proven"));
break;
}
let can_receive = coordinator.can_receive() && !bounded_websocket_incident;
if !can_receive && coordinator.tasks.is_empty() {
fault = std::option::Option::Some(crate::runtime_error("source.hydration_stalled"));
break;
@@ -1801,6 +1873,19 @@ impl crate::RawTransactionIngestStandardLogsSource {
_ = stop_receiver.changed() => {
break;
}
source_snapshot = session_snapshot_source.wait_for_change() => {
let source_snapshot = match source_snapshot {
std::option::Option::Some(value) => value,
std::option::Option::None => {
fault = std::option::Option::Some(crate::runtime_error("source.websocket_snapshot_closed"));
break;
},
};
if let std::result::Result::Err(error) = processing_frontier.observe_websocket_session_snapshot(source_snapshot) {
fault = std::option::Option::Some(error);
break;
}
}
joined = coordinator.tasks.join_next(), if !coordinator.tasks.is_empty() => {
let joined = match joined {
std::option::Option::Some(value) => value,
@@ -1858,10 +1943,18 @@ impl crate::RawTransactionIngestStandardLogsSource {
break;
},
};
let incident_bounded = match processing_frontier.observe_websocket_post_incident_slot(signal.slot) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
fault = std::option::Option::Some(error);
break;
},
};
if let std::result::Result::Err(error) = coordinator.queue_signal(&hydration, signal, received_at, &mut processing_frontier) {
fault = std::option::Option::Some(error);
break;
}
bounded_websocket_incident = bounded_websocket_incident || incident_bounded;
}
}
}
@@ -3364,6 +3457,18 @@ fn map_yellowstone_source_state(state: ksp_onchain_transport_lib::YellowstoneGrp
};
}
fn map_websocket_source_state(state: ksp_onchain_transport_lib::WsSessionState) -> crate::RawTransactionIngestSourceState {
return match state {
ksp_onchain_transport_lib::WsSessionState::Disconnected
| ksp_onchain_transport_lib::WsSessionState::Connecting
| ksp_onchain_transport_lib::WsSessionState::Reconnecting { .. } => crate::RawTransactionIngestSourceState::Reconnecting,
ksp_onchain_transport_lib::WsSessionState::Active => crate::RawTransactionIngestSourceState::Active,
ksp_onchain_transport_lib::WsSessionState::Closing => crate::RawTransactionIngestSourceState::Closing,
ksp_onchain_transport_lib::WsSessionState::Closed => crate::RawTransactionIngestSourceState::Closed,
ksp_onchain_transport_lib::WsSessionState::Failed => crate::RawTransactionIngestSourceState::Failed,
};
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct RawTransactionIngestProcessingSlotState {
pending: usize,
@@ -3447,6 +3552,10 @@ impl RawTransactionIngestProcessingFrontier {
return;
}
fn highest_observed_slot(&self) -> std::option::Option<u64> {
return self.slots.keys().next_back().copied();
}
fn projection(&self) -> crate::RawTransactionIngestProcessingFrontierProjection {
let oldest_pending_slot = self.slots.iter().find_map(|(slot, state)| {
if state.pending == 0 {
@@ -3527,6 +3636,8 @@ struct RawTransactionIngestProcessingFrontierReporter {
source_reconnect_total: u64,
source_replay_attempt_total: u64,
source_continuity_gap_total: u64,
source_overflow_total: u64,
websocket_incident_anchor: std::option::Option<crate::RawTransactionIngestWebSocketIncidentAnchor>,
}
impl RawTransactionIngestProcessingFrontierReporter {
@@ -3538,6 +3649,8 @@ impl RawTransactionIngestProcessingFrontierReporter {
source_reconnect_total: 0,
source_replay_attempt_total: 0,
source_continuity_gap_total: 0,
source_overflow_total: 0,
websocket_incident_anchor: std::option::Option::None,
};
}
@@ -3580,6 +3693,85 @@ impl RawTransactionIngestProcessingFrontierReporter {
);
}
fn observe_websocket_session_snapshot(&mut self, snapshot: ksp_onchain_transport_lib::WsSessionSnapshot) -> ksp_core_lib::Result<()> {
return self.observe_websocket_continuity(map_websocket_source_state(snapshot.state()), snapshot.continuity_gap_count(), snapshot.overflow_count());
}
fn observe_websocket_continuity(
&mut self,
state: crate::RawTransactionIngestSourceState,
reconnect_total: u64,
overflow_total: u64,
) -> ksp_core_lib::Result<()> {
if reconnect_total < self.source_reconnect_total || overflow_total < self.source_overflow_total {
return std::result::Result::Err(crate::runtime_error("source.websocket_continuity_counter_regression"));
}
let continuity_gap_total = match reconnect_total.checked_add(overflow_total) {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(crate::counter_exhausted_error("source.websocket_continuity_gap_total")),
};
if continuity_gap_total < self.source_continuity_gap_total {
return std::result::Result::Err(crate::runtime_error("source.continuity_counter_regression"));
}
let saw_reconnect = reconnect_total > self.source_reconnect_total;
let saw_overflow = overflow_total > self.source_overflow_total;
self.source_reconnect_total = reconnect_total;
self.source_replay_attempt_total = 0;
self.source_continuity_gap_total = continuity_gap_total;
self.source_overflow_total = overflow_total;
if saw_reconnect || saw_overflow {
let start_slot = match self.frontier.highest_observed_slot() {
std::option::Option::Some(value) => value,
std::option::Option::None => {
self.publish();
return std::result::Result::Err(crate::runtime_error("source.websocket_incident_unbounded"));
},
};
match self.websocket_incident_anchor.as_mut() {
std::option::Option::Some(anchor) => {
if let std::result::Result::Err(error) = anchor.extend(reconnect_total, overflow_total, saw_reconnect, saw_overflow) {
self.publish();
return std::result::Result::Err(error);
}
},
std::option::Option::None => {
let anchor =
match crate::RawTransactionIngestWebSocketIncidentAnchor::new(start_slot, reconnect_total, overflow_total, saw_reconnect, saw_overflow)
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
self.publish();
return std::result::Result::Err(error);
},
};
self.websocket_incident_anchor = std::option::Option::Some(anchor);
},
}
}
self.source_state = if self.websocket_incident_anchor.is_some() && state == crate::RawTransactionIngestSourceState::Active {
std::option::Option::Some(crate::RawTransactionIngestSourceState::Reconnecting)
} else {
std::option::Option::Some(state)
};
self.publish();
return std::result::Result::Ok(());
}
fn observe_websocket_post_incident_slot(&mut self, slot: u64) -> ksp_core_lib::Result<bool> {
let anchor = match self.websocket_incident_anchor.as_mut() {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Ok(false),
};
if anchor.end_slot().is_some() {
return std::result::Result::Ok(false);
}
if let std::result::Result::Err(error) = anchor.close_at(slot) {
return std::result::Result::Err(error);
}
self.publish();
return std::result::Result::Ok(true);
}
fn observe_source_continuity(
&mut self,
state: crate::RawTransactionIngestSourceState,