From d98ee736bee90feb1bf0be56867482135b27d7f4 Mon Sep 17 00:00:00 2001 From: SinuS Von SifriduS Date: Mon, 24 Aug 2026 22:48:33 +0200 Subject: [PATCH] v0.2.9-pre.013-fix.001 --- Cargo.toml | 4 +- config/std.transport.json | 51 +++++ crates/ksp-config-lib/unit_tests/transport.rs | 61 +++--- .../tests/yellowstone_publicnode_smoke.rs | 71 ++++--- deltas/0.2.9/pre.013-fix.001.md | 182 ++++++++++++++++++ 5 files changed, 317 insertions(+), 52 deletions(-) create mode 100644 deltas/0.2.9/pre.013-fix.001.md diff --git a/Cargo.toml b/Cargo.toml index 60fdaf5..111dc18 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ # file: Cargo.toml -# version: 254 +# version: 255 [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.13" +version = "0.2.9-pre.13.fix.1" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" diff --git a/config/std.transport.json b/config/std.transport.json index 27dec6d..e7b70e8 100644 --- a/config/std.transport.json +++ b/config/std.transport.json @@ -170,6 +170,57 @@ "url": "https://solana-yellowstone-grpc.publicnode.com:443" } ] + }, + { + "profile_id": "publicnode_testnet", + "endpoints": [ + { + "name": "solana_testnet_public", + "enabled": true, + "provider": "solana-public", + "cluster": "testnet", + "url": "https://api.testnet.solana.com", + "connect_timeout_ms": 5000, + "request_timeout_ms": 15000, + "max_idle_connections_per_host": 8, + "roles": [ + { + "role": "default", + "enabled": true, + "request_kinds": [ + "*" + ], + "priority": 100, + "limits": { + "requests_per_second": 5, + "burst_capacity": 10, + "max_concurrent_requests": 8, + "pause_after_rate_limit_ms": 1000 + } + } + ] + } + ], + "ws_endpoints": [ + { + "name": "solana_testnet_public_ws", + "enabled": true, + "provider": "solana-public", + "cluster": "testnet", + "kind": "solana_standard", + "url": "wss://api.testnet.solana.com" + } + ], + "grpc_endpoints": [ + { + "name": "publicnode_solana_testnet_yellowstone", + "enabled": true, + "provider": "publicnode", + "cluster": "testnet", + "protocol": "solana_yellowstone", + "url": "https://solana-testnet-yellowstone-grpc.publicnode.com:443" + } + ] } ] } diff --git a/crates/ksp-config-lib/unit_tests/transport.rs b/crates/ksp-config-lib/unit_tests/transport.rs index 4364958..ea1f3df 100644 --- a/crates/ksp-config-lib/unit_tests/transport.rs +++ b/crates/ksp-config-lib/unit_tests/transport.rs @@ -1,5 +1,5 @@ // file: crates/ksp-config-lib/unit_tests/transport.rs -// version: 7 +// version: 8 #[test] fn fixture_transport_profile_maps_complete_runtime_contract() { @@ -134,39 +134,44 @@ fn committed_transport_document_maps_default_and_explicit_profiles() { } #[test] -fn committed_v3_publicnode_mainnet_maps_provider_neutral_yellowstone_grpc() { +fn committed_v3_publicnode_profiles_map_provider_neutral_yellowstone_grpc_without_auth_metadata() { let engine = committed_engine(); let engine = match engine { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return, }; let environment = crate::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new()); - let resolved = engine.load_resolved_transport_config(std::option::Option::Some("publicnode_mainnet"), &environment); - assert!(resolved.is_ok(), "committed PublicNode Mainnet V3 profile should map: {resolved:?}"); - let resolved = match resolved { - std::result::Result::Ok(value) => value, - std::result::Result::Err(_) => return, - }; - assert_eq!(resolved.profile_id(), "publicnode_mainnet"); - assert!(resolved.ws_settings().is_some()); - let grpc = resolved.grpc_settings(); - assert!(grpc.is_some(), "PublicNode Mainnet profile should expose Yellowstone gRPC settings"); - if let std::option::Option::Some(grpc) = grpc { - assert_eq!(grpc.endpoints().len(), 1); - let endpoint = &grpc.endpoints()[0]; - assert_eq!(endpoint.name(), "publicnode_solana_mainnet_yellowstone"); - assert_eq!(endpoint.provider().as_str(), "publicnode"); - assert_eq!(endpoint.cluster().as_str(), "mainnet-beta"); - assert_eq!(endpoint.url().as_str(), "https://solana-yellowstone-grpc.publicnode.com:443"); - assert!(endpoint.url().uses_tls()); - assert!(endpoint.metadata().is_empty()); - assert_eq!(endpoint.session().connect_timeout(), std::time::Duration::from_millis(10_000)); - assert_eq!(endpoint.session().unary_timeout(), std::time::Duration::from_millis(10_000)); - assert_eq!(endpoint.session().close_timeout(), std::time::Duration::from_millis(5_000)); - assert_eq!(endpoint.session().reconnect().max_retries(), 5); - assert!(grpc.validate().is_ok(), "Config-produced Yellowstone gRPC settings should satisfy Transport validation"); - let debug = format!("{grpc:?}"); - assert!(!debug.contains("solana-yellowstone-grpc.publicnode.com"), "Transport settings Debug must not expose gRPC endpoint URLs"); + for (profile_id, endpoint_name, cluster, url) in [ + ("publicnode_mainnet", "publicnode_solana_mainnet_yellowstone", "mainnet-beta", "https://solana-yellowstone-grpc.publicnode.com:443"), + ("publicnode_testnet", "publicnode_solana_testnet_yellowstone", "testnet", "https://solana-testnet-yellowstone-grpc.publicnode.com:443"), + ] { + let resolved = engine.load_resolved_transport_config(std::option::Option::Some(profile_id), &environment); + assert!(resolved.is_ok(), "committed PublicNode V3 profile {profile_id} should map: {resolved:?}"); + let resolved = match resolved { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => continue, + }; + assert_eq!(resolved.profile_id(), profile_id); + assert!(resolved.ws_settings().is_some()); + let grpc = resolved.grpc_settings(); + assert!(grpc.is_some(), "PublicNode profile {profile_id} should expose Yellowstone gRPC settings"); + if let std::option::Option::Some(grpc) = grpc { + assert_eq!(grpc.endpoints().len(), 1); + let endpoint = &grpc.endpoints()[0]; + assert_eq!(endpoint.name(), endpoint_name); + assert_eq!(endpoint.provider().as_str(), "publicnode"); + assert_eq!(endpoint.cluster().as_str(), cluster); + assert_eq!(endpoint.url().as_str(), url); + assert!(endpoint.url().uses_tls()); + assert!(endpoint.metadata().is_empty(), "PublicNode public Yellowstone profiles must not invent authentication metadata"); + assert_eq!(endpoint.session().connect_timeout(), std::time::Duration::from_millis(10_000)); + assert_eq!(endpoint.session().unary_timeout(), std::time::Duration::from_millis(10_000)); + assert_eq!(endpoint.session().close_timeout(), std::time::Duration::from_millis(5_000)); + assert_eq!(endpoint.session().reconnect().max_retries(), 5); + assert!(grpc.validate().is_ok(), "Config-produced Yellowstone gRPC settings should satisfy Transport validation"); + let debug = format!("{grpc:?}"); + assert!(!debug.contains("publicnode.com"), "Transport settings Debug must not expose gRPC endpoint URLs"); + } } } diff --git a/crates/ksp-onchain-transport-lib/tests/yellowstone_publicnode_smoke.rs b/crates/ksp-onchain-transport-lib/tests/yellowstone_publicnode_smoke.rs index 441918e..20f117b 100644 --- a/crates/ksp-onchain-transport-lib/tests/yellowstone_publicnode_smoke.rs +++ b/crates/ksp-onchain-transport-lib/tests/yellowstone_publicnode_smoke.rs @@ -1,10 +1,10 @@ // file: crates/ksp-onchain-transport-lib/tests/yellowstone_publicnode_smoke.rs -// version: 1 +// version: 2 -//! Opt-in live PublicNode Mainnet smoke for the provider-neutral Yellowstone gRPC facade. +//! Opt-in live PublicNode Mainnet/Testnet smokes for the provider-neutral Yellowstone gRPC Subscribe facade. -fn publicnode_mainnet_endpoint() -> ksp_core_lib::Result { - let url = match ksp_onchain_transport_lib::YellowstoneGrpcEndpointUrl::parse("https://solana-yellowstone-grpc.publicnode.com:443") { +fn publicnode_endpoint(name: &str, cluster: &str, url: &str) -> ksp_core_lib::Result { + let url = match ksp_onchain_transport_lib::YellowstoneGrpcEndpointUrl::parse(url) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; @@ -19,29 +19,56 @@ fn publicnode_mainnet_endpoint() -> ksp_core_lib::Result 0); +fn slot_request() -> ksp_core_lib::Result { + let mut request = ksp_onchain_transport_lib::YellowstoneSubscribeRequest::new(); + let name = match ksp_onchain_transport_lib::YellowstoneSubscribeFilterName::new("slots") { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + request.insert_slot_filter(name, ksp_onchain_transport_lib::YellowstoneSubscribeSlotFilter::new())?; + return std::result::Result::Ok(request); +} + +async fn assert_publicnode_slot_stream(name: &str, cluster: &str, url: &str) { + let endpoint = publicnode_endpoint(name, cluster, url).expect("programmatic PublicNode Yellowstone settings must construct an unauthenticated endpoint"); + let channel = ksp_onchain_transport_lib::YellowstoneGrpcChannel::connect(&endpoint).await.expect("PublicNode Yellowstone TLS connection must succeed"); + assert_eq!(channel.endpoint_name(), name); + assert_eq!(channel.provider().as_str(), "publicnode"); + assert_eq!(channel.cluster().as_str(), cluster); + let request = slot_request().expect("PublicNode Yellowstone slot request must be valid"); + let mut session = channel.open_standard_subscribe(request).await.expect("PublicNode Yellowstone Subscribe must open without authentication metadata"); + let slot = tokio::time::timeout(std::time::Duration::from_secs(20), async { + loop { + match session.next_update().await.expect("PublicNode Yellowstone Subscribe update must decode") { + std::option::Option::Some(ksp_onchain_transport_lib::YellowstoneSubscribeUpdate::Slot(update)) => break update.slot(), + std::option::Option::Some(_) => {}, + std::option::Option::None => panic!("PublicNode Yellowstone Subscribe ended before a slot update"), + } + } + }) + .await + .expect("PublicNode Yellowstone Subscribe must publish a slot update before the smoke deadline"); + assert!(slot > 0); + session.close().await.expect("PublicNode Yellowstone Subscribe must close cleanly"); +} + +#[tokio::test(flavor = "current_thread")] +#[ignore = "opt-in live PublicNode Mainnet Yellowstone gRPC smoke; performs an unauthenticated external TLS/Subscribe request"] +async fn publicnode_mainnet_yellowstone_streams_slots_without_authentication_metadata() { + assert_publicnode_slot_stream("publicnode_mainnet_yellowstone", "mainnet-beta", "https://solana-yellowstone-grpc.publicnode.com:443").await; +} + +#[tokio::test(flavor = "current_thread")] +#[ignore = "opt-in live PublicNode Testnet Yellowstone gRPC smoke; performs an unauthenticated external TLS/Subscribe request"] +async fn publicnode_testnet_yellowstone_streams_slots_without_authentication_metadata() { + assert_publicnode_slot_stream("publicnode_testnet_yellowstone", "testnet", "https://solana-testnet-yellowstone-grpc.publicnode.com:443").await; } diff --git a/deltas/0.2.9/pre.013-fix.001.md b/deltas/0.2.9/pre.013-fix.001.md new file mode 100644 index 0000000..97f9677 --- /dev/null +++ b/deltas/0.2.9/pre.013-fix.001.md @@ -0,0 +1,182 @@ + + + +# Delta `0.2.9-pre.013-fix.001` — smoke PublicNode streaming + Testnet + +## 1. Base + +```text +livraison : 0.2.9-pre.013 +Cargo : 0.2.9-pre.13 +``` + +Le gate déterministe opérateur de `pre.013` est vert jusqu'au smoke live : + +```text +cargo fmt --all PASS +Rust workspace audit clean / 0 export candidate +Markdown table audit clean, 87 tableaux / 254 fichiers +cargo check --workspace PASS +cargo clippy --workspace --all-targets PASS sans warning +Transport unit 383/383 +Transport public API 49/49 +Transport release completeness 43/43 +Transport doctests 4/4 +workspace dependency canary 3/3 +``` + +Le smoke initial atteint le service PublicNode Mainnet mais `GetVersion` retourne `PERMISSION_DENIED`. + +## 2. Correction du diagnostic + +Cette réponse ne suffit pas à conclure qu'un `x-token` PublicNode est requis. + +La surface Yellowstone upstream permet à un opérateur de désactiver les méthodes unary indépendamment du streaming `Subscribe`. PublicNode expose son service comme gateway Yellowstone gRPC public et aucune procédure d'authentification n'est retenue comme contrat KSP tant qu'elle n'est pas établie par une source provider suffisamment forte. + +Conséquence : + +```text +aucun x-token inventé +aucun secret PublicNode ajouté à Config +aucune variable d'environnement d'authentification ajoutée +smoke recentré sur Subscribe, fonctionnalité centrale de 0.2.9 +``` + +## 3. Endpoint Testnet acquis + +L'opérateur a confirmé le host/port Yellowstone gRPC Testnet : + +```text +solana-testnet-yellowstone-grpc.publicnode.com:443 +``` + +KSP utilise l'URI TLS : + +```text +https://solana-testnet-yellowstone-grpc.publicnode.com:443 +``` + +## 4. Config V3 + +`config/std.transport.json` conserve le profil Mainnet PublicNode sans metadata d'authentification et ajoute : + +```text +profile_id = publicnode_testnet +provider = publicnode +cluster = testnet +protocol = solana_yellowstone +url = https://solana-testnet-yellowstone-grpc.publicnode.com:443 +metadata = none +``` + +Comme pour le profil Mainnet, les axes HTTP/WS du profil restent des transports Solana standards distincts du provider gRPC. + +Le canari Config existant couvre désormais les deux profils PublicNode et impose explicitement : + +```text +metadata gRPC vide +provider/cluster/URL exacts +validation Transport verte +Debug sans URL endpoint +``` + +## 5. Smoke Transport pur + +Le smoke opt-in ne teste plus les méthodes unary provider-dépendantes `GetVersion` / `GetSlot`. + +Il ouvre maintenant la surface standard réellement visée par la release : + +```text +TLS +Subscribe bidirectionnel +filtre slots standard +réception d'un YellowstoneSubscribeUpdate::Slot +slot > 0 +close borné +``` + +Deux cas live sont présents : + +```text +PublicNode Mainnet Yellowstone Subscribe sans metadata d'authentification +PublicNode Testnet Yellowstone Subscribe sans metadata d'authentification +``` + +Le test reste dans `ksp-onchain-transport-lib`, construit ses settings programmatiquement et ne dépend ni de Config ni d'un secret opérateur. + +## 6. Signal de version + +Le correctif modifie une config runtime et un test Rust : + +```text +workspace.package.version = 0.2.9-pre.13.fix.1 +commit attendu = v0.2.9-pre.013-fix.001 +``` + +## 7. Gate opérateur + +### 7.1 Déterministe + +```bash +cargo fmt --all +python3 scripts/audit_rust_workspace_rules.py +python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.2.9 +cargo check --workspace +cargo clippy --workspace --all-targets +cargo test -p ksp-config-lib +cargo test -p ksp-onchain-transport-lib +cargo test -p ksp-core-lib --test workspace_dependencies +``` + +Attendus minima : + +```text +Config unit 113 +Config public API 15 +Config ownership 5 +Transport unit 383 +Transport public API 49 +Transport release completeness 43 +Transport doctests 4 +workspace dependencies 3 +``` + +### 7.2 Smoke live + +```bash +cargo test -p ksp-onchain-transport-lib --test yellowstone_publicnode_smoke -- --ignored --nocapture +``` + +Attendu : + +```text +2 passed +0 failed +0 ignored +``` + +Aucun token ou secret ne doit être nécessaire pour ce smoke. Si `Subscribe` retourne à son tour `PERMISSION_DENIED`, l'hypothèse d'une authentification provider redevient ouverte et doit alors être établie avant tout nouveau correctif Config. + +### 7.3 Graphes et workspace + +```bash +cargo tree -p ksp-onchain-transport-lib +cargo tree -p ksp-onchain-transport-lib --duplicates +cargo tree --duplicates +cargo test --workspace +``` + +## 8. Frontières + +Ce fix reste strictement dans le couloir technique/live de `pre.013` : + +```text +aucun README/USAGE modifié +aucun plan/validation modifié +aucun CHANGELOG/ROADMAP modifié +aucun prompt modifié +aucun provider-specific engine ajouté +aucun secret committé +``` + +La prochaine tranche reste `0.2.9-pre.014` de réconciliation documentaire, uniquement après fermeture verte de ce fix.