diff --git a/Cargo.toml b/Cargo.toml index 88e3d76..a393b98 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ # file: Cargo.toml -# version: 134 +# version: 135 [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.3" +version = "0.2.4-pre.4" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" diff --git a/crates/ksp-onchain-transport-lib/fixtures/http/get_blocks.empty.json b/crates/ksp-onchain-transport-lib/fixtures/http/get_blocks.empty.json new file mode 100644 index 0000000..8661653 --- /dev/null +++ b/crates/ksp-onchain-transport-lib/fixtures/http/get_blocks.empty.json @@ -0,0 +1 @@ +{"jsonrpc":"2.0","result":[],"id":1} diff --git a/crates/ksp-onchain-transport-lib/fixtures/http/get_blocks.success.json b/crates/ksp-onchain-transport-lib/fixtures/http/get_blocks.success.json new file mode 100644 index 0000000..deea7fd --- /dev/null +++ b/crates/ksp-onchain-transport-lib/fixtures/http/get_blocks.success.json @@ -0,0 +1 @@ +{"jsonrpc":"2.0","result":[430000100,430000103,430000109],"id":1} diff --git a/crates/ksp-onchain-transport-lib/fixtures/http/get_blocks_with_limit.empty.json b/crates/ksp-onchain-transport-lib/fixtures/http/get_blocks_with_limit.empty.json new file mode 100644 index 0000000..8661653 --- /dev/null +++ b/crates/ksp-onchain-transport-lib/fixtures/http/get_blocks_with_limit.empty.json @@ -0,0 +1 @@ +{"jsonrpc":"2.0","result":[],"id":1} diff --git a/crates/ksp-onchain-transport-lib/fixtures/http/get_blocks_with_limit.success.json b/crates/ksp-onchain-transport-lib/fixtures/http/get_blocks_with_limit.success.json new file mode 100644 index 0000000..57017b6 --- /dev/null +++ b/crates/ksp-onchain-transport-lib/fixtures/http/get_blocks_with_limit.success.json @@ -0,0 +1 @@ +{"jsonrpc":"2.0","result":[430000200,430000201,430000205],"id":1} diff --git a/crates/ksp-onchain-transport-lib/fixtures/http/get_recent_performance_samples.success.json b/crates/ksp-onchain-transport-lib/fixtures/http/get_recent_performance_samples.success.json new file mode 100644 index 0000000..ad2bd4b --- /dev/null +++ b/crates/ksp-onchain-transport-lib/fixtures/http/get_recent_performance_samples.success.json @@ -0,0 +1 @@ +{"jsonrpc":"2.0","result":[{"slot":430000123,"numTransactions":250000,"numNonVoteTransactions":175000,"numSlots":120,"samplePeriodSecs":60},{"slot":430000000,"numTransactions":200000,"numSlots":110,"samplePeriodSecs":60}],"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 355b67d..d469a7b 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: 3 +// version: 4 /// Transaction detail level accepted by modern `getBlock` requests. #[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)] @@ -570,7 +570,6 @@ impl SolanaPerformanceSample { } /// Decodes one performance sample 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); return match decoded { @@ -622,6 +621,93 @@ impl crate::HttpTransportPool { return self.get_blocks_u64("minimumLedgerSlot", role, std::vec::Vec::new()).await; } + /// Executes typed `getBlocks` while preserving all four supported parameter overloads. + pub async fn get_blocks( + &self, + role: &crate::HttpRoleName, + start_slot: u64, + end_slot: std::option::Option, + config: std::option::Option<&crate::SolanaContextConfig>, + ) -> ksp_core_lib::Result> { + let validated = validate_blocks_context_commitment("getBlocks", config); + if let std::result::Result::Err(error) = validated { + return std::result::Result::Err(error); + } + if let std::option::Option::Some(end_slot) = end_slot + && end_slot >= start_slot + && end_slot - start_slot > MAX_GET_BLOCKS_RANGE + { + return std::result::Result::Err( + ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RPC_PARAMETERS, "getBlocks range must not exceed 500000 slots") + .with_context("rpc_method", "getBlocks") + .with_context("start_slot", start_slot.to_string()) + .with_context("end_slot", end_slot.to_string()), + ); + } + let mut params = std::vec![serde_json::json!(start_slot)]; + if let std::option::Option::Some(end_slot) = end_slot { + params.push(serde_json::json!(end_slot)); + } + push_blocks_explicit_context_config(&mut params, config); + return self.get_blocks_u64_list("getBlocks", role, params).await; + } + + /// Executes typed `getBlocksWithLimit` with the runtime-supported inclusive upper bound. + pub async fn get_blocks_with_limit( + &self, + role: &crate::HttpRoleName, + start_slot: u64, + limit: u64, + config: std::option::Option<&crate::SolanaContextConfig>, + ) -> ksp_core_lib::Result> { + let validated = validate_blocks_context_commitment("getBlocksWithLimit", config); + if let std::result::Result::Err(error) = validated { + return std::result::Result::Err(error); + } + if limit > MAX_GET_BLOCKS_RANGE { + return invalid_blocks_limit("getBlocksWithLimit", "getBlocksWithLimit limit must not exceed 500000", limit); + } + let mut params = std::vec![serde_json::json!(start_slot), serde_json::json!(limit)]; + push_blocks_explicit_context_config(&mut params, config); + return self.get_blocks_u64_list("getBlocksWithLimit", role, params).await; + } + + /// Executes typed `getRecentPerformanceSamples`, preserving runtime order and older sample shapes. + pub async fn get_recent_performance_samples( + &self, + role: &crate::HttpRoleName, + limit: std::option::Option, + ) -> ksp_core_lib::Result> { + if let std::option::Option::Some(limit) = limit + && limit > MAX_GET_RECENT_PERFORMANCE_SAMPLES + { + return invalid_blocks_limit("getRecentPerformanceSamples", "getRecentPerformanceSamples limit must not exceed 720", limit); + } + let mut params = std::vec::Vec::new(); + if let std::option::Option::Some(limit) = limit { + params.push(serde_json::json!(limit)); + } + let value = self.execute_blocks_rpc("getRecentPerformanceSamples", 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_performance_samples("getRecentPerformanceSamples", value); + } + + async fn get_blocks_u64_list( + &self, + method_name: &'static str, + role: &crate::HttpRoleName, + params: std::vec::Vec, + ) -> ksp_core_lib::Result> { + let value = self.execute_blocks_rpc(method_name, role, params).await; + return match value { + std::result::Result::Ok(value) => crate::decode_wire_json::>(method_name, value), + std::result::Result::Err(error) => std::result::Result::Err(error), + }; + } + async fn get_blocks_u64( &self, method_name: &'static str, @@ -650,6 +736,57 @@ impl crate::HttpTransportPool { } } +const MAX_GET_BLOCKS_RANGE: u64 = 500_000; +const MAX_GET_RECENT_PERFORMANCE_SAMPLES: u64 = 720; + +fn validate_blocks_context_commitment(method: &'static str, config: std::option::Option<&crate::SolanaContextConfig>) -> ksp_core_lib::Result<()> { + if let std::option::Option::Some(config) = config + && config.commitment() == std::option::Option::Some(crate::SolanaCommitment::Processed) + { + return std::result::Result::Err( + ksp_core_lib::Error::new( + crate::ERROR_CODE_INVALID_RPC_PARAMETERS, + "block-range commitment must be confirmed or finalized when explicitly provided", + ) + .with_context("rpc_method", method) + .with_context("commitment", "processed"), + ); + } + return std::result::Result::Ok(()); +} + +fn push_blocks_explicit_context_config(params: &mut std::vec::Vec, config: std::option::Option<&crate::SolanaContextConfig>) { + if let std::option::Option::Some(config) = config { + params.push((*config).to_json_value()); + } + return; +} + +fn invalid_blocks_limit(method: &'static str, message: &'static str, limit: u64) -> ksp_core_lib::Result { + return std::result::Result::Err( + ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RPC_PARAMETERS, message) + .with_context("rpc_method", method) + .with_context("limit", limit.to_string()), + ); +} + +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 { + std::result::Result::Ok(values) => values, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let mut samples = std::vec::Vec::with_capacity(values.len()); + for value in values { + let sample = crate::SolanaPerformanceSample::decode_wire(method, value); + match sample { + std::result::Result::Ok(sample) => samples.push(sample), + std::result::Result::Err(error) => return std::result::Result::Err(error), + } + } + return std::result::Result::Ok(samples); +} + fn push_blocks_context_config(params: &mut std::vec::Vec, config: std::option::Option<&crate::SolanaContextConfig>) { if let std::option::Option::Some(config) = config && (config.commitment().is_some() || config.min_context_slot().is_some()) @@ -802,7 +939,6 @@ struct WireConfirmedBlock { block_height: std::option::Option, } -#[cfg(test)] #[derive(serde::Deserialize)] #[serde(rename_all = "camelCase")] struct WirePerformanceSample { diff --git a/crates/ksp-onchain-transport-lib/tests/public_api.rs b/crates/ksp-onchain-transport-lib/tests/public_api.rs index aa087f0..c6ba0dd 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: 17 +// version: 18 //! Integration tests for the public `ksp-onchain-transport-lib` consumer contract. @@ -395,3 +395,10 @@ fn public_v0_2_4_pre_003_simple_block_wrappers_are_available_from_crate_root() { let _get_first_available_block = ksp_onchain_transport_lib::HttpTransportPool::get_first_available_block; let _minimum_ledger_slot = ksp_onchain_transport_lib::HttpTransportPool::minimum_ledger_slot; } + +#[test] +fn public_v0_2_4_pre_004_range_and_performance_wrappers_are_available_from_crate_root() { + let _get_blocks = ksp_onchain_transport_lib::HttpTransportPool::get_blocks; + 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; +} diff --git a/crates/ksp-onchain-transport-lib/tests/release_completeness.rs b/crates/ksp-onchain-transport-lib/tests/release_completeness.rs index b30b4cd..3096b24 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: 15 +// version: 16 //! Release-level completeness canaries for the staged HTTP wrapper sequence. @@ -496,3 +496,28 @@ fn release_v0_2_4_pre_003_simple_blocks_subset_is_exact_and_retry_safe() { assert_eq!(actual, expected); assert_eq!(actual.len(), 5); } + +#[test] +fn release_v0_2_4_pre_004_range_performance_subset_is_exact_and_retry_safe() { + let mut expected = std::vec!["getBlocks", "getBlocksWithLimit", "getRecentPerformanceSamples"]; + let mut actual = std::vec::Vec::new(); + for descriptor in ksp_onchain_transport_lib::current_http_rpc_methods() { + if descriptor.coverage_release() != ksp_onchain_transport_lib::HttpRpcCoverageRelease::V0_2_4 + || descriptor.category() != ksp_onchain_transport_lib::HttpRpcCategory::Blocks + { + continue; + } + match descriptor.method() { + "getBlocks" | "getBlocksWithLimit" | "getRecentPerformanceSamples" => { + assert_eq!(descriptor.operation_kind(), ksp_onchain_transport_lib::RpcOperationKind::Read); + assert_eq!(descriptor.transport_retry_class(), ksp_onchain_transport_lib::TransportRetryClass::RetrySafe); + actual.push(descriptor.method()); + }, + _ => {}, + } + } + actual.sort_unstable(); + expected.sort_unstable(); + assert_eq!(actual, expected); + assert_eq!(actual.len(), 3); +} 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 ca719da..bc43de3 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: 2 +// version: 3 #[test] fn transaction_details_and_get_block_config_preserve_all_modern_options() { @@ -311,3 +311,145 @@ async fn typed_minimum_ledger_slot_has_no_params() { assert_eq!(body["method"], serde_json::json!("minimumLedgerSlot")); assert_eq!(body["params"], serde_json::json!([])); } + +#[tokio::test(flavor = "current_thread")] +async fn typed_get_blocks_covers_all_four_overloads_and_preserves_server_order() { + let role = crate::HttpRoleName::new("default"); + let config = crate::SolanaContextConfig::new(std::option::Option::Some(crate::SolanaCommitment::Confirmed), std::option::Option::Some(429_999_999)); + let empty_config = crate::SolanaContextConfig::default(); + let cases = [ + (std::option::Option::None, std::option::Option::None, serde_json::json!([430000100])), + (std::option::Option::Some(430_000_109), std::option::Option::None, serde_json::json!([430000100, 430000109])), + ( + std::option::Option::None, + std::option::Option::Some(&config), + serde_json::json!([430000100,{"commitment":"confirmed","minContextSlot":429999999}]), + ), + ( + std::option::Option::Some(430_000_109), + std::option::Option::Some(&config), + serde_json::json!([430000100,430000109,{"commitment":"confirmed","minContextSlot":429999999}]), + ), + (std::option::Option::None, std::option::Option::Some(&empty_config), serde_json::json!([430000100, {}])), + ]; + for (end_slot, config, expected_params) in cases { + let (url, handle) = serve_once(include_str!("../fixtures/http/get_blocks.success.json")); + let pool = pool_for_url(url.as_str()); + let blocks = pool.get_blocks(&role, 430_000_100, end_slot, config).await.expect("getBlocks overload fixture must succeed"); + assert_eq!(blocks, std::vec![430_000_100, 430_000_103, 430_000_109]); + let request = handle.join().expect("fixture server must join"); + let body = request_body(request.as_str()); + assert_eq!(body["method"], serde_json::json!("getBlocks")); + assert_eq!(body["params"], expected_params); + } +} + +#[tokio::test(flavor = "current_thread")] +async fn typed_get_blocks_allows_reversed_and_boundary_ranges_but_rejects_oversized_before_io() { + let role = crate::HttpRoleName::new("default"); + let (url, handle) = serve_once(include_str!("../fixtures/http/get_blocks.empty.json")); + let pool = pool_for_url(url.as_str()); + let blocks = pool + .get_blocks(&role, 430_000_100, std::option::Option::Some(430_000_099), std::option::Option::None) + .await + .expect("reversed getBlocks range must remain a valid RPC request"); + assert!(blocks.is_empty()); + let request = handle.join().expect("fixture server must join"); + assert_eq!(request_body(request.as_str())["params"], serde_json::json!([430000100, 430000099])); + + let (url, handle) = serve_once(include_str!("../fixtures/http/get_blocks.success.json")); + let pool = pool_for_url(url.as_str()); + let blocks = pool + .get_blocks(&role, 10, std::option::Option::Some(500_010), std::option::Option::None) + .await + .expect("500000-slot getBlocks range must remain valid"); + assert_eq!(blocks, std::vec![430_000_100, 430_000_103, 430_000_109]); + let request = handle.join().expect("fixture server must join"); + assert_eq!(request_body(request.as_str())["params"], serde_json::json!([10, 500010])); + + let pool = pool_for_url("http://127.0.0.1:9"); + let result = pool.get_blocks(&role, 10, std::option::Option::Some(500_011), std::option::Option::None).await; + let error = result.expect_err("range above 500000 must reject before I/O"); + assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RPC_PARAMETERS); + assert_eq!(error.context()[1].key(), "start_slot"); + assert_eq!(error.context()[1].value(), "10"); + assert_eq!(error.context()[2].key(), "end_slot"); + assert_eq!(error.context()[2].value(), "500011"); +} + +#[tokio::test(flavor = "current_thread")] +async fn typed_block_range_wrappers_reject_processed_commitment_before_io() { + let role = crate::HttpRoleName::new("default"); + let pool = pool_for_url("http://127.0.0.1:9"); + let config = crate::SolanaContextConfig::new(std::option::Option::Some(crate::SolanaCommitment::Processed), std::option::Option::None); + let get_blocks = pool.get_blocks(&role, 1, std::option::Option::None, std::option::Option::Some(&config)).await; + let error = get_blocks.expect_err("getBlocks processed commitment must reject before I/O"); + assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RPC_PARAMETERS); + assert_eq!(error.context()[1].key(), "commitment"); + assert_eq!(error.context()[1].value(), "processed"); + + let get_blocks_with_limit = pool.get_blocks_with_limit(&role, 1, 1, std::option::Option::Some(&config)).await; + let error = get_blocks_with_limit.expect_err("getBlocksWithLimit processed commitment must reject before I/O"); + assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RPC_PARAMETERS); + assert_eq!(error.context()[1].key(), "commitment"); + assert_eq!(error.context()[1].value(), "processed"); +} + +#[tokio::test(flavor = "current_thread")] +async fn typed_get_blocks_with_limit_accepts_zero_and_maximum_and_rejects_above_maximum() { + let role = crate::HttpRoleName::new("default"); + let (url, handle) = serve_once(include_str!("../fixtures/http/get_blocks_with_limit.empty.json")); + let pool = pool_for_url(url.as_str()); + let blocks = pool.get_blocks_with_limit(&role, 430_000_200, 0, std::option::Option::None).await.expect("zero getBlocksWithLimit limit must remain valid"); + assert!(blocks.is_empty()); + let request = handle.join().expect("fixture server must join"); + assert_eq!(request_body(request.as_str())["params"], serde_json::json!([430000200, 0])); + + let config = crate::SolanaContextConfig::new(std::option::Option::Some(crate::SolanaCommitment::Finalized), std::option::Option::Some(430_000_000)); + let (url, handle) = serve_once(include_str!("../fixtures/http/get_blocks_with_limit.success.json")); + let pool = pool_for_url(url.as_str()); + let blocks = pool + .get_blocks_with_limit(&role, 430_000_200, 500_000, std::option::Option::Some(&config)) + .await + .expect("maximum getBlocksWithLimit limit must remain valid"); + assert_eq!(blocks, std::vec![430_000_200, 430_000_201, 430_000_205]); + let request = handle.join().expect("fixture server must join"); + assert_eq!(request_body(request.as_str())["params"], serde_json::json!([430000200,500000,{"commitment":"finalized","minContextSlot":430000000}])); + + let pool = pool_for_url("http://127.0.0.1:9"); + let result = pool.get_blocks_with_limit(&role, 1, 500_001, std::option::Option::None).await; + let error = result.expect_err("getBlocksWithLimit above 500000 must reject before I/O"); + assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RPC_PARAMETERS); + assert_eq!(error.context()[1].key(), "limit"); + assert_eq!(error.context()[1].value(), "500001"); +} + +#[tokio::test(flavor = "current_thread")] +async fn typed_get_recent_performance_samples_preserves_default_explicit_limit_order_and_older_shape() { + let role = crate::HttpRoleName::new("default"); + for (limit, expected_params) in [(std::option::Option::None, serde_json::json!([])), (std::option::Option::Some(720), serde_json::json!([720]))] { + let (url, handle) = serve_once(include_str!("../fixtures/http/get_recent_performance_samples.success.json")); + let pool = pool_for_url(url.as_str()); + let samples = pool.get_recent_performance_samples(&role, limit).await.expect("performance sample fixture must succeed"); + assert_eq!(samples.len(), 2); + assert_eq!(samples[0].slot(), 430_000_123); + assert_eq!(samples[0].num_transactions(), 250_000); + assert_eq!(samples[0].num_non_vote_transactions(), std::option::Option::Some(175_000)); + assert_eq!(samples[1].slot(), 430_000_000); + assert_eq!(samples[1].num_non_vote_transactions(), std::option::Option::None); + let request = handle.join().expect("fixture server must join"); + let body = request_body(request.as_str()); + assert_eq!(body["method"], serde_json::json!("getRecentPerformanceSamples")); + assert_eq!(body["params"], expected_params); + } +} + +#[tokio::test(flavor = "current_thread")] +async fn typed_get_recent_performance_samples_rejects_above_720_before_io() { + let pool = pool_for_url("http://127.0.0.1:9"); + let result = pool.get_recent_performance_samples(&crate::HttpRoleName::new("default"), std::option::Option::Some(721)).await; + let error = result.expect_err("performance sample limit above 720 must reject before I/O"); + assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RPC_PARAMETERS); + assert_eq!(error.context()[1].key(), "limit"); + assert_eq!(error.context()[1].value(), "721"); +} diff --git a/deltas/0.2.4/pre.004.md b/deltas/0.2.4/pre.004.md new file mode 100644 index 0000000..c6a7cd7 --- /dev/null +++ b/deltas/0.2.4/pre.004.md @@ -0,0 +1,423 @@ + + + +# Delta `0.2.4-pre.004` — ranges Blocks et performance samples + +## Base requise + +Livraison précédente : + +```text +0.2.4-pre.003 +workspace.package.version = "0.2.4-pre.3" +``` + +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 -> 202 unit tests OK + 21 public API tests OK + 16 release-completeness tests OK + 1 smoke Devnet ignoré comme prévu + 0 échec +``` + +Le plan canonique `docs/plans/011-V0_2_4_HTTP_BLOCKS_ECONOMICS_PLAN.md` passe de la version 2 à la version 3 dans cette livraison afin de corriger explicitement la config runtime stable de `getBlocks` / `getBlocksWithLimit` : Agave `v4.2.1` utilise `RpcContextConfig` avec `commitment` et `minContextSlot`. + +## Objectif + +Implémenter exactement la tranche `pre.004` prévue : + +```text +getBlocks +getBlocksWithLimit +getRecentPerformanceSamples +``` + +Les trois méthodes sont déjà enregistrées dans la partition `V0_2_4 / Blocks` et classées `Read / RetrySafe`. Cette prerelease ne modifie donc pas le registre central. + +Après cette tranche, huit des dix wrappers Blocks de `0.2.4` sont matérialisés : + +```text +pre.003 + getBlockCommitment + getBlockHeight + getBlockTime + getFirstAvailableBlock + minimumLedgerSlot + +pre.004 + getBlocks + getBlocksWithLimit + getRecentPerformanceSamples +``` + +Restent volontairement différés : + +```text +pre.005 getBlockProduction +pre.006 getBlock +``` + +Les cinq méthodes Economics restent également hors scope. + +## Réaudit RPC de la tranche + +Le contrat HTTP courant a été revérifié avant implémentation et reste conforme au plan `pre.001` : + +- `getBlocks` accepte `start_slot`, un second paramètre qui peut être soit `end_slot`, soit un `RpcContextConfig`, puis éventuellement ce même type de config en troisième position ; la config stable contient `commitment` et `minContextSlot`, et la plage maximale est de `500_000` slots ; +- `getBlocksWithLimit` accepte `start_slot`, `limit` puis un `RpcContextConfig` optionnel `{commitment,minContextSlot}` ; `limit` ne doit pas dépasser `500_000` ; +- `getRecentPerformanceSamples` accepte un `limit` optionnel avec maximum `720`; l'absence du paramètre laisse le runtime appliquer son défaut ; +- les résultats `getBlocks` / `getBlocksWithLimit` restent des listes ordonnées de slots `u64` ; +- `getRecentPerformanceSamples` reste une liste dans l'ordre fourni par le serveur et expose `slot`, `numTransactions`, `numSlots`, `samplePeriodSecs`, avec `numNonVoteTransactions` conservé comme champ optionnel pour compatibilité avec les anciens wires. + +Aucun nouvel overload ni nouvelle limite n'a été identifié. En revanche, le cross-audit source Agave a clarifié un champ stable que la page publique n'affiche pas : `minContextSlot` fait partie du `RpcContextConfig` accepté par `getBlocks` et `getBlocksWithLimit`. Le plan vivant est corrigé en conséquence sous sa version 3. + +## Version Cargo + +Nouvelle prerelease technique : + +```text +0.2.4-pre.3 -> 0.2.4-pre.4 +``` + +Aucune dépendance ni feature Cargo n'est ajoutée ou modifiée. + +## `getBlocks` + +Signature publique : + +```text +HttpTransportPool::get_blocks( + role, + start_slot, + Option, + Option<&SolanaContextConfig>, +) -> Result> +``` + +Le wrapper préserve les quatre formes retenues par `KSP-TRANSPORT-007` : + +```json +[430000100] +[430000100, 430000109] +[430000100, {"commitment":"confirmed", "minContextSlot":429999999}] +[430000100, 430000109, {"commitment":"confirmed", "minContextSlot":429999999}] +``` + +Un `Some(SolanaContextConfig::default())` reste un overload config explicite : + +```json +[430000100, {}] +``` + +Il n'est pas réécrit silencieusement en `[430000100]`. + +Validation déterministe avant I/O : + +```text +commitment = processed -> rejet +end >= start et end - start > 500_000 -> rejet +``` + +La borne est inclusive : + +```text +end - start = 500_000 -> valide +``` + +Le cas : + +```text +end < start +``` + +reste valide et est envoyé au runtime ; KSP accepte alors le tableau vide retourné sans fabriquer une erreur locale ni synthétiser une autre réponse. + +Le résultat `Vec` est conservé dans l'ordre exact retourné par le serveur ; aucun tri ni remplissage des slots absents n'est effectué. + +## `getBlocksWithLimit` + +Signature publique : + +```text +HttpTransportPool::get_blocks_with_limit( + role, + start_slot, + limit, + Option<&SolanaContextConfig>, +) -> Result> +``` + +Validation déterministe avant I/O : + +```text +commitment = processed -> rejet +limit > 500_000 -> rejet +``` + +Les deux bornes importantes sont couvertes explicitement : + +```text +limit = 0 -> valide +limit = 500_000 -> valide +limit = 500_001 -> rejet avant I/O +``` + +Aucune borne minimale artificielle `1` n'est ajoutée. + +Comme pour `getBlocks`, l'ordre et les éventuels trous de slots de la réponse sont laissés au runtime et préservés par Transport. + +## `getRecentPerformanceSamples` + +Signature publique : + +```text +HttpTransportPool::get_recent_performance_samples( + role, + Option, +) -> Result> +``` + +Wire sans limite explicite : + +```json +[] +``` + +Wire avec maximum explicite : + +```json +[720] +``` + +Validation déterministe : + +```text +limit <= 720 -> valide +limit > 720 -> rejet avant I/O +``` + +Le wrapper n'introduit aucune borne minimale locale et ne remplace pas l'absence de limite par une valeur synthétique `720` dans la requête. + +`SolanaPerformanceSample`, introduit en `pre.002`, est désormais consommé par un wrapper runtime. Son décodeur interne et `WirePerformanceSample` sortent donc de `#[cfg(test)]`. + +Le décodage conserve : + +```text +slot +numTransactions +numNonVoteTransactions présent ou omis +numSlots +samplePeriodSecs +``` + +L'ordre retourné par le serveur est conservé sans tri local. + +## Discipline `#[cfg(test)]` + +La règle introduite par `pre.002-fix.001` reste appliquée strictement. + +Nouveaux éléments devenus runtime en `pre.004` : + +```text +SolanaPerformanceSample::decode_wire +WirePerformanceSample +``` + +Éléments déjà runtime en `pre.003` : + +```text +SolanaBlockCommitment::decode_wire +WireBlockCommitment +``` + +Restent test-only jusqu'à leur tranche : + +```text +SolanaGetBlockConfig::{is_empty,to_json_value} +SolanaBlockProductionRange::to_json_value +SolanaBlockProductionConfig::{is_empty,to_json_value} +SolanaBlockProduction::decode_wire +SolanaBlockReward::decode_wire +SolanaBlockTransaction::decode_wire +SolanaConfirmedBlock::decode_wire +helpers privés transaction/reward de bloc +WireBlockProduction* +WireBlockReward +WireBlockTransaction +WireConfirmedBlock +helpers/wires Economics +``` + +Aucun `#[allow(dead_code)]` n'est introduit. + +## Fixtures HTTP ajoutées + +```text +crates/ksp-onchain-transport-lib/fixtures/http/get_blocks.success.json +crates/ksp-onchain-transport-lib/fixtures/http/get_blocks.empty.json +crates/ksp-onchain-transport-lib/fixtures/http/get_blocks_with_limit.success.json +crates/ksp-onchain-transport-lib/fixtures/http/get_blocks_with_limit.empty.json +crates/ksp-onchain-transport-lib/fixtures/http/get_recent_performance_samples.success.json +``` + +Les réponses de blocs utilisent volontairement des slots non continus afin de prouver que Transport ne synthétise pas les slots absents. + +La fixture performance contient un échantillon courant avec `numNonVoteTransactions` et un échantillon compatible ancien wire sans ce champ. + +## Tests unitaires ajoutés + +`unit_tests/rpc_blocks.rs` ajoute six tests async : + +```text +typed_get_blocks_covers_all_four_overloads_and_preserves_server_order +typed_get_blocks_allows_reversed_and_boundary_ranges_but_rejects_oversized_before_io +typed_block_range_wrappers_reject_processed_commitment_before_io +typed_get_blocks_with_limit_accepts_zero_and_maximum_and_rejects_above_maximum +typed_get_recent_performance_samples_preserves_default_explicit_limit_order_and_older_shape +typed_get_recent_performance_samples_rejects_above_720_before_io +``` + +Ils couvrent notamment : + +```text +getBlocks [start] +getBlocks [start,end] +getBlocks [start,config] +getBlocks [start,end,config] +getBlocks [start,{}] +getBlocks end < start +getBlocks différence = 500_000 +autorejet getBlocks différence = 500_001 +commitment processed rejeté avant I/O +getBlocksWithLimit 0 +getBlocksWithLimit 500_000 +getBlocksWithLimit 500_001 rejeté avant I/O +getRecentPerformanceSamples sans limite +getRecentPerformanceSamples 720 +getRecentPerformanceSamples 721 rejeté avant I/O +ordre des résultats conservé +ancien sample sans numNonVoteTransactions accepté +``` + +## Canaries d'intégration + +`tests/public_api.rs` ajoute : + +```text +public_v0_2_4_pre_004_range_and_performance_wrappers_are_available_from_crate_root +``` + +La canarie référence directement : + +```text +HttpTransportPool::get_blocks +HttpTransportPool::get_blocks_with_limit +HttpTransportPool::get_recent_performance_samples +``` + +`tests/release_completeness.rs` ajoute : + +```text +release_v0_2_4_pre_004_range_performance_subset_is_exact_and_retry_safe +``` + +Elle verrouille le sous-ensemble exact : + +```text +getBlocks +getBlocksWithLimit +getRecentPerformanceSamples +``` + +et leur classification commune : + +```text +HttpRpcCoverageRelease::V0_2_4 +HttpRpcCategory::Blocks +RpcOperationKind::Read +TransportRetryClass::RetrySafe +``` + +## Frontières architecturales + +Cette tranche ne modifie pas les frontières établies : + +```text +Transport -X-> Config +Transport -X-> Store +Transport -X-> Program +Transport -X-> tracing direct +``` + +Les wrappers passent exclusivement par `execute_standard_rpc` et la politique centrale de retry. + +Aucun client RPC Solana haut niveau, aucun retry local, aucune lecture d'environnement/configuration et aucune dépendance externe nouvelle ne sont introduits. + +## 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 +docs/plans/011-V0_2_4_HTTP_BLOCKS_ECONOMICS_PLAN.md +``` + +## Fichiers ajoutés + +```text +crates/ksp-onchain-transport-lib/fixtures/http/get_blocks.success.json +crates/ksp-onchain-transport-lib/fixtures/http/get_blocks.empty.json +crates/ksp-onchain-transport-lib/fixtures/http/get_blocks_with_limit.success.json +crates/ksp-onchain-transport-lib/fixtures/http/get_blocks_with_limit.empty.json +crates/ksp-onchain-transport-lib/fixtures/http/get_recent_performance_samples.success.json +deltas/0.2.4/pre.004.md +``` + +Aucun fichier n'est supprimé. + +## Validation à exécuter après application + +```bash +cargo fmt --all +cargo check --workspace +cargo clippy --workspace --all-targets +cargo test -p ksp-onchain-transport-lib +``` + +Compteurs attendus si aucun test existant n'est ajouté/supprimé localement entre-temps : + +```text +208 unit tests +22 public API tests +17 release-completeness tests +1 smoke Devnet ignoré comme prévu +0 échec +``` + +Le sandbox de génération ne possède pas le toolchain Rust ; aucune compilation locale n'est revendiquée dans ce delta. + +## Suite + +La tranche suivante reste celle prévue par le plan : + +```text +0.2.4-pre.005 +getBlockProduction +``` + +Elle devra activer uniquement les helpers runtime nécessaires à `SolanaBlockProductionConfig`, `SolanaBlockProductionRange` et `SolanaBlockProduction`, puis couvrir `identity`, `range`, le résultat contextualisé et la validation `lastSlot >= firstSlot`. + +## Commit attendu + +Après validation locale réussie : + +```text +v0.2.4-pre.004 +``` diff --git a/docs/plans/011-V0_2_4_HTTP_BLOCKS_ECONOMICS_PLAN.md b/docs/plans/011-V0_2_4_HTTP_BLOCKS_ECONOMICS_PLAN.md index cc6ff2b..01f750a 100644 --- a/docs/plans/011-V0_2_4_HTTP_BLOCKS_ECONOMICS_PLAN.md +++ b/docs/plans/011-V0_2_4_HTTP_BLOCKS_ECONOMICS_PLAN.md @@ -1,5 +1,5 @@ - + # Plan `0.2.4` — HTTP Blocks + Economics + compliance HTTP finale @@ -221,13 +221,26 @@ Les SIMDs `Review` liés au block-revenue sharing, notamment SIMD-0123, peuvent | `getBlockCommitment` | `slot` uniquement | `{commitment: array ou null, totalStake: u64}` | aucune config à inventer; préserver commitment nullable | | `getBlockHeight` | config contextuelle optionnelle `{commitment, minContextSlot}` | `u64` | réutiliser `SolanaContextConfig`; `minContextSlot` est transmis tel quel au runtime | | `getBlockProduction` | config optionnelle `{commitment, identity, range:{firstSlot,lastSlot?}}` | `SolanaRpcResponse<{byIdentity, range}>` | `identity` typée Pubkey; validation locale déterministe `lastSlot >= firstSlot` lorsque les deux sont fournis; préserver les couples `[leaderSlots, blocksProduced]` | -| `getBlocks` | `[start]`, `[start,end]`, `[start,config]`, `[start,end,config]` | `Vec` | commitment au moins `confirmed`; conserver l'overload untagged du second paramètre; `end < start` donne `[]`; rejeter localement une différence `end-start > 500_000` | -| `getBlocksWithLimit` | `start`, `limit`, config contextuelle optionnelle | `Vec` | commitment au moins `confirmed`; `limit <= 500_000`; `limit = 0` est valide et donne `[]` | +| `getBlocks` | `[start]`, `[start,end]`, `[start,config]`, `[start,end,config]`; config `{commitment,minContextSlot}` | `Vec` | commitment au moins `confirmed`; transmettre `minContextSlot`; conserver l'overload untagged du second paramètre; `end < start` donne `[]`; rejeter localement une différence `end-start > 500_000` | +| `getBlocksWithLimit` | `start`, `limit`, config contextuelle optionnelle `{commitment,minContextSlot}` | `Vec` | commitment au moins `confirmed`; transmettre `minContextSlot`; `limit <= 500_000`; `limit = 0` est valide et donne `[]` | | `getBlockTime` | `slot` uniquement | `i64 ou null` | préserver l'absence de timestamp; les cas cleaned/skipped/not available restent des erreurs RPC runtime, pas des valeurs synthétiques | | `getFirstAvailableBlock` | aucun paramètre | `u64` | requête exacte `params: []`; aucune config | | `getRecentPerformanceSamples` | `limit?` | tableau de `{slot,numTransactions,numSlots,samplePeriodSecs,numNonVoteTransactions?}` | défaut runtime `720`; maximum `720`; rejeter avant I/O `limit > 720`; préserver `numNonVoteTransactions` nullable/ancien runtime | | `minimumLedgerSlot` | aucun paramètre | `u64` | requête exacte `params: []`; conserver les erreurs ledger/runtime | +### Clarification runtime `pre.004` — config des ranges Blocks + +Le réaudit d’implémentation de `pre.004` confirme dans Agave `v4.2.1` que `getBlocks` et `getBlocksWithLimit` consomment tous deux `RpcContextConfig`, et non un simple objet commitment. Le wrapper `RpcBlocksConfigWrapper` permet en plus à `getBlocks` de recevoir soit `end_slot`, soit cette config en deuxième position. + +KSP doit donc réutiliser `SolanaContextConfig` et préserver les deux champs stables : + +```text +commitment +minContextSlot +``` + +La documentation publique n’affiche actuellement que `commitment` sur ces pages ; la source runtime stable prime ici conformément à `KSP-TRANSPORT-007`. + ### `getBlock` — stratégie wire `getBlock` est le plus gros fil de la release et reçoit une prerelease dédiée. @@ -378,9 +391,9 @@ getBlock result null getBlock numRewardPartitions présent/omis (SIMD-0118) getBlock rewards commission nullable + commissionBps présent/omis (SIMD-0291) getBlock transaction version numérique non nulle conservée (canary SIMD-0385) -getBlocks [start,config] vs [start,end,config] +getBlocks [start,config] vs [start,end,config] + minContextSlot transmis getBlocks range > 500_000 rejetée avant I/O -getBlocksWithLimit 0 et > 500_000 +getBlocksWithLimit 0 et > 500_000 + minContextSlot transmis getRecentPerformanceSamples défaut/720/>720 getBlockProduction lastSlot < firstSlot rejeté avant I/O getInflationReward ordre + null positionnels + commission/commissionBps présent/omis (SIMD-0291)