v0.2.9-pre.010
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
# file: Cargo.toml
|
||||
# version: 250
|
||||
# version: 251
|
||||
|
||||
[workspace]
|
||||
resolver = "3"
|
||||
members = ["crates/ksp-app-config-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-wallet-lib"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.2.9-pre.9.fix.1"
|
||||
version = "0.2.9-pre.10"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/src/lib.rs
|
||||
// version: 42
|
||||
// version: 43
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
@@ -140,6 +140,8 @@ pub use self::grpc_settings::YellowstoneGrpcSessionSettings;
|
||||
pub use self::grpc_settings::YellowstoneGrpcTransportSettings;
|
||||
/// Standard Yellowstone bidirectional Subscribe session.
|
||||
pub use self::grpc_stream::SolanaYellowstoneGrpcSubscribeSession;
|
||||
/// Safe Yellowstone reconnect/replay continuity snapshot.
|
||||
pub use self::grpc_stream::YellowstoneGrpcSubscribeSnapshot;
|
||||
/// Safe Yellowstone bidirectional Subscribe lifecycle state.
|
||||
pub use self::grpc_stream::YellowstoneGrpcSubscribeState;
|
||||
/// One validated standard Yellowstone account predicate.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/tests/public_api.rs
|
||||
// version: 47
|
||||
// version: 48
|
||||
|
||||
//! Integration tests for the public `ksp-onchain-transport-lib` consumer contract.
|
||||
|
||||
@@ -993,3 +993,22 @@ fn public_v0_2_9_pre_009_yellowstone_bidi_session_contract_is_available_from_cra
|
||||
assert_eq!(ksp_onchain_transport_lib::ERROR_CODE_GRPC_SESSION_CLOSED.domain(), "onchain_transport");
|
||||
assert_eq!(ksp_onchain_transport_lib::ERROR_CODE_GRPC_SESSION_CLOSED.code(), "grpc_session_closed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_v0_2_9_pre_010_yellowstone_reconnect_snapshot_is_available_from_crate_root() {
|
||||
fn assert_copy<T: Copy>() {
|
||||
let _ = std::marker::PhantomData::<T>;
|
||||
return;
|
||||
}
|
||||
assert_copy::<ksp_onchain_transport_lib::YellowstoneGrpcSubscribeSnapshot>();
|
||||
let _snapshot = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneGrpcSubscribeSnapshot>();
|
||||
let _state = ksp_onchain_transport_lib::YellowstoneGrpcSubscribeState::Reconnecting;
|
||||
let _session_snapshot = ksp_onchain_transport_lib::SolanaYellowstoneGrpcSubscribeSession::snapshot;
|
||||
let _reconnect_count = ksp_onchain_transport_lib::YellowstoneGrpcSubscribeSnapshot::reconnect_count;
|
||||
let _gap_count = ksp_onchain_transport_lib::YellowstoneGrpcSubscribeSnapshot::continuity_gap_count;
|
||||
let _duplicate_count = ksp_onchain_transport_lib::YellowstoneGrpcSubscribeSnapshot::duplicate_update_count;
|
||||
let _replay_count = ksp_onchain_transport_lib::YellowstoneGrpcSubscribeSnapshot::replay_attempt_count;
|
||||
let _requested = ksp_onchain_transport_lib::YellowstoneGrpcSubscribeSnapshot::last_requested_from_slot;
|
||||
let _observed = ksp_onchain_transport_lib::YellowstoneGrpcSubscribeSnapshot::last_observed_slot;
|
||||
let _terminal = ksp_onchain_transport_lib::YellowstoneGrpcSubscribeSnapshot::terminal_error_code;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/tests/release_completeness.rs
|
||||
// version: 40
|
||||
// version: 41
|
||||
|
||||
//! Release-level completeness canaries for staged HTTP and WebSocket Transport coverage.
|
||||
|
||||
@@ -1264,7 +1264,7 @@ fn release_v0_2_9_pre_008_standard_blocks_contract_remains_complete() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_v0_2_9_pre_009_opens_one_bounded_standard_bidi_session_without_reconnect_or_provider_coupling() {
|
||||
fn release_v0_2_9_pre_009_bounded_standard_bidi_session_contract_remains_complete() {
|
||||
let stream_source = include_str!("../src/grpc_stream.rs");
|
||||
let subscribe_source = include_str!("../src/grpc_subscribe.rs");
|
||||
let channel_source = include_str!("../src/grpc_channel.rs");
|
||||
@@ -1301,10 +1301,46 @@ fn release_v0_2_9_pre_009_opens_one_bounded_standard_bidi_session_without_reconn
|
||||
assert!(!stream_source.contains("PublicNode"));
|
||||
assert!(!stream_source.contains("OrbitFlare"));
|
||||
assert!(!stream_source.contains("Helius"));
|
||||
assert!(!stream_source.contains("reconnect"));
|
||||
assert!(!crate_root.contains("pub use tonic"));
|
||||
assert!(!crate_root.contains("pub use yellowstone_grpc_proto"));
|
||||
let _session = std::any::type_name::<ksp_onchain_transport_lib::SolanaYellowstoneGrpcSubscribeSession>();
|
||||
let _state = ksp_onchain_transport_lib::YellowstoneGrpcSubscribeState::Active;
|
||||
let _update = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneSubscribeUpdate>();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_v0_2_9_pre_010_adds_bounded_reconnect_replay_and_conservative_continuity_observability() {
|
||||
let stream_source = include_str!("../src/grpc_stream.rs");
|
||||
let crate_root = include_str!("../src/lib.rs");
|
||||
for required in [
|
||||
"YellowstoneGrpcSubscribeState::Reconnecting",
|
||||
"YellowstoneGrpcSubscribeSnapshot",
|
||||
"reconnect_subscribe_stream",
|
||||
"replay_first_available",
|
||||
"SubscribeReplayInfo",
|
||||
"last_requested_from_slot",
|
||||
"last_observed_slot",
|
||||
"reconnect_count",
|
||||
"replay_attempt_count",
|
||||
"continuity_gap_count",
|
||||
"duplicate_update_count",
|
||||
"MAX_RECENT_UPDATE_IDENTITIES",
|
||||
"first_available > requested",
|
||||
"max_optional_slot",
|
||||
"reconnect budget is exhausted",
|
||||
"request mutation",
|
||||
] {
|
||||
assert!(stream_source.contains(required), "missing pre.010 reconnect/replay contract token: {required}");
|
||||
}
|
||||
assert!(stream_source.contains("still delivered to the caller"));
|
||||
assert!(stream_source.contains("does not claim exactly-once delivery"));
|
||||
assert!(!stream_source.contains("unbounded_channel"));
|
||||
assert!(!stream_source.contains("SubscribeDeshred"));
|
||||
assert!(!stream_source.contains("PublicNode"));
|
||||
assert!(!stream_source.contains("OrbitFlare"));
|
||||
assert!(!stream_source.contains("Helius"));
|
||||
assert!(!crate_root.contains("pub use tonic"));
|
||||
assert!(!crate_root.contains("pub use yellowstone_grpc_proto"));
|
||||
let _snapshot = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneGrpcSubscribeSnapshot>();
|
||||
let _state = ksp_onchain_transport_lib::YellowstoneGrpcSubscribeState::Reconnecting;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/unit_tests/grpc_stream.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum FixtureMode {
|
||||
@@ -11,12 +11,16 @@ enum FixtureMode {
|
||||
Malformed,
|
||||
Oversized,
|
||||
Idle,
|
||||
ReconnectReplay,
|
||||
ReplayGap,
|
||||
ReconnectExhausted,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct FixtureGeyser {
|
||||
mode: FixtureMode,
|
||||
half_close_seen: std::sync::Arc<std::sync::atomic::AtomicBool>,
|
||||
subscribe_calls: std::sync::Arc<std::sync::atomic::AtomicUsize>,
|
||||
}
|
||||
|
||||
#[allow(clippy::implicit_return)] // tonic::async_trait generates async wrapper tails outside the authored fixture bodies.
|
||||
@@ -32,6 +36,10 @@ impl yellowstone_grpc_proto::geyser::geyser_server::Geyser for FixtureGeyser {
|
||||
if let std::result::Result::Err(error) = verify_fixture_metadata(request.metadata()) {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let subscribe_call = self.subscribe_calls.fetch_add(1, std::sync::atomic::Ordering::SeqCst).saturating_add(1);
|
||||
if matches!(self.mode, FixtureMode::ReconnectExhausted) && subscribe_call > 1 {
|
||||
return std::result::Result::Err(tonic::Status::unavailable("GRPC-RECONNECT-SECRET-CANARY"));
|
||||
}
|
||||
let mut inbound = request.into_inner();
|
||||
let (outbound_tx, outbound_rx) = tokio::sync::mpsc::channel(64);
|
||||
let mode = self.mode;
|
||||
@@ -111,6 +119,45 @@ impl yellowstone_grpc_proto::geyser::geyser_server::Geyser for FixtureGeyser {
|
||||
FixtureMode::Idle => {
|
||||
let _ = inbound.message().await;
|
||||
},
|
||||
FixtureMode::ReconnectReplay => {
|
||||
if subscribe_call == 1 {
|
||||
assert_eq!(initial.from_slot, std::option::Option::None);
|
||||
let _ = outbound_tx.send(std::result::Result::Ok(slot_update(500))).await;
|
||||
} else {
|
||||
assert_eq!(subscribe_call, 2);
|
||||
assert_eq!(initial.from_slot, std::option::Option::Some(500));
|
||||
if outbound_tx.send(std::result::Result::Ok(slot_update(500))).await.is_err() {
|
||||
return;
|
||||
}
|
||||
if outbound_tx.send(std::result::Result::Ok(slot_update(501))).await.is_err() {
|
||||
return;
|
||||
}
|
||||
let half_close = inbound.message().await;
|
||||
if matches!(half_close, std::result::Result::Ok(std::option::Option::None)) {
|
||||
half_close_seen.store(true, std::sync::atomic::Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
},
|
||||
FixtureMode::ReplayGap => {
|
||||
if subscribe_call == 1 {
|
||||
assert_eq!(initial.from_slot, std::option::Option::None);
|
||||
let _ = outbound_tx.send(std::result::Result::Ok(slot_update(500))).await;
|
||||
} else {
|
||||
assert_eq!(subscribe_call, 2);
|
||||
assert_eq!(initial.from_slot, std::option::Option::Some(505));
|
||||
if outbound_tx.send(std::result::Result::Ok(slot_update(505))).await.is_err() {
|
||||
return;
|
||||
}
|
||||
let half_close = inbound.message().await;
|
||||
if matches!(half_close, std::result::Result::Ok(std::option::Option::None)) {
|
||||
half_close_seen.store(true, std::sync::atomic::Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
},
|
||||
FixtureMode::ReconnectExhausted => {
|
||||
assert_eq!(subscribe_call, 1);
|
||||
let _ = outbound_tx.send(std::result::Result::Ok(slot_update(700))).await;
|
||||
},
|
||||
}
|
||||
});
|
||||
return std::result::Result::Ok(tonic::Response::new(super::MpscStream::new(outbound_rx)));
|
||||
@@ -125,9 +172,17 @@ impl yellowstone_grpc_proto::geyser::geyser_server::Geyser for FixtureGeyser {
|
||||
|
||||
async fn subscribe_replay_info(
|
||||
&self,
|
||||
_request: tonic::Request<yellowstone_grpc_proto::geyser::SubscribeReplayInfoRequest>,
|
||||
request: tonic::Request<yellowstone_grpc_proto::geyser::SubscribeReplayInfoRequest>,
|
||||
) -> std::result::Result<tonic::Response<yellowstone_grpc_proto::geyser::SubscribeReplayInfoResponse>, tonic::Status> {
|
||||
return std::result::Result::Err(tonic::Status::unimplemented("unary is outside this fixture"));
|
||||
if let std::result::Result::Err(error) = verify_fixture_metadata(request.metadata()) {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let first_available = match self.mode {
|
||||
FixtureMode::ReconnectReplay | FixtureMode::ReconnectExhausted => std::option::Option::Some(400),
|
||||
FixtureMode::ReplayGap => std::option::Option::Some(505),
|
||||
_ => return std::result::Result::Err(tonic::Status::unimplemented("replay info is outside this fixture mode")),
|
||||
};
|
||||
return std::result::Result::Ok(tonic::Response::new(yellowstone_grpc_proto::geyser::SubscribeReplayInfoResponse { first_available }));
|
||||
}
|
||||
|
||||
async fn ping(
|
||||
@@ -176,6 +231,7 @@ impl yellowstone_grpc_proto::geyser::geyser_server::Geyser for FixtureGeyser {
|
||||
struct FixtureServer {
|
||||
endpoint_url: std::string::String,
|
||||
half_close_seen: std::sync::Arc<std::sync::atomic::AtomicBool>,
|
||||
subscribe_calls: std::sync::Arc<std::sync::atomic::AtomicUsize>,
|
||||
shutdown: std::option::Option<tokio::sync::oneshot::Sender<()>>,
|
||||
task: tokio::task::JoinHandle<()>,
|
||||
}
|
||||
@@ -187,9 +243,15 @@ impl FixtureServer {
|
||||
let local_address = incoming.local_addr().expect("fixture gRPC listener must expose local address");
|
||||
let (shutdown, shutdown_receiver) = tokio::sync::oneshot::channel();
|
||||
let half_close_seen = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
|
||||
let subscribe_calls = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
|
||||
let service_half_close_seen = half_close_seen.clone();
|
||||
let service_subscribe_calls = subscribe_calls.clone();
|
||||
let task = tokio::spawn(async move {
|
||||
let service = yellowstone_grpc_proto::geyser::geyser_server::GeyserServer::new(FixtureGeyser { mode, half_close_seen: service_half_close_seen });
|
||||
let service = yellowstone_grpc_proto::geyser::geyser_server::GeyserServer::new(FixtureGeyser {
|
||||
mode,
|
||||
half_close_seen: service_half_close_seen,
|
||||
subscribe_calls: service_subscribe_calls,
|
||||
});
|
||||
let result = tonic::transport::Server::builder()
|
||||
.serve_with_incoming_shutdown(service, incoming, async move {
|
||||
let _ = shutdown_receiver.await;
|
||||
@@ -200,6 +262,7 @@ impl FixtureServer {
|
||||
return Self {
|
||||
endpoint_url: format!("http://{local_address}"),
|
||||
half_close_seen,
|
||||
subscribe_calls,
|
||||
shutdown: std::option::Option::Some(shutdown),
|
||||
task,
|
||||
};
|
||||
@@ -229,13 +292,32 @@ fn fixture_settings(
|
||||
update_capacity: usize,
|
||||
max_inbound_message_size_bytes: usize,
|
||||
max_outbound_message_size_bytes: usize,
|
||||
) -> crate::YellowstoneGrpcEndpointSettings {
|
||||
let defaults = crate::YellowstoneGrpcSessionSettings::default();
|
||||
return fixture_settings_with_reconnect(
|
||||
url,
|
||||
request_capacity,
|
||||
update_capacity,
|
||||
max_inbound_message_size_bytes,
|
||||
max_outbound_message_size_bytes,
|
||||
crate::YellowstoneGrpcReconnectSettings::new(0, defaults.reconnect().initial_backoff(), defaults.reconnect().max_backoff()),
|
||||
);
|
||||
}
|
||||
|
||||
fn fixture_settings_with_reconnect(
|
||||
url: &str,
|
||||
request_capacity: usize,
|
||||
update_capacity: usize,
|
||||
max_inbound_message_size_bytes: usize,
|
||||
max_outbound_message_size_bytes: usize,
|
||||
reconnect: crate::YellowstoneGrpcReconnectSettings,
|
||||
) -> crate::YellowstoneGrpcEndpointSettings {
|
||||
let defaults = crate::YellowstoneGrpcSessionSettings::default();
|
||||
let session = crate::YellowstoneGrpcSessionSettings::new(
|
||||
defaults.connect_timeout(),
|
||||
defaults.unary_timeout(),
|
||||
std::time::Duration::from_millis(250),
|
||||
defaults.reconnect().clone(),
|
||||
reconnect,
|
||||
request_capacity,
|
||||
update_capacity,
|
||||
max_inbound_message_size_bytes,
|
||||
@@ -466,3 +548,138 @@ async fn yellowstone_outbound_mutation_size_is_rejected_before_queue_dispatch()
|
||||
session.close().await.expect("idle fixture must observe client half-close");
|
||||
server.stop().await;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn yellowstone_reconnect_replays_from_last_observed_slot_and_counts_duplicate_identity() {
|
||||
let server = FixtureServer::start(FixtureMode::ReconnectReplay).await;
|
||||
let defaults = crate::YellowstoneGrpcSessionSettings::default();
|
||||
let settings = fixture_settings_with_reconnect(
|
||||
server.endpoint_url.as_str(),
|
||||
8,
|
||||
8,
|
||||
defaults.max_inbound_message_size_bytes(),
|
||||
defaults.max_outbound_message_size_bytes(),
|
||||
crate::YellowstoneGrpcReconnectSettings::new(3, std::time::Duration::from_millis(5), std::time::Duration::from_millis(20)),
|
||||
);
|
||||
let channel = crate::YellowstoneGrpcChannel::connect(&settings).await.expect("fixture channel must connect");
|
||||
let mut session = channel.open_standard_subscribe(initial_request()).await.expect("fixture Subscribe stream must open");
|
||||
let first = session.next_update().await.expect("first slot must decode").expect("first slot must be present");
|
||||
match first {
|
||||
crate::YellowstoneSubscribeUpdate::Slot(value) => assert_eq!(value.slot(), 500),
|
||||
_ => panic!("fixture must return first Slot"),
|
||||
}
|
||||
let duplicate = session.next_update().await.expect("replayed duplicate must decode").expect("replayed duplicate must be present");
|
||||
match duplicate {
|
||||
crate::YellowstoneSubscribeUpdate::Slot(value) => assert_eq!(value.slot(), 500),
|
||||
_ => panic!("fixture must return replayed Slot"),
|
||||
}
|
||||
let resumed = session.next_update().await.expect("resumed slot must decode").expect("resumed slot must be present");
|
||||
match resumed {
|
||||
crate::YellowstoneSubscribeUpdate::Slot(value) => assert_eq!(value.slot(), 501),
|
||||
_ => panic!("fixture must return resumed Slot"),
|
||||
}
|
||||
let snapshot = session.snapshot();
|
||||
assert_eq!(snapshot.state(), crate::YellowstoneGrpcSubscribeState::Active);
|
||||
assert_eq!(snapshot.reconnect_count(), 1);
|
||||
assert_eq!(snapshot.replay_attempt_count(), 1);
|
||||
assert_eq!(snapshot.continuity_gap_count(), 0);
|
||||
assert_eq!(snapshot.duplicate_update_count(), 1);
|
||||
assert_eq!(snapshot.last_requested_from_slot(), std::option::Option::Some(500));
|
||||
assert_eq!(snapshot.last_observed_slot(), std::option::Option::Some(501));
|
||||
session.close().await.expect("reconnected stream must close cleanly");
|
||||
assert_eq!(server.subscribe_calls.load(std::sync::atomic::Ordering::SeqCst), 2);
|
||||
server.stop().await;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn yellowstone_replay_info_proves_and_clamps_retention_gap_without_lossless_claim() {
|
||||
let server = FixtureServer::start(FixtureMode::ReplayGap).await;
|
||||
let defaults = crate::YellowstoneGrpcSessionSettings::default();
|
||||
let settings = fixture_settings_with_reconnect(
|
||||
server.endpoint_url.as_str(),
|
||||
8,
|
||||
8,
|
||||
defaults.max_inbound_message_size_bytes(),
|
||||
defaults.max_outbound_message_size_bytes(),
|
||||
crate::YellowstoneGrpcReconnectSettings::new(3, std::time::Duration::from_millis(5), std::time::Duration::from_millis(20)),
|
||||
);
|
||||
let channel = crate::YellowstoneGrpcChannel::connect(&settings).await.expect("fixture channel must connect");
|
||||
let mut session = channel.open_standard_subscribe(initial_request()).await.expect("fixture Subscribe stream must open");
|
||||
let _ = session.next_update().await.expect("first slot must decode").expect("first slot must be present");
|
||||
let resumed = session.next_update().await.expect("clamped replay must decode").expect("clamped replay must be present");
|
||||
match resumed {
|
||||
crate::YellowstoneSubscribeUpdate::Slot(value) => assert_eq!(value.slot(), 505),
|
||||
_ => panic!("fixture must return clamped Slot"),
|
||||
}
|
||||
let snapshot = session.snapshot();
|
||||
assert_eq!(snapshot.reconnect_count(), 1);
|
||||
assert_eq!(snapshot.replay_attempt_count(), 1);
|
||||
assert_eq!(snapshot.continuity_gap_count(), 1);
|
||||
assert_eq!(snapshot.duplicate_update_count(), 0);
|
||||
assert_eq!(snapshot.last_requested_from_slot(), std::option::Option::Some(505));
|
||||
assert_eq!(snapshot.last_observed_slot(), std::option::Option::Some(505));
|
||||
session.close().await.expect("gap fixture must close cleanly");
|
||||
server.stop().await;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn yellowstone_reconnect_budget_exhaustion_is_terminal_and_safe() {
|
||||
let server = FixtureServer::start(FixtureMode::ReconnectExhausted).await;
|
||||
let defaults = crate::YellowstoneGrpcSessionSettings::default();
|
||||
let settings = fixture_settings_with_reconnect(
|
||||
server.endpoint_url.as_str(),
|
||||
8,
|
||||
8,
|
||||
defaults.max_inbound_message_size_bytes(),
|
||||
defaults.max_outbound_message_size_bytes(),
|
||||
crate::YellowstoneGrpcReconnectSettings::new(2, std::time::Duration::from_millis(5), std::time::Duration::from_millis(10)),
|
||||
);
|
||||
let channel = crate::YellowstoneGrpcChannel::connect(&settings).await.expect("fixture channel must connect");
|
||||
let mut session = channel.open_standard_subscribe(initial_request()).await.expect("fixture Subscribe stream must open");
|
||||
let _ = session.next_update().await.expect("first slot must decode").expect("first slot must be present");
|
||||
let error = session.next_update().await.expect_err("failed reconnect attempts must exhaust the bounded budget");
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_GRPC_CHANNEL_FAILED);
|
||||
let snapshot = session.snapshot();
|
||||
assert_eq!(snapshot.state(), crate::YellowstoneGrpcSubscribeState::Failed);
|
||||
assert_eq!(snapshot.reconnect_count(), 0);
|
||||
assert_eq!(snapshot.replay_attempt_count(), 2);
|
||||
assert_eq!(snapshot.terminal_error_code(), std::option::Option::Some(crate::ERROR_CODE_GRPC_CHANNEL_FAILED));
|
||||
let rendered = format!("{error:?} {session:?}");
|
||||
assert!(!rendered.contains("GRPC-RECONNECT-SECRET-CANARY"));
|
||||
assert!(!rendered.contains(server.endpoint_url.as_str()));
|
||||
assert_eq!(server.subscribe_calls.load(std::sync::atomic::Ordering::SeqCst), 3);
|
||||
let _ = session.close().await;
|
||||
server.stop().await;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn yellowstone_shutdown_interrupts_reconnect_backoff_and_mutation_is_rejected_during_reconnect() {
|
||||
let server = FixtureServer::start(FixtureMode::ReconnectReplay).await;
|
||||
let defaults = crate::YellowstoneGrpcSessionSettings::default();
|
||||
let settings = fixture_settings_with_reconnect(
|
||||
server.endpoint_url.as_str(),
|
||||
8,
|
||||
8,
|
||||
defaults.max_inbound_message_size_bytes(),
|
||||
defaults.max_outbound_message_size_bytes(),
|
||||
crate::YellowstoneGrpcReconnectSettings::new(3, std::time::Duration::from_millis(500), std::time::Duration::from_millis(500)),
|
||||
);
|
||||
let channel = crate::YellowstoneGrpcChannel::connect(&settings).await.expect("fixture channel must connect");
|
||||
let mut session = channel.open_standard_subscribe(initial_request()).await.expect("fixture Subscribe stream must open");
|
||||
let _ = session.next_update().await.expect("first slot must decode").expect("first slot must be present");
|
||||
tokio::time::timeout(std::time::Duration::from_secs(1), async {
|
||||
loop {
|
||||
if session.state() == crate::YellowstoneGrpcSubscribeState::Reconnecting {
|
||||
return;
|
||||
}
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.expect("fixture must enter reconnecting state");
|
||||
let mutation_error = session.try_update(&mutated_request()).expect_err("request mutation must be rejected during reconnect");
|
||||
assert_eq!(mutation_error.code(), crate::ERROR_CODE_GRPC_SESSION_CLOSED);
|
||||
session.close().await.expect("shutdown must interrupt reconnect backoff without opening another stream");
|
||||
assert_eq!(server.subscribe_calls.load(std::sync::atomic::Ordering::SeqCst), 1);
|
||||
server.stop().await;
|
||||
}
|
||||
|
||||
94
deltas/0.2.9/pre.010.md
Normal file
94
deltas/0.2.9/pre.010.md
Normal file
@@ -0,0 +1,94 @@
|
||||
<!-- file: deltas/0.2.9/pre.010.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.2.9-pre.010` — reconnect borné + replay prudent + continuité observable
|
||||
|
||||
## Base
|
||||
|
||||
```text
|
||||
0.2.9-pre.009-fix.001
|
||||
gate opérateur final : fmt/audit/check/Clippy/workspace PASS sans warning
|
||||
Transport : 379 unit + 48 public API + 42 release-completeness + 4 doctests
|
||||
```
|
||||
|
||||
## Changements runtime
|
||||
|
||||
- ajoute l'état public `YellowstoneGrpcSubscribeState::Reconnecting` ;
|
||||
- ajoute `YellowstoneGrpcSubscribeSnapshot` avec compteurs sûrs de reconnect/replay/continuité ;
|
||||
- active le `YellowstoneGrpcReconnectSettings` déjà introduit en `pre.002` pour le stream `Subscribe` ;
|
||||
- rouvre le stream avec un backoff exponentiel borné et un nombre d'essais borné ;
|
||||
- conserve le dernier `YellowstoneSubscribeRequest` complet accepté ;
|
||||
- après perte du stream, resoumet ce request déterministement avec un `from_slot` au moins égal au plus haut slot déjà observé ;
|
||||
- consulte `SubscribeReplayInfo` avant les tentatives de replay lorsque `from_slot` est disponible ;
|
||||
- si `first_available > from_slot`, incrémente `continuity_gap_count` et clamp le replay à `first_available` ;
|
||||
- maintient un cache borné de 512 identités d'updates pour observer les duplicates autour d'un replay ;
|
||||
- les duplicates restent livrés : aucun contrat exactly-once n'est inventé ;
|
||||
- rejette les mutations request pendant `Reconnecting` pour éviter une application ambiguë entre deux streams physiques ;
|
||||
- un shutdown pendant le backoff interrompt immédiatement la boucle et interdit une nouvelle ouverture ;
|
||||
- un épuisement du budget devient terminal avec un code KSP sûr `grpc_channel_failed`.
|
||||
|
||||
## Sémantique de continuité
|
||||
|
||||
```text
|
||||
from_slot de reconnect = max(from_slot explicite du dernier request, last_observed_slot)
|
||||
ReplayInfo = information de rétention ; pas preuve de replay complet
|
||||
continuity gap = couverture de replay indisponible, comptée seulement si first_available > slot demandé
|
||||
slot manquant entre deux updates filtrés = jamais assimilé automatiquement à un gap
|
||||
duplicate = identité KSP bornée déjà observée ; update toujours livrée
|
||||
exactly-once = non garanti
|
||||
lossless = non garanti
|
||||
ordre global sans gap = non garanti
|
||||
```
|
||||
|
||||
Identités retenues pour l'observation bornée :
|
||||
|
||||
```text
|
||||
Account slot + pubkey + write_version
|
||||
Slot slot + status
|
||||
Transaction slot + signature
|
||||
TransactionStatus slot + signature
|
||||
Block slot + blockhash
|
||||
BlockMeta slot + blockhash
|
||||
Entry slot + index + hash
|
||||
Ping/Pong hors déduplication
|
||||
```
|
||||
|
||||
La couverture d'une éventuelle divergence de node reste volontairement limitée : deux Block/BlockMeta de même slot mais de blockhash différent ne sont pas classés duplicate. KSP ne généralise pas cette preuve aux familles qui ne transportent pas de blockhash.
|
||||
|
||||
## Fixture locale
|
||||
|
||||
| Cas | Preuve |
|
||||
|------------------------------------------------|-------------|
|
||||
| reconnect après server half-close | PASS source |
|
||||
| `from_slot = last_observed_slot` | PASS source |
|
||||
| ReplayInfo disponible | PASS source |
|
||||
| replay duplicate observable | PASS source |
|
||||
| couverture replay indisponible prouvée + clamp | PASS source |
|
||||
| budget reconnect épuisé | PASS source |
|
||||
| mutation pendant reconnect rejetée | PASS source |
|
||||
| shutdown pendant backoff | PASS source |
|
||||
| endpoint/Status secret non réémis | PASS source |
|
||||
| anciens gates bidi avec reconnect `0` | PASS source |
|
||||
|
||||
## Frontières
|
||||
|
||||
```text
|
||||
OUT pre.010 : Config V3 / PublicNode / provider facade
|
||||
OUT 0.2.9 : SubscribeDeshred
|
||||
aucune nouvelle dépendance / feature Cargo
|
||||
aucun yellowstone-grpc-client runtime
|
||||
```
|
||||
|
||||
## Validation candidate
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
python3 scripts/audit_rust_workspace_rules.py
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo test -p ksp-onchain-transport-lib
|
||||
cargo test -p ksp-core-lib --test workspace_dependencies
|
||||
cargo test --workspace
|
||||
```
|
||||
|
||||
Le `cargo tree` n'est pas requis : aucune dépendance ni feature n'a changé.
|
||||
@@ -1,9 +1,9 @@
|
||||
<!-- file: docs/plans/016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md -->
|
||||
<!-- version: 19 -->
|
||||
<!-- version: 20 -->
|
||||
|
||||
# Plan `0.2.9` — moteur Yellowstone gRPC + standard Solana + PublicNode
|
||||
|
||||
> **Statut : `0.2.9-pre.009` est fonctionnellement verte sur gate opérateur (Transport 379 unit + 48 public API + 42 completeness + 4 doctests, workspace PASS), mais Clippy signale deux warnings d’hygiène. `0.2.9-pre.009-fix.001` est candidate pour les supprimer sans modifier le protocole ni le lifecycle ; reconnect/replay restent `pre.010`.**
|
||||
> **Statut : `0.2.9-pre.009-fix.001` est fermé sur gate opérateur intégralement vert : fmt/audit/check/Clippy sans warning, Transport 379 unit + 48 public API + 42 completeness + 4 doctests, dependency canary et workspace PASS. `0.2.9-pre.010` est candidate pour le reconnect/replay KSP-owned, les gaps de couverture replay prouvés par ReplayInfo et les duplicates observables sans promesse lossless.**
|
||||
|
||||
## 1. Objet, base et état d'ouverture
|
||||
|
||||
@@ -934,11 +934,11 @@ pre.007 DONE — standard Solana : Transactions + transaction_status
|
||||
pre.008 DONE — standard Solana : Blocks + block_meta + entry
|
||||
budget : 15–20 min ; gate final fix.001 : fmt/audit/check/Clippy/workspace PASS sans warning + Transport 370/47/41/4
|
||||
|
||||
pre.009 FIX.001 CANDIDATE — moteur partagé : bidi mutation + Ping/Pong + half-close + backpressure + shutdown
|
||||
budget : 15–20 min ; gate fonctionnel 379/48/42/4 + workspace PASS ; fix de deux warnings Clippy
|
||||
pre.009 DONE — moteur partagé : bidi mutation + Ping/Pong + half-close + backpressure + shutdown
|
||||
budget : 15–20 min ; gate final fix.001 : fmt/audit/check/Clippy/workspace PASS sans warning + Transport 379/48/42/4
|
||||
|
||||
pre.010 moteur partagé : reconnect/resubscribe + from_slot/ReplayInfo + gaps/duplicates
|
||||
budget : 15–20 min ; preuve : reconnect local déterministe + aucune promesse lossless
|
||||
pre.010 CANDIDATE — moteur partagé : reconnect/resubscribe + from_slot/ReplayInfo + gaps/duplicates
|
||||
budget : 15–20 min ; preuve source : reconnect local déterministe + ReplayInfo clamp + duplicate observable + aucune promesse lossless
|
||||
|
||||
pre.011 Config V3 + séparation protocol/provider + profils PublicNode Mainnet/Testnet
|
||||
budget : 15–20 min ; preuve : V1/V2 backward + schema/mapping/redaction + Config -> Transport
|
||||
@@ -1387,4 +1387,34 @@ Le fix applique deux corrections sans `allow` :
|
||||
|
||||
Aucun changement n’est apporté aux neuf variantes wire, à la mutation request, à Ping/Pong, au half-close, au shutdown, à la backpressure, aux dépendances ou aux frontières de `pre.010`.
|
||||
|
||||
**Gate fix requis :** fmt/audit/check/Clippy sans warning + Transport 379/48/42/4 + dependency canary + workspace.
|
||||
**Gate fix final :** fmt/audit/check/Clippy sans warning + Transport 379/48/42/4 + dependency canary + workspace **PASS le 2026-08-24**.
|
||||
|
||||
|
||||
## 28. `pre.010` — reconnect KSP-owned, reprise `from_slot` et continuité prudente
|
||||
|
||||
Le proto courant conserve `SubscribeRequest.from_slot` et le unary `SubscribeReplayInfo.first_available`. Le changelog upstream relu le `2026-08-24` confirme qu’un replay `from_slot` peut avoir des défauts spécifiques de famille : le correctif du `2026-07-22` concernait précisément des subscriptions Blocks acceptées mais reprenant live avec un state gap. KSP ne transforme donc pas la présence de `from_slot` en garantie lossless.
|
||||
|
||||
Politique retenue :
|
||||
|
||||
```text
|
||||
stream perdu -> état Reconnecting
|
||||
budget -> max_retries borné déjà dans YellowstoneGrpcReconnectSettings
|
||||
backoff -> exponentiel, initial/max bornés
|
||||
request de resubscribe -> dernier request complet accepté
|
||||
resume slot -> max(from_slot explicite, highest observed slot)
|
||||
ReplayInfo first_available > resume -> couverture replay indisponible prouvée + compteur + clamp à first_available
|
||||
ReplayInfo indisponible -> reconnect poursuit sans inférer de gap
|
||||
mutation pendant reconnect -> rejet explicite, pas de coalescing ambigu
|
||||
duplicate replay -> identité bornée comptée mais update toujours livrée
|
||||
shutdown pendant backoff -> interruption, aucune nouvelle ouverture
|
||||
```
|
||||
|
||||
`YellowstoneGrpcSubscribeSnapshot` expose `reconnect_count`, `continuity_gap_count`, `duplicate_update_count`, `replay_attempt_count`, `last_requested_from_slot`, `last_observed_slot`, l’état lifecycle et le terminal error code sûr. Le cache d’identité est borné à 512 entrées et ne contient pas les payloads arbitraires complets.
|
||||
|
||||
La détection de gap est volontairement **plus stricte** qu’un simple saut de numéro de slot : avec des filtres Accounts/Transactions/Blocks, l’absence d’update sur un slot intermédiaire peut être normale. Seule une borne de rétention `first_available` supérieure au replay demandé constitue ici une preuve objective que cette plage n'est plus rejouable ; elle ne prouve pas qu'un update correspondant aux filtres existait ou a été perdu.
|
||||
|
||||
De même, KSP ne supprime pas les duplicates : il les observe autour du replay, les compte et continue de les livrer. Cela maintient explicitement les non-promesses `exactly-once`, `lossless` et `ordre global sans gap`.
|
||||
|
||||
Couverture de divergence : les identités Block/BlockMeta incluent le blockhash ; deux histoires de même slot avec blockhash différent ne sont donc pas prises pour un duplicate. Aucune généralisation d’equivocation n’est annoncée pour les familles sans preuve de blockhash.
|
||||
|
||||
**Gate candidat :** fixture locale reconnect/replay, duplicate, gap de couverture replay, budget épuisé, mutation rejetée pendant reconnect et shutdown pendant backoff ; audit statique clean. Aucun changement de dépendance ou feature.
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<!-- file: docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md -->
|
||||
<!-- version: 20 -->
|
||||
<!-- version: 21 -->
|
||||
|
||||
# Validation `0.2.9` — moteur Yellowstone + standard Solana + PublicNode
|
||||
|
||||
> **Statut : `pre.009` est fonctionnellement verte : Transport 379 unit + 48 public API + 42 completeness + 4 doctests et workspace PASS. Clippy signale deux warnings d’hygiène ; `pre.009-fix.001` est candidate pour les supprimer sans changement de protocole/lifecycle.**
|
||||
> **Statut : `pre.009-fix.001` est fermé : fmt/audit/check/Clippy sans warning, Transport 379 unit + 48 public API + 42 completeness + 4 doctests, dependency canary et workspace PASS. `pre.010` est candidate reconnect/replay avec observabilité prudente des gaps/duplicates.**
|
||||
|
||||
## 1. Autorités du gate
|
||||
|
||||
@@ -287,25 +287,25 @@ DONE pre.003 source/tests channel/client Debug sans URL, metadata value ni raw
|
||||
|
||||
## 11. Lifecycle / backpressure / replay
|
||||
|
||||
| Cas | Attendu | État |
|
||||
|--------------------------------------------|------------------------------------------|-----------------------------------|
|
||||
| stream open | session bornée | CANDIDATE pre.009 |
|
||||
| request mutation | ordre déterministe | CANDIDATE pre.009 |
|
||||
| server Ping -> client request ping -> Pong | explicite | CANDIDATE pre.009 |
|
||||
| server half-close | terminal normal avant reconnect | CANDIDATE pre.009 |
|
||||
| client close | half-close + cleanup borné | CANDIDATE pre.009 |
|
||||
| receiver drop | Drop session => cleanup best-effort | CANDIDATE pre.009 |
|
||||
| slow subscription | pas de queue infinie | CANDIDATE pre.009 |
|
||||
| inbound oversized | limite Tonic avant payload KSP | CANDIDATE pre.009 |
|
||||
| outbound oversized | encoded_len avant queue/write | CANDIDATE pre.009 |
|
||||
| reconnect budget | borné | TODO |
|
||||
| resubscribe order | déterministe | TODO |
|
||||
| `from_slot` | utilisé sans promesse lossless | TODO |
|
||||
| ReplayInfo | informatif | DONE unary ; usage reconnect TODO |
|
||||
| duplicates | observables | TODO |
|
||||
| gaps | observables | TODO |
|
||||
| divergent node history | couverture documentée | TODO |
|
||||
| shutdown during reconnect | aucune nouvelle connexion après shutdown | TODO |
|
||||
| Cas | Attendu | État |
|
||||
|--------------------------------------------|-----------------------------------------------------------------|----------------------------------------|
|
||||
| stream open | session bornée | DONE pre.009 / gate fix.001 PASS |
|
||||
| request mutation | ordre déterministe | DONE pre.009 |
|
||||
| server Ping -> client request ping -> Pong | explicite | DONE pre.009 |
|
||||
| server half-close | reconnect si budget > 0 ; terminal normal sinon | CANDIDATE pre.010 |
|
||||
| client close | half-close + cleanup borné | DONE pre.009 |
|
||||
| receiver drop | Drop session => cleanup best-effort | DONE pre.009 |
|
||||
| slow subscription | pas de queue infinie | DONE pre.009 |
|
||||
| inbound oversized | limite Tonic avant payload KSP | DONE pre.009 |
|
||||
| outbound oversized | encoded_len avant queue/write | DONE pre.009 |
|
||||
| reconnect budget | borné | CANDIDATE pre.010 |
|
||||
| resubscribe order | dernier request complet accepté | CANDIDATE pre.010 |
|
||||
| `from_slot` | max(explicite, highest observed), sans promesse lossless | CANDIDATE pre.010 |
|
||||
| ReplayInfo | informatif ; clamp seulement si gap de couverture replay prouvé | DONE unary + CANDIDATE usage reconnect |
|
||||
| duplicates | identités bornées observables, jamais supprimées | CANDIDATE pre.010 |
|
||||
| gaps | seulement si `first_available > requested_from_slot` | CANDIDATE pre.010 |
|
||||
| divergent node history | Block/BlockMeta hash-aware ; pas de claim global | CANDIDATE couverture documentée |
|
||||
| shutdown during reconnect | aucune nouvelle connexion après shutdown | CANDIDATE pre.010 |
|
||||
|
||||
Claims interdits sans nouvelle preuve :
|
||||
|
||||
@@ -411,8 +411,8 @@ pre.005 DONE standard: accounts + slots 15
|
||||
pre.006 DONE structure: namespace privé HTTP `http_*` 15–20 min ; gate PASS
|
||||
pre.007 DONE standard: transactions + transaction_status 15–20 min ; gate PASS
|
||||
pre.008 DONE standard: blocks + block_meta + entry 15–20 min ; gate final fix.001 PASS sans warning
|
||||
pre.009 FIX.001 CANDIDATE moteur: bidi/backpressure/half-close/shutdown 15–20 min ; gate fonctionnel vert, 2 warnings Clippy
|
||||
pre.010 TODO moteur: reconnect/replay/gap/duplicate 15–20 min
|
||||
pre.009 DONE moteur: bidi/backpressure/half-close/shutdown 15–20 min ; gate final fix.001 PASS sans warning, 379/48/42/4
|
||||
pre.010 CANDIDATE moteur: reconnect/replay/gap/duplicate 15–20 min ; source/fixture candidate, gate opérateur à exécuter
|
||||
pre.011 TODO Config V3 + protocol/provider + profils PublicNode 15–20 min
|
||||
pre.012 TODO PublicNode live + compliance + docs/prompt 0.2.10 15–20 min
|
||||
rel.001 TODO stable
|
||||
@@ -918,7 +918,7 @@ Le flux sortant utilise la même `mpsc` bornée que celle fournie à `tonic::cli
|
||||
|
||||
`pre.009` ne tente aucun reconnect. Un half-close serveur est terminal normal ; `Status`, decode invalide et overflow sont terminaux en erreur. Le budget de reconnect, l'ordre de resubscribe, `from_slot`/ReplayInfo, gaps, duplicates et node divergence restent exclusivement `pre.010`.
|
||||
|
||||
**Verdict `pre.009` :** contrat fonctionnel validé ; fix Clippy-only requis avant fermeture.
|
||||
**Verdict `pre.009` :** fermé après `pre.009-fix.001`, gate opérateur final intégralement vert et sans warning.
|
||||
|
||||
## 28. Gate `pre.009-fix.001` — taille de l’enum update + canari `Send`
|
||||
|
||||
@@ -937,6 +937,45 @@ Le flux sortant utilise la même `mpsc` bornée que celle fournie à `tonic::cli
|
||||
| `large_enum_variant` | Transaction ~664 B, Block ~280 B | `Transaction(Box<YellowstoneTransactionUpdate>)` |
|
||||
| `extra_unused_type_parameters` | helper `assert_send<T>()` n’utilise pas T | `PhantomData<T>` matérialise l’usage compile-time |
|
||||
|
||||
Le boxing ne modifie ni le protobuf ni `YellowstoneTransactionUpdate` : il réduit uniquement la taille du discriminant public transporté dans la queue bidi. Le canari `Send` conserve la même contrainte et ne construit aucune session réseau. Aucun `allow` Clippy n’est ajouté.
|
||||
Le boxing ne modifie ni le protobuf ni `YellowstoneTransactionUpdate` : il réduit uniquement la taille du discriminant public transporté dans la queue bidi. Le canari `Send` conserve la même contrainte et ne construit aucune session réseau. Aucun `allow` Clippy n’est ajouté. Le gate opérateur final du `2026-08-24` repasse intégralement vert, Clippy sans warning, avec les compteurs 379/48/42/4 inchangés.
|
||||
|
||||
**Verdict fix candidate :** correction minimale prête ; fermeture de `pre.009` après gate opérateur sans warning.
|
||||
|
||||
|
||||
## 29. Candidate `pre.010` — reconnect/replay et continuité observable
|
||||
|
||||
Réaduit upstream au `2026-08-24` :
|
||||
|
||||
```text
|
||||
SubscribeRequest.from_slot toujours présent
|
||||
SubscribeReplayInfoResponse.first_available? toujours présent
|
||||
changelog 2026-07-22 fix replay Blocks/state gap
|
||||
changelog 2026-06-15 upstream autoreconnect traite l'equivocation par quarantaine/blockhash
|
||||
```
|
||||
|
||||
KSP conserve sa stratégie B : aucune importation de la sémantique `yellowstone-grpc-client` autoreconnect. Le stream KSP rouvre lui-même `/geyser.Geyser/Subscribe` sur le channel N1 existant.
|
||||
|
||||
| Preuve source `pre.010` | État |
|
||||
|-------------------------------------------------------------|--------------------|
|
||||
| état public `Reconnecting` | SOURCE |
|
||||
| snapshot reconnect/replay/gap/duplicate | SOURCE |
|
||||
| budget `max_retries` | SOURCE |
|
||||
| backoff exponentiel initial/max | SOURCE |
|
||||
| dernier request complet resoumis | SOURCE |
|
||||
| resume `max(explicit, last_observed)` | SOURCE |
|
||||
| ReplayInfo consulté avant replay | SOURCE |
|
||||
| `first_available > requested` => replay non couvert + clamp | SOURCE+TEST |
|
||||
| duplicate identité bornée, update livrée | SOURCE+TEST |
|
||||
| mutation refusée pendant `Reconnecting` | SOURCE+TEST |
|
||||
| shutdown interrompt le backoff | SOURCE+TEST |
|
||||
| épuisement budget => terminal sûr | SOURCE+TEST |
|
||||
| cache identité borné à 512 | SOURCE |
|
||||
| exactement-once/lossless | EXPLICIT NON-CLAIM |
|
||||
| Config V3 / PublicNode | OUT pre.010 |
|
||||
| `SubscribeDeshred` | OUT 0.2.9 |
|
||||
|
||||
La détection de gap n'utilise pas les sauts entre slots d'updates filtrés : un filtre peut légitimement ne produire aucun message pendant plusieurs slots. Le seul gap compté par cette tranche est une plage de replay objectivement devenue indisponible d'après `SubscribeReplayInfo.first_available` ; cela ne prouve pas qu'un update correspondant aux filtres existait ou a été perdu.
|
||||
|
||||
Le cache de duplicates retient des identités minimales Account/Slot/Transaction/TransactionStatus/Block/BlockMeta/Entry. Il ne retient pas les payloads complets et ne supprime jamais un update. Pour Block/BlockMeta, le blockhash fait partie de l'identité afin de ne pas classer deux histoires divergentes de même slot comme un simple duplicate ; aucune détection globale d'equivocation n'est prétendue.
|
||||
|
||||
**Gate candidate attendu :** audit statique clean ; après exécution opérateur, Transport attendu autour de 383 unit / 49 public API / 43 release-completeness / 4 doctests. Aucune dépendance/feature changée, donc pas de `cargo tree` supplémentaire.
|
||||
|
||||
Reference in New Issue
Block a user