From d3cc7c93f0e68e758150ada06468f9344f81d382 Mon Sep 17 00:00:00 2001 From: SinuS Von SifriduS Date: Tue, 18 Aug 2026 21:03:00 +0200 Subject: [PATCH] v0.2.4-pre.005 --- Cargo.toml | 4 +- ...get_block_production.invalid_identity.json | 11 + .../http/get_block_production.success.json | 11 + .../src/rpc_blocks.rs | 64 +++- .../tests/public_api.rs | 16 +- .../tests/release_completeness.rs | 11 +- .../unit_tests/rpc_blocks.rs | 84 ++++- deltas/0.2.4/pre.005.md | 355 ++++++++++++++++++ 8 files changed, 544 insertions(+), 12 deletions(-) create mode 100644 crates/ksp-onchain-transport-lib/fixtures/http/get_block_production.invalid_identity.json create mode 100644 crates/ksp-onchain-transport-lib/fixtures/http/get_block_production.success.json create mode 100644 deltas/0.2.4/pre.005.md diff --git a/Cargo.toml b/Cargo.toml index a393b98..9dd3710 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ # file: Cargo.toml -# version: 135 +# version: 136 [workspace] resolver = "3" members = ["crates/ksp-app-config-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib", "crates/ksp-onchain-transport-lib"] [workspace.package] -version = "0.2.4-pre.4" +version = "0.2.4-pre.5" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" diff --git a/crates/ksp-onchain-transport-lib/fixtures/http/get_block_production.invalid_identity.json b/crates/ksp-onchain-transport-lib/fixtures/http/get_block_production.invalid_identity.json new file mode 100644 index 0000000..e6ad583 --- /dev/null +++ b/crates/ksp-onchain-transport-lib/fixtures/http/get_block_production.invalid_identity.json @@ -0,0 +1,11 @@ +{ + "jsonrpc":"2.0", + "result":{ + "context":{"slot":430000123,"apiVersion":"4.2.1"}, + "value":{ + "byIdentity":{"not-a-pubkey":[8,7]}, + "range":{"firstSlot":430000000,"lastSlot":430000099} + } + }, + "id":1 +} diff --git a/crates/ksp-onchain-transport-lib/fixtures/http/get_block_production.success.json b/crates/ksp-onchain-transport-lib/fixtures/http/get_block_production.success.json new file mode 100644 index 0000000..8b1fbbd --- /dev/null +++ b/crates/ksp-onchain-transport-lib/fixtures/http/get_block_production.success.json @@ -0,0 +1,11 @@ +{ + "jsonrpc":"2.0", + "result":{ + "context":{"slot":430000123,"apiVersion":"4.2.1"}, + "value":{ + "byIdentity":{"11111111111111111111111111111111":[8,7]}, + "range":{"firstSlot":430000000,"lastSlot":430000099} + } + }, + "id":1 +} diff --git a/crates/ksp-onchain-transport-lib/src/rpc_blocks.rs b/crates/ksp-onchain-transport-lib/src/rpc_blocks.rs index d469a7b..c198ee4 100644 --- a/crates/ksp-onchain-transport-lib/src/rpc_blocks.rs +++ b/crates/ksp-onchain-transport-lib/src/rpc_blocks.rs @@ -1,5 +1,5 @@ // file: crates/ksp-onchain-transport-lib/src/rpc_blocks.rs -// version: 4 +// version: 5 /// Transaction detail level accepted by modern `getBlock` requests. #[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)] @@ -143,7 +143,6 @@ impl SolanaBlockProductionRange { /// Serializes this range to the exact Solana JSON-RPC object. #[must_use] - #[cfg(test)] pub(crate) fn to_json_value(self) -> serde_json::Value { let mut object = serde_json::Map::new(); object.insert("firstSlot".to_owned(), serde_json::Value::Number(self.first_slot.into())); @@ -192,14 +191,12 @@ impl SolanaBlockProductionConfig { } /// Returns whether this config would serialize to an empty object. - #[cfg(test)] pub(crate) const fn is_empty(&self) -> bool { return self.commitment.is_none() && self.identity.is_none() && self.range.is_none(); } /// Serializes this config to the exact Solana JSON-RPC object. #[must_use] - #[cfg(test)] pub(crate) fn to_json_value(&self) -> serde_json::Value { let mut object = serde_json::Map::new(); if let std::option::Option::Some(commitment) = self.commitment { @@ -287,7 +284,6 @@ impl SolanaBlockProduction { } /// Decodes a block-production result from its Solana JSON wire shape. - #[cfg(test)] pub(crate) fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result { let decoded = crate::decode_wire_json::(method, value); let wire = match decoded { @@ -672,6 +668,38 @@ impl crate::HttpTransportPool { return self.get_blocks_u64_list("getBlocksWithLimit", role, params).await; } + /// Executes typed `getBlockProduction` with optional identity, range, and commitment filters. + pub async fn get_block_production( + &self, + role: &crate::HttpRoleName, + config: std::option::Option<&crate::SolanaBlockProductionConfig>, + ) -> ksp_core_lib::Result> { + if let std::option::Option::Some(config) = config + && let std::option::Option::Some(range) = config.range() + && let std::option::Option::Some(last_slot) = range.last_slot() + && last_slot < range.first_slot() + { + return std::result::Result::Err( + ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RPC_PARAMETERS, "getBlockProduction lastSlot must not be less than firstSlot") + .with_context("rpc_method", "getBlockProduction") + .with_context("first_slot", range.first_slot().to_string()) + .with_context("last_slot", last_slot.to_string()), + ); + } + let mut params = std::vec::Vec::new(); + if let std::option::Option::Some(config) = config + && !config.is_empty() + { + params.push(config.to_json_value()); + } + let value = self.execute_blocks_rpc("getBlockProduction", role, params).await; + let value = match value { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + return decode_block_production_response("getBlockProduction", value); + } + /// Executes typed `getRecentPerformanceSamples`, preserving runtime order and older sample shapes. pub async fn get_recent_performance_samples( &self, @@ -770,6 +798,24 @@ fn invalid_blocks_limit(method: &'static str, message: &'static str, limit: u ); } +fn decode_block_production_response(method: &str, value: serde_json::Value) -> ksp_core_lib::Result> { + let decoded = crate::decode_wire_json::(method, value); + let wire = match decoded { + std::result::Result::Ok(wire) => wire, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let context = crate::SolanaRpcContext::decode_wire(method, wire.context); + let context = match context { + std::result::Result::Ok(context) => context, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let value = crate::SolanaBlockProduction::decode_wire(method, wire.value); + return match value { + std::result::Result::Ok(value) => std::result::Result::Ok(crate::SolanaRpcResponse::new(context, value)), + std::result::Result::Err(error) => std::result::Result::Err(error), + }; +} + fn decode_performance_samples(method: &str, value: serde_json::Value) -> ksp_core_lib::Result> { let values = crate::decode_wire_json::>(method, value); let values = match values { @@ -878,7 +924,6 @@ struct WireBlockCommitment { total_stake: u64, } -#[cfg(test)] #[derive(serde::Deserialize)] #[serde(rename_all = "camelCase")] struct WireBlockProductionRange { @@ -886,7 +931,6 @@ struct WireBlockProductionRange { last_slot: u64, } -#[cfg(test)] #[derive(serde::Deserialize)] #[serde(rename_all = "camelCase")] struct WireBlockProduction { @@ -894,6 +938,12 @@ struct WireBlockProduction { range: WireBlockProductionRange, } +#[derive(serde::Deserialize)] +struct WireBlockProductionRpcResponse { + context: serde_json::Value, + value: serde_json::Value, +} + #[cfg(test)] #[derive(serde::Deserialize)] #[serde(rename_all = "camelCase")] diff --git a/crates/ksp-onchain-transport-lib/tests/public_api.rs b/crates/ksp-onchain-transport-lib/tests/public_api.rs index c6ba0dd..7cdb0f9 100644 --- a/crates/ksp-onchain-transport-lib/tests/public_api.rs +++ b/crates/ksp-onchain-transport-lib/tests/public_api.rs @@ -1,5 +1,5 @@ // file: crates/ksp-onchain-transport-lib/tests/public_api.rs -// version: 18 +// version: 19 //! Integration tests for the public `ksp-onchain-transport-lib` consumer contract. @@ -402,3 +402,17 @@ fn public_v0_2_4_pre_004_range_and_performance_wrappers_are_available_from_crate let _get_blocks_with_limit = ksp_onchain_transport_lib::HttpTransportPool::get_blocks_with_limit; let _get_recent_performance_samples = ksp_onchain_transport_lib::HttpTransportPool::get_recent_performance_samples; } + +#[test] +fn public_v0_2_4_pre_005_block_production_wrapper_is_available_from_crate_root() { + let identity = "11111111111111111111111111111111".parse::().expect("public fixture identity must parse"); + let range = ksp_onchain_transport_lib::SolanaBlockProductionRange::new(10, std::option::Option::Some(20)); + let config = ksp_onchain_transport_lib::SolanaBlockProductionConfig::new( + std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Finalized), + std::option::Option::Some(identity), + std::option::Option::Some(range), + ); + assert_eq!(config.identity(), std::option::Option::Some(&identity)); + assert_eq!(config.range().expect("public range must exist").last_slot(), std::option::Option::Some(20)); + let _method = ksp_onchain_transport_lib::HttpTransportPool::get_block_production; +} diff --git a/crates/ksp-onchain-transport-lib/tests/release_completeness.rs b/crates/ksp-onchain-transport-lib/tests/release_completeness.rs index 3096b24..2ddcf0f 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: 16 +// version: 17 //! Release-level completeness canaries for the staged HTTP wrapper sequence. @@ -521,3 +521,12 @@ fn release_v0_2_4_pre_004_range_performance_subset_is_exact_and_retry_safe() { assert_eq!(actual, expected); assert_eq!(actual.len(), 3); } + +#[test] +fn release_v0_2_4_pre_005_block_production_subset_is_exact_and_retry_safe() { + let descriptor = ksp_onchain_transport_lib::find_http_rpc_method("getBlockProduction").expect("getBlockProduction descriptor must exist"); + assert_eq!(descriptor.category(), ksp_onchain_transport_lib::HttpRpcCategory::Blocks); + assert_eq!(descriptor.coverage_release(), ksp_onchain_transport_lib::HttpRpcCoverageRelease::V0_2_4); + assert_eq!(descriptor.operation_kind(), ksp_onchain_transport_lib::RpcOperationKind::Read); + assert_eq!(descriptor.transport_retry_class(), ksp_onchain_transport_lib::TransportRetryClass::RetrySafe); +} diff --git a/crates/ksp-onchain-transport-lib/unit_tests/rpc_blocks.rs b/crates/ksp-onchain-transport-lib/unit_tests/rpc_blocks.rs index bc43de3..f44d29a 100644 --- a/crates/ksp-onchain-transport-lib/unit_tests/rpc_blocks.rs +++ b/crates/ksp-onchain-transport-lib/unit_tests/rpc_blocks.rs @@ -1,5 +1,5 @@ // file: crates/ksp-onchain-transport-lib/unit_tests/rpc_blocks.rs -// version: 3 +// version: 4 #[test] fn transaction_details_and_get_block_config_preserve_all_modern_options() { @@ -453,3 +453,85 @@ async fn typed_get_recent_performance_samples_rejects_above_720_before_io() { assert_eq!(error.context()[1].key(), "limit"); assert_eq!(error.context()[1].value(), "721"); } + +#[tokio::test(flavor = "current_thread")] +async fn typed_get_block_production_serializes_full_config_and_decodes_contextual_result() { + let identity = "11111111111111111111111111111111".parse::().expect("fixture identity must parse"); + let range = crate::SolanaBlockProductionRange::new(430_000_000, std::option::Option::Some(430_000_099)); + let config = crate::SolanaBlockProductionConfig::new( + std::option::Option::Some(crate::SolanaCommitment::Finalized), + std::option::Option::Some(identity), + std::option::Option::Some(range), + ); + let (url, handle) = serve_once(include_str!("../fixtures/http/get_block_production.success.json")); + let pool = pool_for_url(url.as_str()); + let response = pool + .get_block_production(&crate::HttpRoleName::new("default"), std::option::Option::Some(&config)) + .await + .expect("block production fixture must succeed"); + assert_eq!(response.context().slot(), 430_000_123); + assert_eq!(response.context().api_version(), std::option::Option::Some("4.2.1")); + assert_eq!(response.value().by_identity().get(&identity), std::option::Option::Some(&(8, 7))); + assert_eq!(response.value().range().first_slot(), 430_000_000); + assert_eq!(response.value().range().last_slot(), 430_000_099); + let request = handle.join().expect("fixture server must join"); + let body = request_body(request.as_str()); + assert_eq!(body["method"], serde_json::json!("getBlockProduction")); + assert_eq!( + body["params"], + serde_json::json!([{ + "commitment":"finalized", + "identity":"11111111111111111111111111111111", + "range":{"firstSlot":430000000,"lastSlot":430000099} + }]) + ); +} + +#[tokio::test(flavor = "current_thread")] +async fn typed_get_block_production_omits_empty_config_and_supports_open_range_with_processed_commitment() { + let role = crate::HttpRoleName::new("default"); + let empty = crate::SolanaBlockProductionConfig::default(); + let (url, handle) = serve_once(include_str!("../fixtures/http/get_block_production.success.json")); + let pool = pool_for_url(url.as_str()); + let response = pool.get_block_production(&role, std::option::Option::Some(&empty)).await.expect("empty block production config must succeed"); + assert_eq!(response.value().range().last_slot(), 430_000_099); + let request = handle.join().expect("fixture server must join"); + assert_eq!(request_body(request.as_str())["params"], serde_json::json!([])); + + let range = crate::SolanaBlockProductionRange::new(430_000_000, std::option::Option::None); + let config = crate::SolanaBlockProductionConfig::new( + std::option::Option::Some(crate::SolanaCommitment::Processed), + std::option::Option::None, + std::option::Option::Some(range), + ); + let (url, handle) = serve_once(include_str!("../fixtures/http/get_block_production.success.json")); + let pool = pool_for_url(url.as_str()); + pool.get_block_production(&role, std::option::Option::Some(&config)).await.expect("open block production range must succeed"); + let request = handle.join().expect("fixture server must join"); + assert_eq!(request_body(request.as_str())["params"], serde_json::json!([{"commitment":"processed","range":{"firstSlot":430000000}}])); +} + +#[tokio::test(flavor = "current_thread")] +async fn typed_get_block_production_rejects_reversed_range_before_io() { + let range = crate::SolanaBlockProductionRange::new(430_000_100, std::option::Option::Some(430_000_099)); + let config = crate::SolanaBlockProductionConfig::new(std::option::Option::None, std::option::Option::None, std::option::Option::Some(range)); + let pool = pool_for_url("http://127.0.0.1:9"); + let result = pool.get_block_production(&crate::HttpRoleName::new("default"), std::option::Option::Some(&config)).await; + let error = result.expect_err("reversed block production range must reject before I/O"); + assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RPC_PARAMETERS); + assert_eq!(error.context()[1].key(), "first_slot"); + assert_eq!(error.context()[1].value(), "430000100"); + assert_eq!(error.context()[2].key(), "last_slot"); + assert_eq!(error.context()[2].value(), "430000099"); +} + +#[tokio::test(flavor = "current_thread")] +async fn typed_get_block_production_rejects_invalid_wire_identity_without_echoing_value() { + let (url, handle) = serve_once(include_str!("../fixtures/http/get_block_production.invalid_identity.json")); + let pool = pool_for_url(url.as_str()); + let result = pool.get_block_production(&crate::HttpRoleName::new("default"), std::option::Option::None).await; + let error = result.expect_err("invalid block production identity must fail closed"); + assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RESPONSE); + assert!(!error.to_string().contains("not-a-pubkey")); + handle.join().expect("fixture server must join"); +} diff --git a/deltas/0.2.4/pre.005.md b/deltas/0.2.4/pre.005.md new file mode 100644 index 0000000..701995c --- /dev/null +++ b/deltas/0.2.4/pre.005.md @@ -0,0 +1,355 @@ + + + +# Delta `0.2.4-pre.005` — `getBlockProduction` + +## Base requise + +Livraison précédente : + +```text +0.2.4-pre.004 +workspace.package.version = "0.2.4-pre.4" +``` + +Les validations locales fournies pour cette base sont propres : + +```text +cargo fmt --all -> terminé +cargo check --workspace -> terminé sans warning +cargo clippy --workspace --all-targets -> terminé sans warning +cargo test -p ksp-onchain-transport-lib -> 208 unit tests OK + 22 public API tests OK + 17 release-completeness tests OK + 1 smoke Devnet ignoré comme prévu + 0 échec +``` + +## Objectif + +Implémenter exactement la tranche dédiée prévue par le plan `0.2.4` : + +```text +getBlockProduction +``` + +Cette méthode est déjà enregistrée dans la partition : + +```text +V0_2_4 / Blocks / Read / RetrySafe +``` + +Le registre central n'est donc pas modifié. + +Après cette tranche, neuf des dix wrappers Blocks de `0.2.4` sont matérialisés. Il reste volontairement : + +```text +pre.006 getBlock +``` + +Les cinq méthodes Economics restent hors scope jusqu'aux tranches `pre.007` et `pre.008`. + +## Réaudit RPC de la tranche + +Le contrat stable a été recroisé avec Agave `v4.2.1` avant implémentation. + +`RpcBlockProductionConfig` expose exactement : + +```text +identity: Option +range: Option<{firstSlot, lastSlot?}> +commitment: Option +``` + +Le résultat est contextualisé : + +```text +RpcResponse +``` + +avec : + +```text +context +value.byIdentity +value.range.firstSlot +value.range.lastSlot +``` + +`byIdentity` associe chaque validator identity aux comptes : + +```text +(leader slots, blocks produced) +``` + +Le runtime Agave rejette `lastSlot < firstSlot`. KSP effectue donc le même contrôle déterministe avant I/O. + +Aucune restriction locale `confirmed/finalized` n'est appliquée au commitment de `getBlockProduction` : le `CommitmentConfig` général du runtime est préservé, y compris `processed`. + +Aucun nouvel overload, champ ou type de résultat n'a été identifié ; le plan `011` reste donc inchangé en version 3. + +## Version Cargo + +Nouvelle prerelease technique : + +```text +0.2.4-pre.4 -> 0.2.4-pre.5 +``` + +Aucune dépendance ni feature Cargo n'est ajoutée ou modifiée. + +## Wrapper typed `getBlockProduction` + +Signature publique : + +```text +HttpTransportPool::get_block_production( + role, + Option<&SolanaBlockProductionConfig>, +) -> Result> +``` + +La configuration publique acquise en `pre.002` est réutilisée sans duplication : + +```text +SolanaBlockProductionConfig +SolanaBlockProductionRange +``` + +Exemple complet : + +```json +[ + { + "commitment":"finalized", + "identity":"11111111111111111111111111111111", + "range":{"firstSlot":430000000,"lastSlot":430000099} + } +] +``` + +Une plage ouverte conserve l'absence de `lastSlot` : + +```json +[ + { + "commitment":"processed", + "range":{"firstSlot":430000000} + } +] +``` + +Un `Some(SolanaBlockProductionConfig::default())` est canoniquement omis et produit : + +```json +[] +``` + +Il n'existe ici aucun overload positional dont la distinction dépendrait d'un objet vide ; l'omission est donc équivalente au config runtime par défaut. + +## Validation déterministe avant I/O + +Lorsque les deux bornes sont explicites : + +```text +lastSlot >= firstSlot -> valide +lastSlot < firstSlot -> rejet avant I/O +``` + +L'erreur KSP utilise le domaine partagé : + +```text +ERROR_CODE_INVALID_RPC_PARAMETERS +``` + +avec contexte non secret : + +```text +rpc_method +first_slot +last_slot +``` + +Aucune autre validation économique ou de commitment n'est inventée. + +## Résultat contextualisé + +Le wrapper préserve le `SolanaRpcResponse` commun : + +```text +context.slot +context.apiVersion? +value.byIdentity +value.range +``` + +Les clés de `byIdentity` sont décodées en `Pubkey` KSP. Une clé wire invalide ferme le décodage avec `ERROR_CODE_INVALID_RESPONSE` sans recopier la valeur fautive dans le diagnostic. + +Les couples : + +```text +(leader slots, blocks produced) +``` + +sont conservés exactement ; Transport ne recalcule pas les statistiques et n'interprète pas leur économie. + +## Discipline `#[cfg(test)]` + +La règle introduite par `pre.002-fix.001` reste appliquée. + +Éléments devenus runtime dans cette tranche parce qu'ils ont désormais un consommateur réel : + +```text +SolanaBlockProductionRange::to_json_value +SolanaBlockProductionConfig::is_empty +SolanaBlockProductionConfig::to_json_value +SolanaBlockProduction::decode_wire +WireBlockProductionRange +WireBlockProduction +WireBlockProductionRpcResponse +``` + +Éléments déjà runtime des tranches précédentes : + +```text +SolanaBlockCommitment::decode_wire +WireBlockCommitment +SolanaPerformanceSample::decode_wire +WirePerformanceSample +``` + +Restent test-only jusqu'à `pre.006` : + +```text +SolanaGetBlockConfig::{is_empty,to_json_value} +SolanaBlockReward::decode_wire +SolanaBlockTransaction::decode_wire +SolanaConfirmedBlock::decode_wire +helpers privés transaction/reward de bloc +WireBlockReward +WireBlockTransaction +WireConfirmedBlock +``` + +Tous les helpers/wires Economics restent également test-only jusqu'à leurs prereleases dédiées. + +Aucun `#[allow(dead_code)]` n'est introduit. + +## Fixtures HTTP ajoutées + +```text +crates/ksp-onchain-transport-lib/fixtures/http/get_block_production.success.json +crates/ksp-onchain-transport-lib/fixtures/http/get_block_production.invalid_identity.json +``` + +Elles couvrent : + +- contexte `slot + apiVersion` ; +- `byIdentity` typé ; +- plage effective ; +- erreur fermée sur identity wire invalide. + +## Tests ajoutés + +Quatre unit tests HTTP sont ajoutés : + +```text +typed_get_block_production_serializes_full_config_and_decodes_contextual_result +typed_get_block_production_omits_empty_config_and_supports_open_range_with_processed_commitment +typed_get_block_production_rejects_reversed_range_before_io +typed_get_block_production_rejects_invalid_wire_identity_without_echoing_value +``` + +Une canarie public API vérifie que le wrapper reste disponible depuis la racine de crate : + +```text +public_v0_2_4_pre_005_block_production_wrapper_is_available_from_crate_root +``` + +Une canarie release-completeness vérifie le descriptor : + +```text +release_v0_2_4_pre_005_block_production_subset_is_exact_and_retry_safe +``` + +Compteurs attendus après validation locale : + +```text +212 unit tests +23 public API tests +18 release-completeness tests +1 smoke Devnet ignoré +``` + +## Fichiers modifiés + +```text +Cargo.toml +crates/ksp-onchain-transport-lib/src/rpc_blocks.rs +crates/ksp-onchain-transport-lib/unit_tests/rpc_blocks.rs +crates/ksp-onchain-transport-lib/tests/public_api.rs +crates/ksp-onchain-transport-lib/tests/release_completeness.rs +``` + +## Fichiers ajoutés + +```text +crates/ksp-onchain-transport-lib/fixtures/http/get_block_production.success.json +crates/ksp-onchain-transport-lib/fixtures/http/get_block_production.invalid_identity.json +deltas/0.2.4/pre.005.md +``` + +Aucun fichier n'est supprimé. + +## Validation attendue + +À exécuter après application de l'overlay : + +```bash +cargo fmt --all +cargo check --workspace +cargo clippy --workspace --all-targets +cargo test -p ksp-onchain-transport-lib +``` + +Les critères de fermeture sont : + +```text +aucun warning introduit +212 unit tests OK +23 public API tests OK +18 release-completeness tests OK +smoke Devnet ignoré comme prévu +0 échec +``` + +Le sandbox de préparation ne possède pas le toolchain Cargo/Rust ; aucune compilation locale n'est revendiquée par ce delta. + +## Commit attendu + +Après validation locale : + +```text +v0.2.4-pre.005 +``` + +## Suite + +La prerelease suivante reste : + +```text +0.2.4-pre.006 +``` + +Scope prévu : + +```text +réaudit SIMD-0298 / SIMD-0307 +getBlock moderne +bare encoding legacy +transactionDetails +wire bloc riche +SIMD-0118 numRewardPartitions +SIMD-0291 commissionBps +canary version transaction numérique non zéro / SIMD-0385 +```