diff --git a/Cargo.toml b/Cargo.toml index 24c80eb..ce4fa30 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ # file: Cargo.toml -# version: 238 +# version: 239 [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.2" +version = "0.2.9-pre.2.fix.1" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" @@ -15,33 +15,33 @@ publish = false [workspace.dependencies] argon2 = { version = "^0.5", default-features = false } +base64 = { version = "^0.23" } chacha20poly1305 = { version = "^0.11", default-features = false } +chrono = { version = "^0.4", default-features = false } directories = { version = "^6.0" } ed25519-dalek = { version = "^3.0", default-features = false } -getrandom = { version = "^0.4", default-features = false } -base64 = { version = "^0.23" } fs2 = { version = "^0.4" } futures-util = { version = "^0.3", default-features = false } -serde = { version = "^1.0" } -serde_json = { version = "^1.0" } +getrandom = { version = "^0.4", default-features = false } jsonschema = { version = "^0.51", default-features = false } reqwest = { version = "^0.13", default-features = false } +serde = { version = "^1.0" } +serde_json = { version = "^1.0" } solana-keypair = { version = "^3.1", default-features = false } solana-pubkey = { version = "^4.3", default-features = false } +tauri = { version = "^2.11" } +tauri-build = { version = "^2.6" } +tauri-plugin-dialog = { version = "^2.7" } +tauri-plugin-tracing = { version = "^0.3" } +tempfile = { version = "^3.27" } tracing = { version = "^0.1", default-features = false } tracing-subscriber = { version = "^0.3", default-features = false } tracing-appender = { version = "^0.2", default-features = false } tokio = { version = "^1.53", default-features = false } tokio-tungstenite = { version = "^0.30", default-features = false } tonic = { version = "^0.14", default-features = false } -yellowstone-grpc-proto = { version = "^12.6", default-features = false } -tempfile = { version = "^3.27" } -chrono = { version = "^0.4", default-features = false } -tauri = { version = "^2.11" } -tauri-build = { version = "^2.6" } -tauri-plugin-dialog = { version = "^2.7" } -tauri-plugin-tracing = { version = "^0.3" } ts-rs = { version = "^12.0" } +yellowstone-grpc-proto = { version = "^12.6", default-features = false } zeroize = { version = "^1.9" } [workspace.lints.rust] diff --git a/crates/ksp-onchain-transport-lib/src/grpc_channel.rs b/crates/ksp-onchain-transport-lib/src/grpc_channel.rs index 230eb15..fc9f91c 100644 --- a/crates/ksp-onchain-transport-lib/src/grpc_channel.rs +++ b/crates/ksp-onchain-transport-lib/src/grpc_channel.rs @@ -1,5 +1,5 @@ // file: crates/ksp-onchain-transport-lib/src/grpc_channel.rs -// version: 1 +// version: 2 /// Prepared Yellowstone gRPC channel owned by KSP Transport. /// @@ -9,7 +9,7 @@ pub struct YellowstoneGrpcChannel { endpoint_name: std::string::String, provider: crate::YellowstoneGrpcProviderName, cluster: crate::YellowstoneGrpcClusterName, - channel: tonic::transport::Channel, + _channel: tonic::transport::Channel, } impl YellowstoneGrpcChannel { @@ -28,6 +28,19 @@ impl YellowstoneGrpcChannel { .with_context("endpoint_name", endpoint.name()), ); } + if tokio::runtime::Handle::try_current().is_err() { + ksp_logging_lib::warn!( + target: crate::TRACING_TARGET, + endpoint_name = endpoint.name(), + provider = endpoint.provider().as_str(), + cluster = endpoint.cluster().as_str(), + "Yellowstone gRPC channel preparation requires an active Tokio runtime" + ); + return std::result::Result::Err( + ksp_core_lib::Error::new(crate::ERROR_CODE_GRPC_CHANNEL_FAILED, "Yellowstone gRPC channel requires an active Tokio runtime") + .with_context("endpoint_name", endpoint.name()), + ); + } let tonic_endpoint = match tonic::transport::Endpoint::from_shared(endpoint.url().as_str().to_owned()) { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => { @@ -60,7 +73,7 @@ impl YellowstoneGrpcChannel { endpoint_name: endpoint.name().to_owned(), provider: endpoint.provider().clone(), cluster: endpoint.cluster().clone(), - channel, + _channel: channel, }); } diff --git a/crates/ksp-onchain-transport-lib/unit_tests/grpc_channel.rs b/crates/ksp-onchain-transport-lib/unit_tests/grpc_channel.rs index 2bb7544..8dfc6c4 100644 --- a/crates/ksp-onchain-transport-lib/unit_tests/grpc_channel.rs +++ b/crates/ksp-onchain-transport-lib/unit_tests/grpc_channel.rs @@ -1,5 +1,5 @@ // file: crates/ksp-onchain-transport-lib/unit_tests/grpc_channel.rs -// version: 1 +// version: 2 fn endpoint(enabled: bool, value: &str) -> crate::YellowstoneGrpcEndpointSettings { let parsed = crate::YellowstoneGrpcEndpointUrl::parse(value).expect("fixture Yellowstone gRPC URL must parse"); @@ -13,8 +13,8 @@ fn endpoint(enabled: bool, value: &str) -> crate::YellowstoneGrpcEndpointSetting ); } -#[test] -fn grpc_channel_prepare_is_lazy_safe_and_keeps_tonic_private() { +#[tokio::test(flavor = "current_thread")] +async fn grpc_channel_prepare_is_lazy_safe_and_keeps_tonic_private() { let endpoint = endpoint(true, "http://127.0.0.1:10000/GRPC-SECRET-CANARY"); let channel = crate::YellowstoneGrpcChannel::prepare(&endpoint); assert!(channel.is_ok()); @@ -28,10 +28,20 @@ fn grpc_channel_prepare_is_lazy_safe_and_keeps_tonic_private() { let rendered = format!("{channel:?}"); assert!(!rendered.contains("GRPC-SECRET-CANARY")); assert!(!rendered.contains("127.0.0.1")); - let _channel_type = std::any::type_name_of_val(&channel.channel); + let _channel_type = std::any::type_name_of_val(&channel._channel); let _wire_type = std::any::type_name::(); } +#[test] +fn grpc_channel_prepare_requires_active_tokio_runtime_without_panicking() { + let endpoint = endpoint(true, "http://127.0.0.1:10000/GRPC-SECRET-CANARY"); + let result = crate::YellowstoneGrpcChannel::prepare(&endpoint); + assert!(result.is_err()); + let rendered = format!("{result:?}"); + assert!(!rendered.contains("GRPC-SECRET-CANARY")); + assert!(!rendered.contains("127.0.0.1")); +} + #[test] fn grpc_channel_prepare_rejects_disabled_endpoint_before_network_io() { let endpoint = endpoint(false, "http://127.0.0.1:10000"); diff --git a/crates/ksp-onchain-transport-lib/unit_tests/grpc_settings.rs b/crates/ksp-onchain-transport-lib/unit_tests/grpc_settings.rs index 9cbcc7b..213b02b 100644 --- a/crates/ksp-onchain-transport-lib/unit_tests/grpc_settings.rs +++ b/crates/ksp-onchain-transport-lib/unit_tests/grpc_settings.rs @@ -1,5 +1,5 @@ // file: crates/ksp-onchain-transport-lib/unit_tests/grpc_settings.rs -// version: 1 +// version: 2 fn endpoint(name: &str, enabled: bool, url: &str, session: crate::YellowstoneGrpcSessionSettings) -> crate::YellowstoneGrpcEndpointSettings { let parsed = crate::YellowstoneGrpcEndpointUrl::parse(url).expect("fixture Yellowstone gRPC URL must parse"); @@ -130,14 +130,14 @@ fn grpc_transport_settings_reject_excessive_descriptor_and_endpoint_count() { let parsed = crate::YellowstoneGrpcEndpointUrl::parse("http://127.0.0.1:10000").expect("fixture Yellowstone gRPC URL must parse"); let endpoints = (0..=super::MAX_GRPC_ENDPOINT_COUNT) .map(|index| { - crate::YellowstoneGrpcEndpointSettings::new( + return crate::YellowstoneGrpcEndpointSettings::new( format!("endpoint-{index}"), true, crate::YellowstoneGrpcProviderName::new("fixture"), crate::YellowstoneGrpcClusterName::new("devnet"), parsed.clone(), crate::YellowstoneGrpcSessionSettings::default(), - ) + ); }) .collect(); assert!(crate::YellowstoneGrpcTransportSettings::new(endpoints).validate().is_err()); diff --git a/deltas/0.2.9/pre.002-fix.001.md b/deltas/0.2.9/pre.002-fix.001.md new file mode 100644 index 0000000..8a51137 --- /dev/null +++ b/deltas/0.2.9/pre.002-fix.001.md @@ -0,0 +1,105 @@ + + + +# Delta `0.2.9-pre.002-fix.001` — Clippy + runtime Tokio du channel lazy + +## 1. Déclencheur + +Le premier gate opérateur de `pre.002` a fourni les preuves suivantes : + +```text +cargo fmt --all PASS +audit_rust_workspace_rules.py PASS / clean +cargo check --workspace PASS avec warning channel dead_code +cargo clippy --workspace --all-targets FAIL implicit_return dans grpc_settings test +cargo test -p ksp-onchain-transport-lib FAIL 344/345 : connect_lazy hors runtime Tokio +cargo test -p ksp-core-lib --test workspace_dependencies PASS 3/3 +cargo tree Transport / duplicates fournis et inspectés +``` + +Le test gRPC a révélé que `tonic::transport::Endpoint::connect_lazy()` nécessite un reactor Tokio actif et peut paniquer s'il est appelé depuis un contexte synchrone sans runtime. Ce comportement doit être borné dans la surface KSP au lieu d'être laissé comme précondition implicite Tonic. + +## 2. Corrections + +### Clippy + +La closure qui génère l'inventaire d'endpoints excessif dans `unit_tests/grpc_settings.rs` utilise désormais un `return` explicite, conformément à `clippy::implicit_return = deny`. + +### Runtime Tokio + +`YellowstoneGrpcChannel::prepare()` vérifie maintenant : + +```text +tokio::runtime::Handle::try_current() +``` + +avant l'appel à `connect_lazy()`. En l'absence de runtime actif, KSP retourne : + +```text +onchain_transport/grpc_channel_failed +``` + +avec uniquement `endpoint_name` comme contexte sûr. L'URL et le message interne Tonic ne sont pas réémis. + +Le test positif devient un `#[tokio::test(flavor = "current_thread")]`. Un nouveau test synchrone prouve que l'absence de runtime est retournée comme erreur sans exposer l'URL canari. + +### Champ Tonic + +Le channel **reste un champ de production**. Il n'est pas placé sous `#[cfg(test)]`, car `pre.002` matérialise réellement la ressource physique N1 qui sera consommée en `pre.003`. + +Le champ privé est temporairement nommé : + +```text +_channel: tonic::transport::Channel +``` + +pour exprimer qu'il est intentionnellement conservé mais pas encore consommé par une opération de production dans cette tranche. Cela supprime le warning `dead_code` sans modifier la shape production/test. + +## 3. Graphe Cargo observé + +Le gate opérateur confirme : + +```text +tonic 0.14.6 +yellowstone-grpc-proto 12.6.0 +prost/prost-types 0.14.4 +solana-pubkey 4.3.0 unifié +http 1.5.0 +hyper 1.11.0 +hyper-util 0.1.20 +tower 0.5.3 +bytes 1.12.1 +``` + +Les occurrences Prost 0.14.4 dans `--duplicates` viennent des unités runtime/build (`prost-build`, `tonic-prost-build`) de la même version ; aucune seconde version concurrente n'est introduite. + +## 4. Version + +Le fix touche le code/runtime ; la version workspace devient : + +```text +0.2.9-pre.2.fix.1 +``` + +Commit attendu après validation : + +```text +v0.2.9-pre.002-fix.001 +``` + +## 5. Validation requise + +```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 +cargo tree -p ksp-onchain-transport-lib +cargo tree -p ksp-onchain-transport-lib --duplicates +cargo tree --duplicates +``` + +Le passage à `pre.003` est interdit tant que ces gates ne sont pas verts ou qu'un nouvel écart n'est pas traité par un fix supplémentaire. diff --git a/docs/plans/016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md b/docs/plans/016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md index c11ac0e..8e64694 100644 --- a/docs/plans/016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md +++ b/docs/plans/016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md @@ -1,9 +1,9 @@ - + # Plan `0.2.9` — moteur Yellowstone gRPC + standard Solana + PublicNode -> **Statut : `0.2.9-pre.002` — première matérialisation N1 préparée : dépendances Yellowstone/Tonic minimales, settings runtime bornés, error mapping initial et channel Tonic lazy privé. Les gates Cargo restent à exécuter par l’opérateur avant commit. `0.2.9` reste bornée à un moteur client Yellowstone partagé, une façade Solana Yellowstone standard et une première intégration concrète PublicNode. Seuls OrbitFlare puis Helius LaserStream gRPC sont actuellement planifiés comme releases provider suivantes ; les autres providers restent en TODO/IDEAS sans numéro réservé. Chaque prerelease vise 15–20 minutes de travail effectif et la release complète doit rester clôturable dans une seule session de chat.** +> **Statut : `0.2.9-pre.002-fix.001` — correctif technique du premier gate opérateur : le channel lazy est conservé comme état N1 de production, `prepare()` refuse proprement l’absence de runtime Tokio au lieu de laisser Tonic paniquer, et les canaries Clippy/runtime sont corrigés. Les gates Cargo doivent être réexécutés après ce fix avant commit. `0.2.9` reste bornée à un moteur client Yellowstone partagé, une façade Solana Yellowstone standard et une première intégration concrète PublicNode. Seuls OrbitFlare puis Helius LaserStream gRPC sont actuellement planifiés comme releases provider suivantes ; les autres providers restent en TODO/IDEAS sans numéro réservé. Chaque prerelease vise 15–20 minutes de travail effectif et la release complète doit rester clôturable dans une seule session de chat.** ## 1. Objet, base et état d'ouverture @@ -549,6 +549,8 @@ futures-util dépendance existante inchangée Le choix est volontairement plus étroit que le forecast initial : `pre.002` matérialise les messages wire publiés et le channel HTTP/2 lazy, mais **pas encore le client RPC généré, TLS, metadata ou compression**. Les éventuelles features/dependencies supplémentaires doivent être justifiées par `pre.003` et vérifiées par `cargo tree`; elles ne sont pas activées par anticipation. +Le premier gate opérateur de `pre.002` confirme l'alignement de versions utile : Tonic 0.14.6 réutilise `http` 1.5, `hyper` 1.11, `hyper-util` 0.1, `tower` 0.5 et `bytes` 1.12 déjà présents ; `yellowstone-grpc-proto` unifie `solana-pubkey` en 4.3.0. Les occurrences Prost 0.14.4 visibles dans `cargo tree --duplicates` correspondent aux contextes runtime/build de la même version, notamment `prost-build`/`tonic-prost-build`, et ne constituent pas une seconde génération de version à corriger. + ### C. proto/génération KSP minimale bornée Avantage : contrôle maximum du code généré. @@ -895,8 +897,8 @@ Prévision courante : pre.001 DONE — audit upstream/service/proto + providers gratuits + licences/deps + architecture + threat model + sizing budget : 15–20 min nominal ; preuve : plan + matrice + stratégie B + forecast recalibré -pre.002 IMPLEMENTED — moteur Yellowstone : proto/dependencies + settings/errors + channel minimal - budget : 15–20 min ; preuve locale : audit statique KSP clean + redaction/API canaries ajoutés ; compile/tests/cargo tree = gate opérateur +pre.002 FIX.001 CANDIDATE — moteur Yellowstone : proto/dependencies + settings/errors + channel minimal + budget : 15–20 min ; premier gate : fmt/audit/check PASS, Clippy/test ont révélé return implicite + runtime Tokio ; fix appliqué ; cargo tree inspecté pre.003 moteur Yellowstone : TLS/metadata générique + fixture locale + 7 unary RPCs budget : 15–20 min ; preuve : connect/TLS/timeouts/Status safe + wire unary exact diff --git a/docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md b/docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md index c0d4b0c..e07a1d2 100644 --- a/docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md +++ b/docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md @@ -1,9 +1,9 @@ - + # Validation `0.2.9` — moteur Yellowstone + standard Solana + PublicNode -> **Statut : matrice ouverte par `0.2.9-pre.001`, corrigée par `pre.001-fix.002` et avancée par `pre.002`. N1 possède désormais une première matérialisation settings/error/channel lazy ; la validation Cargo de cette tranche reste un gate opérateur. Le scope distingue moteur Yellowstone, façade Solana standard et première intégration PublicNode. OrbitFlare et Helius sont les seules releases provider suivantes actuellement planifiées ; les autres providers restent en TODO/IDEAS sans numéro réservé.** +> **Statut : matrice ouverte par `0.2.9-pre.001`, corrigée par `pre.001-fix.002`, avancée par `pre.002` puis corrigée par `pre.002-fix.001`. Le premier gate opérateur a validé fmt/audit/check et le graphe, puis a révélé un retour Clippy implicite et l'exigence de runtime Tokio de `connect_lazy()` ; le fix borne désormais ce cas sans panic et attend la réexécution Cargo complète. Le scope distingue moteur Yellowstone, façade Solana standard et première intégration PublicNode. OrbitFlare et Helius sont les seules releases provider suivantes actuellement planifiées ; les autres providers restent en TODO/IDEAS sans numéro réservé.** ## 1. Autorités du gate @@ -402,7 +402,7 @@ Les autres providers restent en TODO/IDEAS sans release dédiée. Aucune façade ```text pre.001 DONE audit/sizing/architecture 15–20 min nominal -pre.002 IMPLEMENTED / CARGO GATE moteur: deps/settings/errors/channel 15–20 min +pre.002 FIX.001 CANDIDATE moteur: deps/settings/errors/channel 15–20 min pre.003 TODO moteur: TLS/metadata + fixture + 7 unary 15–20 min pre.004 TODO standard: Subscribe common/from_slot/bounds 15–20 min pre.005 TODO standard: accounts + slots 15–20 min @@ -446,25 +446,48 @@ Gate de sizing : une prerelease estimée à plus de 20 minutes est scindée ; si **Verdict `pre.001` : gate positif ; `pre.002` peut matérialiser la stratégie B sous validation Cargo.** -## 17. Gate `pre.002` +## 17. Gate `pre.002` / `pre.002-fix.001` -| Critère de tranche | Verdict local | -|---------------------------------------------------------|------------------------------------------------| -| workspace version `0.2.9-pre.2` | PASS source | -| stratégie B matérialisée sans `yellowstone-grpc-client` | PASS source | -| proto `12.6` sans default/features KSP | PASS manifest | -| Tonic `0.14` limité à `channel` | PASS manifest | -| aucun Config/env dans Transport gRPC | PASS source/audit | -| aucun couplage WebSocket | PASS source | -| settings runtime bornés | PASS source/tests ajoutés | -| URL/channel diagnostics redacted | PASS source/tests ajoutés | -| channel Tonic lazy privé | PASS source/API canary ajouté | -| raw Tonic/Yellowstone absent de l'API publique | PASS source/API canary ajouté | -| audit `scripts/audit_rust_workspace_rules.py` | PASS / clean | -| `cargo fmt --all` | PENDING opérateur | -| `cargo check --workspace` | PENDING opérateur | -| `cargo clippy --workspace --all-targets` | PENDING opérateur | -| tests Transport/Core/workspace | PENDING opérateur | -| `cargo tree` direct/duplicates | PENDING opérateur | +Premier passage opérateur sur `pre.002` : -**Verdict `pre.002` local : implémentation candidate prête pour validation opérateur. La tranche n'est déclarée fermée qu'après les gates Cargo réels.** +| Gate / observation | Résultat avant fix | État après `fix.001` | +|---------------------------------------------------------|----------------------------------------------------------------------|---------------------------------------------| +| workspace version | `0.2.9-pre.2` | `0.2.9-pre.2.fix.1` | +| `cargo fmt --all` | PASS | à réexécuter | +| audit Rust workspace | PASS / clean | PASS local / clean | +| `cargo check --workspace` | PASS, avec warning `channel` jamais lu | warning supprimé par état privé `_channel` | +| `cargo clippy --workspace --all-targets` | FAIL : `implicit_return` dans `unit_tests/grpc_settings.rs` | corrigé ; à réexécuter | +| `cargo test -p ksp-onchain-transport-lib` | FAIL : `connect_lazy()` appelé hors reactor Tokio, 344/345 avant fix | test/runtime corrigés ; à réexécuter | +| dependency canary Core | PASS / 3 passed | inchangé | +| `cargo tree -p ksp-onchain-transport-lib` | obtenu et inspecté | PASS graphe | +| `cargo tree -p ... --duplicates` / workspace duplicates | obtenu et inspecté | PASS avec doublons build/runtime documentés | + +Correction `fix.001` : + +```text +le champ Tonic reste présent en production ; il n'est pas placé sous cfg(test) +le champ privé devient `_channel` tant qu'aucune opération pre.003 ne le consomme +prepare() vérifie tokio::runtime::Handle::try_current() avant connect_lazy() +absence de runtime -> KspError grpc_channel_failed sûr, jamais panic Tonic +le test positif s'exécute sous #[tokio::test] +un test sync couvre explicitement l'absence de runtime et la redaction +la closure de cardinalité settings utilise un return explicite conforme à Clippy +``` + +Le choix de ne pas mettre le channel sous `#[cfg(test)]` est intentionnel : `pre.002` matérialise une ressource N1 réelle de production qui sera consommée par `pre.003`; une compilation normale sans ce champ rendrait le contrat de tranche artificiellement différent entre production et tests. + +Inspection du graphe opérateur : + +```text +tonic 0.14.6 +yellowstone-grpc-proto 12.6.0 +prost/prost-types 0.14.4 +solana-pubkey 4.3.0 unifié +http/hyper/hyper-util 1.5.0 / 1.11.0 / 0.1.20 +tower 0.5.3 +bytes 1.12.1 +``` + +Aucune version concurrente Tonic/Prost/Solana n'a été introduite dans le graphe Transport. Les répétitions Prost de même version proviennent des unités build/runtime de la crate proto publiée. + +**Verdict `pre.002-fix.001` : correctif source prêt ; `pre.002` reste ouverte jusqu'à réexécution verte de fmt/audit/check/clippy/tests/workspace et confirmation finale du graphe.**