From 7c12ec886b0ea4bca433792ada523e2ad2c40f6c Mon Sep 17 00:00:00 2001 From: SinuS Von SifriduS Date: Sun, 23 Aug 2026 17:19:44 +0200 Subject: [PATCH] v0.2.8-pre.008 --- Cargo.toml | 4 +- .../src/ws_helius_transactions.rs | 55 +++- .../src/ws_protocol_session.rs | 11 +- .../tests/release_completeness.rs | 25 +- .../unit_tests/ws_helius_transactions.rs | 196 ++++++++++++++- deltas/0.2.8/pre.008.md | 238 ++++++++++++++++++ ...0_2_8_HELIUS_LASERSTREAM_WEBSOCKET_PLAN.md | 112 +++++++-- ...011-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET.md | 58 ++++- 8 files changed, 672 insertions(+), 27 deletions(-) create mode 100644 deltas/0.2.8/pre.008.md diff --git a/Cargo.toml b/Cargo.toml index 04ae5a8..a587897 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ # file: Cargo.toml -# version: 231 +# version: 232 [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.8-pre.7.fix.4" +version = "0.2.8-pre.8" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" diff --git a/crates/ksp-onchain-transport-lib/src/ws_helius_transactions.rs b/crates/ksp-onchain-transport-lib/src/ws_helius_transactions.rs index a1942a6..53793c8 100644 --- a/crates/ksp-onchain-transport-lib/src/ws_helius_transactions.rs +++ b/crates/ksp-onchain-transport-lib/src/ws_helius_transactions.rs @@ -1,5 +1,5 @@ // file: crates/ksp-onchain-transport-lib/src/ws_helius_transactions.rs -// version: 4 +// version: 5 const MAX_HELIUS_TRANSACTION_FILTER_ACCOUNTS: usize = 50_000; @@ -348,7 +348,7 @@ fn helius_transaction_subscribe_params(request: &crate::HeliusTransactionSubscri /// /// The nested transaction payload is deliberately retained as JSON because its exact Solana wire representation depends on the requested encoding and detail /// mode. KSP types the stable provider envelope while preserving the full nested payload without Program-specific decoding. -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, PartialEq)] pub struct HeliusFullTransactionNotification { transaction: serde_json::Value, signature: std::string::String, @@ -382,8 +382,20 @@ impl HeliusFullTransactionNotification { } } +impl std::fmt::Debug for HeliusFullTransactionNotification { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + return formatter + .debug_struct("HeliusFullTransactionNotification") + .field("transaction", &"") + .field("signature", &"") + .field("slot", &self.slot) + .field("transaction_index", &self.transaction_index) + .finish(); + } +} + /// Signatures-mode notification delivered by Helius `transactionSubscribe`. -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, PartialEq)] pub struct HeliusTransactionSignatureNotification { signature: std::string::String, slot: u64, @@ -438,11 +450,36 @@ impl HeliusTransactionSignatureNotification { } } +impl std::fmt::Debug for HeliusTransactionSignatureNotification { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + return formatter + .debug_struct("HeliusTransactionSignatureNotification") + .field("signature", &"") + .field("slot", &self.slot) + .field("transaction_index", &self.transaction_index) + .field("err", &wire_field_debug_state(&self.err)) + .field("memo", &wire_field_debug_state(&self.memo)) + .field("block_time", &wire_field_debug_state(&self.block_time)) + .field("confirmation_status", &wire_field_debug_state(&self.confirmation_status)) + .finish(); + } +} + +fn wire_field_debug_state(field: &crate::SolanaWireField) -> &'static str { + if field.is_omitted() { + return "omitted"; + } + if field.is_null() { + return "null"; + } + return "value"; +} + /// Typed Helius `transactionNotification` payload union. /// /// `Full` also covers the provider `accounts` detail mode because both contain the nested `transaction` member. `Signature` covers the lightweight /// signatures mode. `Unknown` preserves `none` mode and forward-compatible provider shapes instead of failing the logical subscription. -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, PartialEq)] #[non_exhaustive] pub enum HeliusTransactionNotification { /// Full/accounts notification carrying the nested transaction payload. @@ -453,6 +490,16 @@ pub enum HeliusTransactionNotification { Unknown(serde_json::Value), } +impl std::fmt::Debug for HeliusTransactionNotification { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + return match self { + Self::Full(notification) => formatter.debug_tuple("Full").field(notification).finish(), + Self::Signature(notification) => formatter.debug_tuple("Signature").field(notification).finish(), + Self::Unknown(_) => formatter.debug_tuple("Unknown").field(&"").finish(), + }; + } +} + impl crate::HeliusLaserStreamWsSession { /// Opens one Helius `transactionSubscribe` logical subscription through the shared physical actor. /// diff --git a/crates/ksp-onchain-transport-lib/src/ws_protocol_session.rs b/crates/ksp-onchain-transport-lib/src/ws_protocol_session.rs index abcb287..44e30bc 100644 --- a/crates/ksp-onchain-transport-lib/src/ws_protocol_session.rs +++ b/crates/ksp-onchain-transport-lib/src/ws_protocol_session.rs @@ -1,11 +1,20 @@ // file: crates/ksp-onchain-transport-lib/src/ws_protocol_session.rs -// version: 4 +// version: 5 /// Typed facade for one standard Solana WebSocket physical session. /// /// The facade delegates to the same [`crate::WsSession`] actor used by the compatibility API. It owns no socket, registry, reconnect loop or queue of its /// own and therefore does not duplicate the physical WebSocket runtime. Subscription wrappers are implemented beside their wire owners in the /// `ws_accounts`, `ws_blocks`, `ws_cluster` and `ws_transactions` modules. +/// +/// ```compile_fail +/// async fn unsupported_helius_transaction( +/// session: &ksp_onchain_transport_lib::SolanaStandardWsSession, +/// request: &ksp_onchain_transport_lib::HeliusTransactionSubscribeRequest, +/// ) { +/// let _ = session.transaction_subscribe(request).await; +/// } +/// ``` #[derive(Clone)] pub struct SolanaStandardWsSession { inner: crate::WsSession, diff --git a/crates/ksp-onchain-transport-lib/tests/release_completeness.rs b/crates/ksp-onchain-transport-lib/tests/release_completeness.rs index a2e0603..5c67452 100644 --- a/crates/ksp-onchain-transport-lib/tests/release_completeness.rs +++ b/crates/ksp-onchain-transport-lib/tests/release_completeness.rs @@ -1,5 +1,5 @@ // file: crates/ksp-onchain-transport-lib/tests/release_completeness.rs -// version: 29 +// version: 30 //! Release-level completeness canaries for staged HTTP and WebSocket Transport coverage. @@ -808,7 +808,7 @@ fn release_v0_2_8_pre_003_helius_surface_is_exactly_six_standard_families_before assert!(source.contains("unsupported_block")); assert!(source.contains("unsupported_slots_updates")); assert!(source.contains("unsupported_vote")); - assert!(!source.contains("transaction_subscribe")); + assert!(!source.contains("pub async fn transaction_subscribe")); assert_eq!(ksp_onchain_transport_lib::WsProtocolKind::HeliusLaserStream.as_str(), "helius_laserstream"); } @@ -865,3 +865,24 @@ fn release_v0_2_8_pre_007_helius_heartbeat_is_provider_owned_by_shared_actor_onl let protocol_source = include_str!("../src/ws_protocol_session.rs"); assert!(!protocol_source.contains("heartbeat_interval")); } + +#[test] +fn release_v0_2_8_pre_008_adversarial_guards_preserve_provider_isolation_and_safe_diagnostics() { + let protocol_source = include_str!("../src/ws_protocol_session.rs"); + assert!(protocol_source.contains("unsupported_helius_transaction")); + assert!(protocol_source.contains("unsupported_block")); + assert!(protocol_source.contains("unsupported_slots_updates")); + assert!(protocol_source.contains("unsupported_vote")); + let helius_source = include_str!("../src/ws_helius_transactions.rs"); + assert!(helius_source.contains("impl std::fmt::Debug for HeliusFullTransactionNotification")); + assert!(helius_source.contains("impl std::fmt::Debug for HeliusTransactionSignatureNotification")); + assert!(helius_source.contains("impl std::fmt::Debug for HeliusTransactionNotification")); + assert!(helius_source.contains("Self::Unknown(_)")); + assert!(helius_source.contains("")); + let actor_source = include_str!("../src/ws_session.rs"); + assert!(actor_source.contains("max_message_size_bytes")); + assert!(actor_source.contains("max_frame_size_bytes")); + assert!(actor_source.contains("WsNotificationDispatchOutcome::QueueFull")); + assert!(actor_source.contains("ERROR_CODE_WS_BACKPRESSURE_OVERFLOW")); + assert!(actor_source.contains("remote_to_local.remove")); +} diff --git a/crates/ksp-onchain-transport-lib/unit_tests/ws_helius_transactions.rs b/crates/ksp-onchain-transport-lib/unit_tests/ws_helius_transactions.rs index af85fa3..d7b70a8 100644 --- a/crates/ksp-onchain-transport-lib/unit_tests/ws_helius_transactions.rs +++ b/crates/ksp-onchain-transport-lib/unit_tests/ws_helius_transactions.rs @@ -1,5 +1,5 @@ // file: crates/ksp-onchain-transport-lib/unit_tests/ws_helius_transactions.rs -// version: 4 +// version: 5 use futures_util::SinkExt; // rust-rules: trait-import use futures_util::StreamExt; // rust-rules: trait-import @@ -78,6 +78,23 @@ fn backpressure_session_settings() -> crate::WsSessionSettings { ); } +fn adversarial_payload_session_settings() -> crate::WsSessionSettings { + let defaults = crate::WsSessionSettings::default(); + return crate::WsSessionSettings::new( + std::time::Duration::from_millis(250), + std::time::Duration::from_millis(200), + crate::WsReconnectSettings::new(2, std::time::Duration::from_millis(20), std::time::Duration::from_millis(20)), + crate::WsResubscribePolicy::ActiveSubscriptions, + defaults.command_queue_capacity(), + defaults.notification_queue_capacity(), + defaults.max_active_subscriptions(), + defaults.max_pending_requests(), + 256, + 128, + defaults.max_write_buffer_size_bytes(), + ); +} + async fn bind_local_listener() -> (tokio::net::TcpListener, std::string::String) { let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.expect("local listener must bind"); let address = listener.local_addr().expect("local listener must expose address"); @@ -97,6 +114,19 @@ async fn send_result(websocket: &mut tokio_tungstenite::WebSocketStream, + request: &serde_json::Value, + code: i64, + message: &str, + data: serde_json::Value, +) { + let id = request.get("id").and_then(serde_json::Value::as_u64).expect("request id must be numeric"); + let response = serde_json::json!({"jsonrpc":"2.0","id":id,"error":{"code":code,"message":message,"data":data}}); + websocket.send(tokio_tungstenite::tungstenite::Message::Text(response.to_string().into())).await.expect("local error response must send"); + return; +} + async fn send_notification(websocket: &mut tokio_tungstenite::WebSocketStream, subscription: u64, result: serde_json::Value) { let notification = serde_json::json!({"jsonrpc":"2.0","method":"transactionNotification","params":{"subscription":subscription,"result":result}}); websocket.send(tokio_tungstenite::tungstenite::Message::Text(notification.to_string().into())).await.expect("local notification must send"); @@ -115,7 +145,7 @@ async fn send_root_notification(websocket: &mut tokio_tungstenite::WebSocketStre async fn wait_for_gap_count(session: &crate::HeliusLaserStreamWsSession, expected: u64) { let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(2); loop { - if session.snapshot().continuity_gap_count() >= expected { + if session.snapshot().continuity_gap_count() >= expected && session.state() == crate::WsSessionState::Active { return; } assert!(tokio::time::Instant::now() < deadline, "Helius session continuity gap count must advance before timeout"); @@ -134,6 +164,17 @@ async fn wait_for_overflow_count(session: &crate::HeliusLaserStreamWsSession, ex } } +async fn wait_for_session_subscription_count(session: &crate::HeliusLaserStreamWsSession, expected: usize) { + let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(2); + loop { + if session.snapshot().subscription_count() == expected { + return; + } + assert!(tokio::time::Instant::now() < deadline, "Helius session subscription count must settle before timeout"); + tokio::time::sleep(std::time::Duration::from_millis(5)).await; + } +} + async fn wait_for_subscription_state(subscription: &crate::WsSubscription, expected: crate::WsSubscriptionState) { let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(2); loop { @@ -380,6 +421,157 @@ fn helius_transaction_filter_debug_omits_signature_and_account_values() { assert!(!debug.contains("Vote111111111111111111111111111111111111111")); } +#[test] +fn helius_transaction_notification_debug_omits_raw_provider_payloads() { + let full_value = serde_json::json!({ + "transaction":{"raw":"MASSIVE-RAW-PAYLOAD-CANARY"}, + "signature":"FULL-SIGNATURE-CANARY", + "slot":77, + "transactionIndex":3 + }); + let full = super::decode_helius_transaction_notification(full_value).expect("full Helius notification must decode"); + let signature_value = serde_json::json!({ + "signature":"SIGNATURE-MODE-CANARY", + "slot":78, + "transactionIndex":4, + "err":{"secret":"ERROR-DATA-CANARY"}, + "memo":"MEMO-CANARY", + "blockTime":123, + "confirmationStatus":"CONFIRMATION-CANARY" + }); + let signature = super::decode_helius_transaction_notification(signature_value).expect("signature Helius notification must decode"); + let unknown = super::decode_helius_transaction_notification(serde_json::json!({"provider":"UNKNOWN-PAYLOAD-CANARY"})) + .expect("unknown Helius notification must remain forward-compatible"); + let rendered = format!("{full:?} {signature:?} {unknown:?}"); + assert!(rendered.contains("transaction: \"\"")); + assert!(rendered.contains("signature: \"\"")); + assert!(rendered.contains("err: \"value\"")); + assert!(rendered.contains("memo: \"value\"")); + assert!(rendered.contains("Unknown(\"\")")); + for forbidden in [ + "MASSIVE-RAW-PAYLOAD-CANARY", + "FULL-SIGNATURE-CANARY", + "SIGNATURE-MODE-CANARY", + "ERROR-DATA-CANARY", + "MEMO-CANARY", + "CONFIRMATION-CANARY", + "UNKNOWN-PAYLOAD-CANARY", + ] { + assert!(!rendered.contains(forbidden)); + } +} + +#[tokio::test(flavor = "current_thread")] +async fn helius_provider_rpc_application_error_is_safe_and_does_not_fail_session() { + let (listener, url) = bind_local_listener().await; + let server = tokio::spawn(async move { + let (stream, _) = listener.accept().await.expect("local server must accept Helius client"); + let mut websocket = tokio_tungstenite::accept_async(stream).await.expect("local Helius handshake must succeed"); + let transaction_subscribe = read_request(&mut websocket).await; + assert_eq!(transaction_subscribe["method"], serde_json::json!("transactionSubscribe")); + send_error( + &mut websocket, + &transaction_subscribe, + -32602, + "PROVIDER-MESSAGE-SECRET-CANARY", + serde_json::json!({"apiKey":"PROVIDER-ERROR-SECRET-CANARY","payload":"X".repeat(4096)}), + ) + .await; + let root_subscribe = read_request(&mut websocket).await; + assert_eq!(root_subscribe["method"], serde_json::json!("rootSubscribe")); + send_result(&mut websocket, &root_subscribe, serde_json::json!(72)).await; + send_root_notification(&mut websocket, 72, 88).await; + let root_unsubscribe = read_request(&mut websocket).await; + assert_eq!(root_unsubscribe["method"], serde_json::json!("rootUnsubscribe")); + assert_eq!(root_unsubscribe["params"], serde_json::json!([72])); + send_result(&mut websocket, &root_unsubscribe, serde_json::json!(true)).await; + wait_for_close_frame(&mut websocket).await; + }); + let endpoint_url = format!("{url}/?api-key=HELIUS-ENDPOINT-SECRET-CANARY"); + let session = crate::HeliusLaserStreamWsSession::connect(helius_endpoint(endpoint_url.as_str())).await.expect("Helius facade must connect"); + let request = crate::HeliusTransactionSubscribeRequest::new(base_filter(), std::option::Option::None); + let error = session.transaction_subscribe(&request).await.expect_err("provider application error must reject only the logical subscribe request"); + assert_eq!(error.code(), crate::ERROR_CODE_RPC_APPLICATION_ERROR); + assert!(error.context().iter().any(|entry| return entry.key() == "rpc_code" && entry.value() == "-32602")); + assert!(error.context().iter().any(|entry| return entry.key() == "method" && entry.value() == "transactionSubscribe")); + let rendered = format!("{error:?} {error} {session:?} {:?}", session.snapshot()); + for forbidden in ["PROVIDER-MESSAGE-SECRET-CANARY", "PROVIDER-ERROR-SECRET-CANARY", "HELIUS-ENDPOINT-SECRET-CANARY", "fixture-signature-secret-canary"] { + assert!(!rendered.contains(forbidden)); + } + assert_eq!(session.state(), crate::WsSessionState::Active); + wait_for_session_subscription_count(&session, 0).await; + let mut root = session.root_subscribe().await.expect("session must accept a healthy subscription after provider application error"); + assert_eq!(root.recv().await.expect("healthy root notification must arrive").expect("healthy root notification must decode"), 88); + assert!(root.unsubscribe().await.expect("healthy root unsubscribe must complete")); + session.close().await.expect("Helius fixture session must close"); + server.await.expect("provider error fixture server must finish"); +} + +#[tokio::test(flavor = "current_thread")] +async fn helius_notification_method_mismatch_fails_only_transaction_subscription() { + let (listener, url) = bind_local_listener().await; + let server = tokio::spawn(async move { + let (stream, _) = listener.accept().await.expect("local server must accept Helius client"); + let mut websocket = tokio_tungstenite::accept_async(stream).await.expect("local Helius handshake must succeed"); + let transaction_subscribe = read_request(&mut websocket).await; + send_result(&mut websocket, &transaction_subscribe, serde_json::json!(41)).await; + let root_subscribe = read_request(&mut websocket).await; + send_result(&mut websocket, &root_subscribe, serde_json::json!(42)).await; + send_root_notification(&mut websocket, 41, 5).await; + let cleanup = read_request(&mut websocket).await; + assert_eq!(cleanup["method"], serde_json::json!("transactionUnsubscribe")); + assert_eq!(cleanup["params"], serde_json::json!([41])); + send_result(&mut websocket, &cleanup, serde_json::json!(true)).await; + send_root_notification(&mut websocket, 42, 99).await; + wait_for_close_frame(&mut websocket).await; + }); + let session = crate::HeliusLaserStreamWsSession::connect(helius_endpoint(url.as_str())).await.expect("Helius facade must connect"); + let request = crate::HeliusTransactionSubscribeRequest::new(crate::HeliusTransactionSubscribeFilter::default(), std::option::Option::None); + let mut transaction = session.transaction_subscribe(&request).await.expect("transaction subscription must register"); + let mut root = session.root_subscribe().await.expect("root subscription must register"); + wait_for_subscription_state(&transaction, crate::WsSubscriptionState::Failed).await; + assert_eq!(transaction.terminal_error_code(), std::option::Option::Some(crate::ERROR_CODE_WS_PROTOCOL_ERROR)); + assert!(transaction.recv().await.is_none()); + assert_eq!(session.state(), crate::WsSessionState::Active); + wait_for_session_subscription_count(&session, 1).await; + assert_eq!(root.recv().await.expect("healthy root notification must arrive").expect("healthy root notification must decode"), 99); + assert_eq!(root.state(), crate::WsSubscriptionState::Active); + session.close().await.expect("Helius fixture session must close"); + server.await.expect("notification mismatch fixture server must finish"); +} + +#[tokio::test(flavor = "current_thread")] +async fn helius_oversized_inbound_payload_reconnects_before_provider_json_decode() { + let (listener, url) = bind_local_listener().await; + let server = tokio::spawn(async move { + let (first_stream, _) = listener.accept().await.expect("initial Helius client must connect"); + let mut first = tokio_tungstenite::accept_async(first_stream).await.expect("initial Helius handshake must succeed"); + first + .send(tokio_tungstenite::tungstenite::Message::Text("PROVIDER-PAYLOAD-CANARY".repeat(32).into())) + .await + .expect("oversized provider fixture payload must send"); + let (replacement_stream, _) = listener.accept().await.expect("replacement Helius client must connect"); + let mut replacement = tokio_tungstenite::accept_async(replacement_stream).await.expect("replacement Helius handshake must succeed"); + let root_subscribe = read_request(&mut replacement).await; + assert_eq!(root_subscribe["method"], serde_json::json!("rootSubscribe")); + send_result(&mut replacement, &root_subscribe, serde_json::json!(91)).await; + let root_unsubscribe = read_request(&mut replacement).await; + assert_eq!(root_unsubscribe["method"], serde_json::json!("rootUnsubscribe")); + send_result(&mut replacement, &root_unsubscribe, serde_json::json!(true)).await; + wait_for_close_frame(&mut replacement).await; + }); + let session = crate::HeliusLaserStreamWsSession::connect(helius_endpoint_with_session(url.as_str(), adversarial_payload_session_settings())) + .await + .expect("Helius facade must connect before adversarial payload"); + wait_for_gap_count(&session, 1).await; + assert_eq!(session.state(), crate::WsSessionState::Active); + assert_eq!(session.snapshot().continuity_gap_count(), 1); + let mut root = session.root_subscribe().await.expect("recovered Helius session must remain usable"); + assert!(root.unsubscribe().await.expect("recovered root subscription must unsubscribe")); + session.close().await.expect("recovered Helius session must close"); + server.await.expect("oversized provider payload fixture server must finish"); +} + #[tokio::test(flavor = "current_thread")] async fn helius_transaction_live_handle_decodes_notification_and_unsubscribes_through_shared_actor() { let (listener, url) = bind_local_listener().await; diff --git a/deltas/0.2.8/pre.008.md b/deltas/0.2.8/pre.008.md new file mode 100644 index 0000000..ce385a7 --- /dev/null +++ b/deltas/0.2.8/pre.008.md @@ -0,0 +1,238 @@ + + + +# Delta `0.2.8-pre.008` — adversarial Helius provider + limites + diagnostics sûrs + +## 1. Base et objet + +Base appliquée : + +```text +0.2.8-pre.7.fix.4 +``` + +Le checkpoint opérateur final de `pre.007` est intégralement vert : + +```text +cargo fmt --all OK +python3 scripts/audit_rust_workspace_rules.py clean +cargo check --workspace OK +cargo clippy --workspace --all-targets OK +Transport unit 331/331 +Transport public API 40/40 +Transport release completeness 29/29 +Transport doctests compile-fail 4/4 +cargo test --workspace OK +``` + +`pre.008` durcit la surface Helius WebSocket contre des comportements provider adversariaux sans créer de nouveau runtime. La tranche réutilise l'actor physique, les limites frame/message, le reconnect, le registry et la backpressure existants. + +## 2. Version technique + +```text +workspace.package.version = 0.2.8-pre.8 +commit attendu = v0.2.8-pre.008 +Git tag = aucun tag prerelease +``` + +Le header root `Cargo.toml` passe en version `232`. + +## 3. Ré-audit Helius courant + +La documentation Helius réauditée le 2026-08-23 confirme : + +```text +transactionSubscribe supporté +transactionUnsubscribe supporté +accountInclude/accountExclude/accountRequired 50 000 adresses max par liste +blockSubscribe non supporté Helius +slotsUpdatesSubscribe non supporté Helius +voteSubscribe non supporté Helius +endpoints WSS unifiés mainnet/devnet +``` + +La tranche n'introduit donc aucune nouvelle famille provider. + +## 4. `Debug` sûr pour les notifications Helius + +Le prompt `0.2.8` interdit les payloads arbitraires/massifs dans les diagnostics génériques. Avant cette tranche, les types de notification Helius dérivaient `Debug`, ce qui pouvait rendre : + +```text +transaction JSON brut +signature complète +memo/error provider +fallback Unknown(serde_json::Value) brut +``` + +`pre.008` remplace ces dérivations par des implémentations explicites : + +```text +HeliusFullTransactionNotification + transaction + signature + slot visible + transaction_index visible + +HeliusTransactionSignatureNotification + signature + slot/index visibles + err omitted/null/value seulement + memo omitted/null/value seulement + block_time omitted/null/value seulement + confirmation_status omitted/null/value seulement + +HeliusTransactionNotification::Unknown + payload Value conservé fonctionnellement + Debug = Unknown("") +``` + +Aucun accessor fonctionnel n'est supprimé. La donnée reste accessible explicitement au consommateur qui la demande ; seul le chemin diagnostic implicite est redacted. + +## 5. Capability guard inverse + +La façade Helius avait déjà quatre doctests compile-fail : + +```text +Helius -X-> blockSubscribe +Helius -X-> slotsUpdatesSubscribe +Helius -X-> voteSubscribe +Helius -X-> into_inner +``` + +`pre.008` ajoute le canari inverse : + +```text +SolanaStandardWsSession -X-> transactionSubscribe +``` + +La séparation standard/provider est donc prouvée dans les deux sens sans exposer le `WsSession` générique. + +## 6. Provider RPC application error + +Un serveur local adversarial retourne une erreur JSON-RPC `transactionSubscribe` avec : + +```text +code provider +message canary sensible +payload data arbitraire de plusieurs KiB +``` + +Le canari exige : + +```text +ERROR_CODE_RPC_APPLICATION_ERROR +context sûr = rpc_code + method +message/data provider non copiés dans KspError +api-key URL non rendue +filter signature non rendue +session physique reste Active +subscription_count revient à 0 +rootSubscribe fonctionne ensuite normalement +``` + +Une erreur d'application provider ne doit donc pas être promue en panne de transport. + +## 7. Notification method mismatch + +Un remote ID enregistré comme `HeliusTransaction` reçoit volontairement un `rootNotification`. + +Résultat attendu : + +```text +logical transaction subscription -> Failed / WS_PROTOCOL_ERROR +remote binding transaction -> cleanup transactionUnsubscribe +session physique -> Active +subscription root saine -> Active et notification reçue +``` + +Le mismatch de famille ne doit pas contaminer les autres logical subscriptions. + +## 8. Payload entrant oversized + +Un endpoint Helius local envoie une frame texte supérieure aux limites configurées : + +```text +max_message_size_bytes = 256 +max_frame_size_bytes = 128 +``` + +Le canari prouve que la limite tungstenite/actor s'applique avant tout décodage JSON provider, déclenche le reconnect borné existant et laisse la connexion Helius de remplacement utilisable pour un `rootSubscribe`/unsubscribe normal. + +Aucun nouveau compteur ou chemin de reconnect n'est ajouté. + +## 9. Backpressure et lifecycle conservés + +La preuve provider-specific de `pre.006` reste autoritaire pour : + +```text +queue capacity = 1 +transaction notification overflow +échec de la seule transaction subscription +cleanup transactionUnsubscribe +root subscription saine non affectée +session physique Active +``` + +`pre.008` ne duplique pas ce scénario. Les canaris partagés hérités continuent aussi de couvrir unsubscribe pendant reconnect, decode failure isolé, pending bounds et shutdown pendant backoff. + +## 10. Canaris ajoutés + +Quatre unit tests Helius sont ajoutés : + +```text +notification Debug redaction Full/Signature/Unknown +provider RPC application error sûre + session réutilisable +notification method mismatch isolé + cleanup transactionUnsubscribe +oversized inbound provider payload + reconnect + session réutilisable +``` + +Un release-completeness canary supplémentaire vérifie les guards, les `Debug` explicites et la réutilisation des mécanismes de bornage de l'actor. + +Un doctest compile-fail supplémentaire interdit `transactionSubscribe` sur `SolanaStandardWsSession`. + +Comptages attendus : + +```text +Transport unit 335 +Transport public API 40 +release completeness 30 +doctests compile-fail 5 +``` + +## 11. Fichiers modifiés + +```text +Cargo.toml +crates/ksp-onchain-transport-lib/src/ws_helius_transactions.rs +crates/ksp-onchain-transport-lib/src/ws_protocol_session.rs +crates/ksp-onchain-transport-lib/unit_tests/ws_helius_transactions.rs +crates/ksp-onchain-transport-lib/tests/release_completeness.rs +docs/plans/015-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET_PLAN.md +docs/validation/011-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET.md +deltas/0.2.8/pre.008.md +``` + +## 12. Hors scope + +Restent hors de `pre.008` : + +```text +compliance finale Helius + standard/HTTP pre.009 +smoke Helius live + cargo tree final pre.010 +fermeture docs/indexes + prompt 0.2.9 pre.011 +LaserStream gRPC futur backend séparé +Gatekeeper/preconf WebSocket hors scope 0.2.8 +``` + +## 13. Gate opérateur + +```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 --workspace +``` + +Critère de fermeture : zéro warning, audit clean, **335 unit / 40 public API / 30 completeness / 5 doctests** et workspace vert. diff --git a/docs/plans/015-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET_PLAN.md b/docs/plans/015-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET_PLAN.md index cc46b78..b6ad5df 100644 --- a/docs/plans/015-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET_PLAN.md +++ b/docs/plans/015-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET_PLAN.md @@ -1,9 +1,9 @@ - + # Plan `0.2.8` — Helius LaserStream WebSocket -> **Statut : `0.2.8-pre.007-fix.003` ferme enfin le heartbeat Transport à 331/331, avec fmt/audit/check/Clippy verts. Le workspace révèle toutefois un canari de dépendances devenu obsolète : `ksp-core-lib/tests/workspace_dependencies.rs` attend encore l’ancienne déclaration Tokio dev `net, rt` alors que `pre.007` a ajouté `io-util, test-util` pour les tests d’horloge. `0.2.8-pre.007-fix.004` aligne ce canari workspace sans toucher au runtime heartbeat.** +> **Statut : `0.2.8-pre.007-fix.004` est validé intégralement par l’opérateur : fmt/audit/check/Clippy, 331 unit Transport, 40 API, 29 completeness, 4 doctests et workspace complet verts. `0.2.8-pre.008` est préparé pour les guards adversariaux provider, limites/payload, isolation de session et redaction des diagnostics Helius.** ## 1. Objet, base et état courant @@ -34,14 +34,15 @@ pre.005 contrat typed transactionSubscribe/unsubscribe validé pre.005-fix.001 correction types de canaris + visibilité/tests/règles validée pre.005-fix.002 suppression warnings dead_code via cfg(test) validée pre.006 transactionNotification + lifecycle actor validé -pre.007 heartbeat Helius WebSocket / idle — checkpoint gate à corriger -pre.007-fix.001 lint + close canary corrigés ; un canari périodique reste non déterministe -pre.007-fix.002 observation yield-only insuffisante — checkpoint 330/331 -pre.007-fix.003 heartbeat Transport fermé ; workspace bloqué par canari Tokio dev obsolète -pre.007-fix.004 alignement canari dependency-firewall Tokio dev — préparé +pre.007 heartbeat Helius WebSocket / idle validé +pre.007-fix.001 lint + close canary corrigés +pre.007-fix.002 observation yield-only insuffisante — checkpoint historique +pre.007-fix.003 heartbeat Transport fermé +pre.007-fix.004 alignement canari dependency-firewall Tokio dev validé +pre.008 adversarial provider/capabilities/payload/security — préparé -workspace.package.version courant = 0.2.8-pre.7.fix.4 -commit attendu = v0.2.8-pre.007-fix.004 +workspace.package.version courant = 0.2.8-pre.8 +commit attendu = v0.2.8-pre.008 aucun tag prerelease ``` @@ -71,12 +72,12 @@ pre.005 DONE — transactionSubscribe request typed + filters/options/tokenAcco fix.002 DONE — helpers wire non consommés en production bornés à #[cfg(test)] ; zéro warning dead_code pre.006 DONE — transactionNotification + handle live + actor registry/reconnect/resubscribe/unsubscribe races + late notifications + backpressure ciblée, sans second actor/socket -pre.007 FIX REQUIRED — heartbeat Helius WebSocket/idle + timers + interaction reconnect/control frames/shutdown - fix.001 CHECKPOINT — lint et close canary corrigés ; 330/331 unit - fix.002 CHECKPOINT — observation yield-only insuffisante ; 330/331 unit - fix.003 CHECKPOINT — heartbeat Transport fermé à 331/331 ; workspace échoue sur canari Tokio dev obsolète - fix.004 PREPARED — alignement exact du dependency-firewall sur `io-util, net, rt, test-util` -pre.008 provider adversarial lifecycle + capability guards + payload/backpressure + security/redaction +pre.007 DONE — heartbeat Helius WebSocket/idle + timers + interaction reconnect/control frames/shutdown + fix.001 DONE — lint et close canary corrigés + fix.002 DONE — diagnostic de propagation scheduler établi + fix.003 DONE — canari heartbeat périodique stabilisé sans changer le runtime + fix.004 DONE — dependency-firewall aligné sur `io-util, net, rt, test-util` +pre.008 PREPARED — provider adversarial lifecycle + capability guards + payload/backpressure + security/redaction pre.009 compliance Helius WebSocket + non-régressions Solana standard 18/18 + HTTP 52/14 + Config/API/dependency-firewall canaries pre.010 smoke Helius WebSocket live opt-in si stratégie sûre + README/USAGE @@ -1314,3 +1315,84 @@ transport_manifest_preserves_ksp_dependency_firewall `pre.008` reste bloqué jusqu'au replay workspace vert de `pre.007-fix.004`. +## 20. Fermeture `pre.007` et préparation `pre.008` + +Checkpoint opérateur `0.2.8-pre.7.fix.4` reçu le 2026-08-23 : + +```text +[x] cargo fmt --all +[x] python3 scripts/audit_rust_workspace_rules.py = clean / 0 candidate +[x] cargo check --workspace = vert, sans warning +[x] cargo clippy --workspace --all-targets = vert, sans warning +[x] cargo test -p ksp-onchain-transport-lib = 331 unit + 40 API + 29 completeness + 4 doctests +[x] cargo test --workspace = vert +``` + +La tranche heartbeat et ses quatre fixes sont donc fermés. `pre.008` ne rouvre ni cadence, ni Config, ni lifecycle transaction nominal. + +Ré-audit Helius du 2026-08-23 : + +```text +transactionSubscribe supporté sur endpoints WSS unifiés mainnet/devnet +accountInclude/accountExclude/accountRequired 50 000 adresses max chacune +transactionUnsubscribe ID distant numérique + bool de réponse +blockSubscribe non supporté Helius +slotsUpdatesSubscribe non supporté Helius +voteSubscribe non supporté Helius +``` + +Le runtime partagé possède déjà les mécanismes de bornage : `max_message_size_bytes`, `max_frame_size_bytes`, queues de notifications bornées, reconnect fini, remapping remote/local et isolation de logical subscription. Le travail `pre.008` porte donc principalement sur des canaris adversariaux Helius et sur un défaut de diagnostic identifié pendant la revue : les notifications Helius publiques dérivaient encore `Debug` sur leur payload brut. + +Décisions `pre.008` : + +```text +1. HeliusFullTransactionNotification::Debug + - transaction brute omise + - signature omise + - slot/index seulement conservés comme métadonnées sûres + +2. HeliusTransactionSignatureNotification::Debug + - signature/memo/err/confirmation value omis + - états omitted/null/value seulement pour les champs wire optionnels + +3. HeliusTransactionNotification::Unknown + - payload serde_json::Value toujours accessible via la variante + - Debug rend uniquement + +4. provider JSON-RPC application error + - ERROR_CODE_RPC_APPLICATION_ERROR + - rpc_code + method sûrs seulement + - message/data arbitraires provider absents du KspError + - session physique reste Active et accepte une subscription saine ensuite + +5. notification method mismatch + - remote ID Helius lié à transactionNotification recevant une autre méthode + - seule la logical transaction subscription échoue + - cleanup transactionUnsubscribe best-effort + - autre subscription saine et session physique restent actives + +6. oversized provider payload + - max frame/message partagé appliqué avant JSON provider + - reconnect borné du même actor + - connexion de remplacement reste utilisable + +7. capability inverse + - Helius garde compile-fail block/slotsUpdates/vote + - SolanaStandard ajoute compile-fail transactionSubscribe +``` + +Aucune nouvelle dépendance, aucun nouveau socket/actor, aucune extension Config, aucune lecture d'environnement Transport. Les garanties `pre.006` de queue overflow transaction + root restent la preuve provider-specific de backpressure ciblée ; `pre.008` ne duplique pas ce test inutilement. + +Gate attendu après application : + +```text +Transport unit 335 +Transport public API 40 +release completeness 30 +Transport doctests 5 +workspace vert +warnings 0 +``` + +`pre.009` reste bloqué jusqu'au replay intégralement vert de `pre.008`. + diff --git a/docs/validation/011-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET.md b/docs/validation/011-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET.md index 6e047b0..d54a7dc 100644 --- a/docs/validation/011-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET.md +++ b/docs/validation/011-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET.md @@ -1,5 +1,5 @@ - + # Validation `0.2.8` — Helius LaserStream WebSocket @@ -838,3 +838,59 @@ Critères `pre.007-fix.004` : Verdict courant : **`pre.007` FIX REQUIRED ; `fix.001` CHECKPOINT ; `fix.002` CHECKPOINT ; `fix.003` CHECKPOINT Transport vert ; `fix.004` PREPARED ; `pre.008` bloqué.** +## 22. Fermeture `pre.007-fix.004` et gate `pre.008` + +Résultat opérateur final `0.2.8-pre.7.fix.4` : + +```text +[x] cargo fmt --all +[x] audit Rust workspace = clean / 0 candidate +[x] cargo check --workspace = vert, sans warning +[x] cargo clippy --workspace --all-targets = vert, sans warning +[x] Transport unit = 331/331 +[x] Transport public API = 40/40 +[x] release completeness = 29/29 +[x] Transport doctests = 4/4 +[x] cargo test --workspace = vert +``` + +Verdict : **`pre.007` DONE avec `fix.001..004` DONE. `pre.008` peut être ouvert.** + +Ré-audit provider courant : Helius documente toujours `transactionSubscribe` sur les endpoints WSS unifiés mainnet/devnet, les trois listes d'adresses à 50 000 max et l'absence de support de `blockSubscribe`, `slotsUpdatesSubscribe` et `voteSubscribe`. + +Critères `pre.008` préparés : + +```text +[ ] workspace.package.version = 0.2.8-pre.8 +[ ] aucune nouvelle dépendance +[ ] aucun second actor/socket/provider registry +[ ] SolanaStandard ne peut pas appeler transactionSubscribe (compile-fail) +[ ] Helius ne peut toujours pas appeler block/slotsUpdates/vote (compile-fail) +[ ] provider RPC application error ne tue pas la session physique +[ ] provider message/data arbitraires ne sont pas copiés dans KspError +[ ] mismatch notification method échoue seulement la logical transaction subscription +[ ] cleanup du mismatch utilise transactionUnsubscribe +[ ] autre subscription saine survit au mismatch +[ ] oversized inbound provider payload déclenche le reconnect borné avant JSON decode métier +[ ] session Helius de remplacement reste utilisable après oversized payload +[ ] queue overflow transaction reste isolée (preuve pre.006 conservée) +[ ] Debug Full notification omet transaction et signature brutes +[ ] Debug Signature notification omet signature/memo/error values +[ ] Debug Unknown notification omet le serde_json::Value brut +[ ] endpoint api-key/filter values restent absents session/request/snapshot diagnostics +[ ] remote subscription id reste absent des snapshots publics +[ ] standard WebSocket 18/18 inchangé +[ ] HTTP 52 current + 14 historical inchangé +[ ] Config V1/V2 et helius_laserstream inchangés +[ ] cargo fmt --all +[ ] audit Rust = clean +[ ] cargo check = sans warning +[ ] cargo clippy = sans warning +[ ] Transport = 335 unit + 40 API + 30 completeness + 5 doctests +[ ] cargo test --workspace = vert +``` + +La correction `Debug` ne retire aucune donnée de l'API fonctionnelle : `transaction()`, `signature()`, les accessors wire et la variante `Unknown(Value)` restent disponibles explicitement. Elle empêche seulement les diagnostics génériques de rendre un payload provider arbitraire ou massif. + +Verdict courant : **`pre.008` PREPARED ; `pre.009` bloqué jusqu'au gate opérateur.** +