v0.2.9-pre.013-fix.001

This commit is contained in:
2026-08-24 22:48:33 +02:00
parent 39dbd86f1e
commit d98ee736be
5 changed files with 317 additions and 52 deletions

View File

@@ -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"

View File

@@ -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"
}
]
}
]
}

View File

@@ -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");
}
}
}

View File

@@ -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<ksp_onchain_transport_lib::YellowstoneGrpcEndpointSettings> {
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<ksp_onchain_transport_lib::YellowstoneGrpcEndpointSettings> {
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<ksp_onchain_transport_l
4 * 1024 * 1024,
);
return std::result::Result::Ok(ksp_onchain_transport_lib::YellowstoneGrpcEndpointSettings::new(
"publicnode_mainnet_yellowstone",
name,
true,
ksp_onchain_transport_lib::YellowstoneGrpcProviderName::new("publicnode"),
ksp_onchain_transport_lib::YellowstoneGrpcClusterName::new("mainnet-beta"),
ksp_onchain_transport_lib::YellowstoneGrpcClusterName::new(cluster),
url,
session,
));
}
#[tokio::test(flavor = "current_thread")]
#[ignore = "opt-in live PublicNode Mainnet Yellowstone gRPC smoke; performs external TLS and unary gRPC requests"]
async fn publicnode_mainnet_yellowstone_reaches_standard_version_and_slot_without_credentials() {
let endpoint = publicnode_mainnet_endpoint().expect("programmatic PublicNode Yellowstone settings must construct an endpoint");
let channel = ksp_onchain_transport_lib::YellowstoneGrpcChannel::connect(&endpoint).await.expect("PublicNode Yellowstone TLS connection must succeed");
assert_eq!(channel.endpoint_name(), "publicnode_mainnet_yellowstone");
assert_eq!(channel.provider().as_str(), "publicnode");
assert_eq!(channel.cluster().as_str(), "mainnet-beta");
let client = channel.standard_unary_client();
let version = client.get_version().await.expect("PublicNode Yellowstone GetVersion must succeed");
assert!(!version.version().is_empty());
let slot = client
.get_slot(std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Confirmed))
.await
.expect("PublicNode Yellowstone GetSlot must succeed");
assert!(slot.slot() > 0);
fn slot_request() -> ksp_core_lib::Result<ksp_onchain_transport_lib::YellowstoneSubscribeRequest> {
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;
}

View File

@@ -0,0 +1,182 @@
<!-- file: deltas/0.2.9/pre.013-fix.001.md -->
<!-- version: 1 -->
# 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.