diff --git a/.env.example b/.env.example index a848739..ee2b3c6 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,5 @@ # file: .env.example -# version: 6 +# version: 7 # KSP Logging root directory. Used by config/std.logging.json for relative log output paths. # The current Config document fallback is "logs" when neither the process environment nor .env defines this variable. @@ -34,6 +34,10 @@ KSP_PUBLIC_SOLANA_MAINNET_WS_URL=wss://api.mainnet-beta.solana.com # Keep provider credentials in a KSP_SECRET_* variable; do not copy a real credential-bearing URL into committed JSON. # KSP_SECRET_SOLANA_HTTP_URL=https://provider.example/?api-key=replace-me +# PublicNode personal token used as secret x-token metadata by the committed Yellowstone gRPC profiles. +# Obtain/manage it through the PublicNode / Allnodes token flow; keep the real value only in the process environment or local .env. +# KSP_SECRET_PUBLICNODE_GRPC_X_TOKEN=replace-me + # Helius API key used by the LaserStream WebSocket endpoint in config/examples/std.transport.example.json. # Keep the real credential only in the process environment or local .env; never commit it. # KSP_SECRET_HELIUS_API_KEY=replace-me diff --git a/Cargo.toml b/Cargo.toml index 111dc18..05f547c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ # file: Cargo.toml -# version: 255 +# version: 256 [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.fix.1" +version = "0.2.9-pre.13.fix.2" 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 e7b70e8..189c76f 100644 --- a/config/std.transport.json +++ b/config/std.transport.json @@ -167,7 +167,13 @@ "provider": "publicnode", "cluster": "mainnet-beta", "protocol": "solana_yellowstone", - "url": "https://solana-yellowstone-grpc.publicnode.com:443" + "url": "https://solana-yellowstone-grpc.publicnode.com:443", + "secret_metadata": [ + { + "key": "x-token", + "value": "${KSP_SECRET_PUBLICNODE_GRPC_X_TOKEN}" + } + ] } ] }, @@ -218,7 +224,13 @@ "provider": "publicnode", "cluster": "testnet", "protocol": "solana_yellowstone", - "url": "https://solana-testnet-yellowstone-grpc.publicnode.com:443" + "url": "https://solana-testnet-yellowstone-grpc.publicnode.com:443", + "secret_metadata": [ + { + "key": "x-token", + "value": "${KSP_SECRET_PUBLICNODE_GRPC_X_TOKEN}" + } + ] } ] } diff --git a/crates/ksp-config-lib/unit_tests/transport.rs b/crates/ksp-config-lib/unit_tests/transport.rs index ea1f3df..3d16b14 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: 8 +// version: 9 #[test] fn fixture_transport_profile_maps_complete_runtime_contract() { @@ -134,13 +134,16 @@ fn committed_transport_document_maps_default_and_explicit_profiles() { } #[test] -fn committed_v3_publicnode_profiles_map_provider_neutral_yellowstone_grpc_without_auth_metadata() { +fn committed_v3_publicnode_profiles_map_provider_neutral_yellowstone_grpc_with_secret_x_token() { 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 canary = "PUBLICNODE-GRPC-X-TOKEN-CANARY"; + let mut process = std::collections::BTreeMap::::new(); + process.insert("KSP_SECRET_PUBLICNODE_GRPC_X_TOKEN".to_owned(), canary.to_owned()); + let environment = crate::ConfigEnvironment::from_maps(process, std::collections::BTreeMap::new()); 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"), @@ -163,7 +166,11 @@ fn committed_v3_publicnode_profiles_map_provider_neutral_yellowstone_grpc_withou 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.metadata().len(), 1); + assert_eq!(endpoint.metadata()[0].key(), "x-token"); + assert!(endpoint.metadata()[0].is_secret()); + let endpoint_debug = format!("{endpoint:?}"); + assert!(!endpoint_debug.contains(canary), "PublicNode x-token must stay redacted from endpoint Debug"); 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)); 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 20f117b..9c04c5f 100644 --- a/crates/ksp-onchain-transport-lib/tests/yellowstone_publicnode_smoke.rs +++ b/crates/ksp-onchain-transport-lib/tests/yellowstone_publicnode_smoke.rs @@ -1,9 +1,34 @@ // file: crates/ksp-onchain-transport-lib/tests/yellowstone_publicnode_smoke.rs -// version: 2 +// version: 3 -//! Opt-in live PublicNode Mainnet/Testnet smokes for the provider-neutral Yellowstone gRPC Subscribe facade. +//! Opt-in live PublicNode Mainnet/Testnet smokes for authenticated provider-neutral Yellowstone gRPC Subscribe. -fn publicnode_endpoint(name: &str, cluster: &str, url: &str) -> ksp_core_lib::Result { +use std::io::IsTerminal; // rust-rules: trait-import + +static PUBLICNODE_X_TOKEN: std::sync::OnceLock = std::sync::OnceLock::new(); + +fn publicnode_x_token() -> &'static str { + return PUBLICNODE_X_TOKEN + .get_or_init(|| { + assert!( + !std::io::stdin().is_terminal(), + "pipe the PublicNode personal x-token to this ignored smoke on stdin; never pass it as a command-line argument" + ); + let mut token = std::string::String::new(); + std::io::stdin().read_line(&mut token).expect("PublicNode x-token must be readable from smoke stdin"); + let token = token.trim().to_owned(); + assert!(!token.is_empty(), "PublicNode x-token provided on smoke stdin must not be empty"); + return token; + }) + .as_str(); +} + +fn publicnode_endpoint( + name: &str, + cluster: &str, + url: &str, + x_token: &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), @@ -18,14 +43,16 @@ fn publicnode_endpoint(name: &str, cluster: &str, url: &str) -> ksp_core_lib::Re 16 * 1024 * 1024, 4 * 1024 * 1024, ); - return std::result::Result::Ok(ksp_onchain_transport_lib::YellowstoneGrpcEndpointSettings::new( + let endpoint = ksp_onchain_transport_lib::YellowstoneGrpcEndpointSettings::new( name, true, ksp_onchain_transport_lib::YellowstoneGrpcProviderName::new("publicnode"), ksp_onchain_transport_lib::YellowstoneGrpcClusterName::new(cluster), url, session, - )); + ); + let metadata = ksp_onchain_transport_lib::YellowstoneGrpcMetadataEntry::secret("x-token", x_token)?; + return endpoint.with_metadata(vec![metadata]); } fn slot_request() -> ksp_core_lib::Result { @@ -39,13 +66,16 @@ fn slot_request() -> ksp_core_lib::Result + + +# Delta `0.2.9-pre.013-fix.002` — authentification PublicNode Yellowstone + +## 1. Base + +```text +livraison : 0.2.9-pre.013-fix.001 +Cargo : 0.2.9-pre.13.fix.1 +``` + +Le second smoke opérateur invalide l'hypothèse retenue dans `fix.001` : + +```text +Mainnet SubscribeOpen -> PERMISSION_DENIED +Testnet SubscribeOpen -> PERMISSION_DENIED +``` + +Les deux endpoints sont donc atteignables au niveau TLS/gRPC mais refusent aussi la surface `Subscribe` sans autorisation. + +## 2. Diagnostic retenu + +Les éléments concordants sont désormais suffisants pour traiter PublicNode Yellowstone comme une surface à personal token : + +```text +provider = publicnode +auth wire = metadata ASCII x-token +secret = oui +URL = sans credential +``` + +La page PublicNode expose les endpoints Yellowstone Mainnet/Testnet. Une capture récente de la page PublicNode expose en outre un lien `Get token` vers le flow Allnodes `https://www.allnodes.com/publicnode`. Des implémentations Yellowstone récentes visant explicitement PublicNode rapportent le même comportement `PERMISSION_DENIED` sans personal token et utilisent `x-token`. + +Le provisioning effectif du token reste un gate externe/opérateur : KSP ne génère, ne devine et ne versionne aucun credential provider. + +## 3. Config V3 + +Les profils : + +```text +publicnode_mainnet +publicnode_testnet +``` + +conservent leurs URLs TLS sans secret et ajoutent chacun : + +```json +"secret_metadata": [ + { + "key": "x-token", + "value": "${KSP_SECRET_PUBLICNODE_GRPC_X_TOKEN}" + } +] +``` + +L'absence de `KSP_SECRET_PUBLICNODE_GRPC_X_TOKEN` rend donc volontairement le profil PublicNode explicite non résolvable au lieu d'envoyer silencieusement une requête anonyme vouée à `PERMISSION_DENIED`. + +`.env.example` inventorie la variable sans valeur réelle. + +## 4. Canari Config + +Le canari des profils PublicNode fournit un canary secret via `ConfigEnvironment`, vérifie : + +```text +Mainnet/Testnet exacts +metadata count = 1 +metadata key = x-token +metadata class = secret +Debug = sans canary +Transport validation = PASS +``` + +Les règles génériques V3 de provenance `secret_metadata` restent inchangées. + +## 5. Smoke Transport pur + +Le smoke reste dans `ksp-onchain-transport-lib` et ne dépend pas de Config. + +Pour ne pas violer la frontière `Transport -X-> process environment` et pour ne pas exposer le token dans les arguments/process list, le harness lit une seule ligne secrète depuis son `stdin`. Le même personal token est réutilisé par les deux tests Mainnet/Testnet via un `OnceLock`, puis injecté avec : + +```text +YellowstoneGrpcMetadataEntry::secret("x-token", ...) +``` + +Le token n'est jamais loggé et un canari runtime vérifie qu'il n'apparaît pas dans `Debug`. + +Le smoke continue de valider : + +```text +TLS +Subscribe bidirectionnel +filtre slots +première update Slot +slot > 0 +close borné +``` + +## 6. Signal de version + +Le correctif modifie Config runtime et un test Rust : + +```text +workspace.package.version = 0.2.9-pre.13.fix.2 +commit attendu = v0.2.9-pre.013-fix.002 +``` + +## 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 +``` + +### 7.2 Provisioning externe + +Obtenir le personal token PublicNode via le flow Allnodes/PublicNode : + +```text +https://www.allnodes.com/publicnode +``` + +Ne jamais le committer, le coller dans l'URL ou le passer comme argument de processus. + +### 7.3 Smoke live + +Le token est saisi dans une variable shell non exportée, puis envoyé au harness via stdin : + +```bash +read -rsp 'PublicNode Yellowstone x-token: ' PUBLICNODE_TOKEN +echo +printf '%s\n' "$PUBLICNODE_TOKEN" | cargo test -p ksp-onchain-transport-lib --test yellowstone_publicnode_smoke -- --ignored --nocapture --test-threads=1 +unset PUBLICNODE_TOKEN +``` + +Attendu après provisioning valide : + +```text +2 passed +0 failed +0 ignored +``` + +Si le provisioning provider est inaccessible ou si le token reste refusé, le smoke reste `EXTERNAL BLOCK`; ne pas transformer ce blocage en faux PASS. + +### 7.4 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 credential committé +``` + +La prochaine tranche reste `0.2.9-pre.014` de réconciliation documentaire uniquement après fermeture du gate technique, ou après qualification explicite d'un blocage provider externe.