diff --git a/Cargo.toml b/Cargo.toml index 9dd3710..e2a5fd2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ # file: Cargo.toml -# version: 136 +# version: 137 [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.5" +version = "0.2.4-pre.6" 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.accounts.success.json b/crates/ksp-onchain-transport-lib/fixtures/http/get_block.accounts.success.json new file mode 100644 index 0000000..ce28973 --- /dev/null +++ b/crates/ksp-onchain-transport-lib/fixtures/http/get_block.accounts.success.json @@ -0,0 +1 @@ +{"jsonrpc":"2.0","result":{"previousBlockhash":"previous-blockhash-fixture","blockhash":"blockhash-fixture","parentSlot":430000122,"numRewardPartitions":4,"blockTime":1787072400,"blockHeight":410000000,"transactions":[{"transaction":{"signatures":["sig-accounts"],"accountKeys":[{"pubkey":"11111111111111111111111111111111","writable":true,"signer":true,"source":"transaction"}]},"meta":{"err":null,"status":{"Ok":null},"fee":5000,"preBalances":[10],"postBalances":[5]},"version":1}]},"id":1} diff --git a/crates/ksp-onchain-transport-lib/fixtures/http/get_block.base58.success.json b/crates/ksp-onchain-transport-lib/fixtures/http/get_block.base58.success.json new file mode 100644 index 0000000..8e1342a --- /dev/null +++ b/crates/ksp-onchain-transport-lib/fixtures/http/get_block.base58.success.json @@ -0,0 +1 @@ +{"jsonrpc":"2.0","result":{"previousBlockhash":"previous-blockhash-fixture","blockhash":"blockhash-fixture","parentSlot":430000122,"rewards":[{"pubkey":"11111111111111111111111111111111","lamports":2500,"postBalance":1000002500,"rewardType":"Fee","commission":null,"commissionBps":1234},{"pubkey":"Vote111111111111111111111111111111111111111","lamports":1250,"postBalance":2000001250,"rewardType":"Voting","commission":5}],"numRewardPartitions":4,"blockTime":1787072400,"blockHeight":410000000,"transactions":[{"transaction":["3BxsFixtureBase58","base58"],"meta":{"err":null,"fee":5000},"version":"legacy"}]},"id":1} diff --git a/crates/ksp-onchain-transport-lib/fixtures/http/get_block.base64.success.json b/crates/ksp-onchain-transport-lib/fixtures/http/get_block.base64.success.json new file mode 100644 index 0000000..87440de --- /dev/null +++ b/crates/ksp-onchain-transport-lib/fixtures/http/get_block.base64.success.json @@ -0,0 +1 @@ +{"jsonrpc":"2.0","result":{"previousBlockhash":"previous-blockhash-fixture","blockhash":"blockhash-fixture","parentSlot":430000122,"rewards":[{"pubkey":"11111111111111111111111111111111","lamports":2500,"postBalance":1000002500,"rewardType":"Fee","commission":null,"commissionBps":1234},{"pubkey":"Vote111111111111111111111111111111111111111","lamports":1250,"postBalance":2000001250,"rewardType":"Voting","commission":5}],"numRewardPartitions":4,"blockTime":1787072400,"blockHeight":410000000,"transactions":[{"transaction":["AQID","base64"],"meta":{"err":null,"fee":5000},"version":"legacy"}]},"id":1} diff --git a/crates/ksp-onchain-transport-lib/fixtures/http/get_block.binary.success.json b/crates/ksp-onchain-transport-lib/fixtures/http/get_block.binary.success.json new file mode 100644 index 0000000..9ce1d8e --- /dev/null +++ b/crates/ksp-onchain-transport-lib/fixtures/http/get_block.binary.success.json @@ -0,0 +1 @@ +{"jsonrpc":"2.0","result":{"previousBlockhash":"previous-blockhash-fixture","blockhash":"blockhash-fixture","parentSlot":430000122,"rewards":[{"pubkey":"11111111111111111111111111111111","lamports":2500,"postBalance":1000002500,"rewardType":"Fee","commission":null,"commissionBps":1234},{"pubkey":"Vote111111111111111111111111111111111111111","lamports":1250,"postBalance":2000001250,"rewardType":"Voting","commission":5}],"numRewardPartitions":4,"blockTime":1787072400,"blockHeight":410000000,"transactions":[{"transaction":"3BxsFixtureLegacyBinary","meta":{"err":null,"fee":5000},"version":"legacy"}]},"id":1} diff --git a/crates/ksp-onchain-transport-lib/fixtures/http/get_block.error_block_not_available.json b/crates/ksp-onchain-transport-lib/fixtures/http/get_block.error_block_not_available.json new file mode 100644 index 0000000..8629568 --- /dev/null +++ b/crates/ksp-onchain-transport-lib/fixtures/http/get_block.error_block_not_available.json @@ -0,0 +1 @@ +{"jsonrpc":"2.0","error":{"code":-32004,"message":"Block not available for slot 430000123"},"id":1} diff --git a/crates/ksp-onchain-transport-lib/fixtures/http/get_block.error_unsupported_version.json b/crates/ksp-onchain-transport-lib/fixtures/http/get_block.error_unsupported_version.json new file mode 100644 index 0000000..67b4755 --- /dev/null +++ b/crates/ksp-onchain-transport-lib/fixtures/http/get_block.error_unsupported_version.json @@ -0,0 +1 @@ +{"jsonrpc":"2.0","error":{"code":-32015,"message":"Transaction version (1) is not supported by the requesting client. Please try the request again with maxSupportedTransactionVersion 1"},"id":1} diff --git a/crates/ksp-onchain-transport-lib/fixtures/http/get_block.full.success.json b/crates/ksp-onchain-transport-lib/fixtures/http/get_block.full.success.json new file mode 100644 index 0000000..1295d46 --- /dev/null +++ b/crates/ksp-onchain-transport-lib/fixtures/http/get_block.full.success.json @@ -0,0 +1 @@ +{"jsonrpc":"2.0","result":{"previousBlockhash":"previous-blockhash-fixture","blockhash":"blockhash-fixture","parentSlot":430000122,"rewards":[{"pubkey":"11111111111111111111111111111111","lamports":2500,"postBalance":1000002500,"rewardType":"Fee","commission":null,"commissionBps":1234},{"pubkey":"Vote111111111111111111111111111111111111111","lamports":1250,"postBalance":2000001250,"rewardType":"Voting","commission":5}],"numRewardPartitions":4,"blockTime":1787072400,"blockHeight":410000000,"transactions":[{"transaction":{"signatures":["sig-v1"],"message":{"accountKeys":[],"recentBlockhash":"recent-v1","instructions":[],"transactionConfig":{"priorityFee":1,"computeUnitLimit":200000}}},"meta":{"err":null,"fee":5000,"status":{"Ok":null},"rewards":[{"pubkey":"11111111111111111111111111111111","lamports":1,"postBalance":2,"rewardType":"Fee","commission":null,"commissionBps":1234}]},"version":1},{"transaction":["AQID","base64"],"meta":null}]},"id":1} diff --git a/crates/ksp-onchain-transport-lib/fixtures/http/get_block.full_no_rewards.success.json b/crates/ksp-onchain-transport-lib/fixtures/http/get_block.full_no_rewards.success.json new file mode 100644 index 0000000..aa67a48 --- /dev/null +++ b/crates/ksp-onchain-transport-lib/fixtures/http/get_block.full_no_rewards.success.json @@ -0,0 +1 @@ +{"jsonrpc":"2.0","result":{"previousBlockhash":"previous-blockhash-fixture","blockhash":"blockhash-fixture","parentSlot":430000122,"numRewardPartitions":4,"blockTime":1787072400,"blockHeight":410000000,"transactions":[{"transaction":{"signatures":["sig-full-no-rewards"],"message":{"accountKeys":[],"recentBlockhash":"recent-full-no-rewards","instructions":[]}},"meta":{"err":null,"fee":5000,"rewards":null},"version":1}]},"id":1} diff --git a/crates/ksp-onchain-transport-lib/fixtures/http/get_block.json.success.json b/crates/ksp-onchain-transport-lib/fixtures/http/get_block.json.success.json new file mode 100644 index 0000000..e5d6be1 --- /dev/null +++ b/crates/ksp-onchain-transport-lib/fixtures/http/get_block.json.success.json @@ -0,0 +1 @@ +{"jsonrpc":"2.0","result":{"previousBlockhash":"previous-blockhash-fixture","blockhash":"blockhash-fixture","parentSlot":430000122,"rewards":[{"pubkey":"11111111111111111111111111111111","lamports":2500,"postBalance":1000002500,"rewardType":"Fee","commission":null,"commissionBps":1234},{"pubkey":"Vote111111111111111111111111111111111111111","lamports":1250,"postBalance":2000001250,"rewardType":"Voting","commission":5}],"numRewardPartitions":4,"blockTime":1787072400,"blockHeight":410000000,"transactions":[{"transaction":{"signatures":["sig-json"],"message":{"accountKeys":[],"recentBlockhash":"recent-json","instructions":[]}},"meta":{"err":null,"fee":5000},"version":"legacy"}]},"id":1} diff --git a/crates/ksp-onchain-transport-lib/fixtures/http/get_block.json_parsed.success.json b/crates/ksp-onchain-transport-lib/fixtures/http/get_block.json_parsed.success.json new file mode 100644 index 0000000..f535b81 --- /dev/null +++ b/crates/ksp-onchain-transport-lib/fixtures/http/get_block.json_parsed.success.json @@ -0,0 +1 @@ +{"jsonrpc":"2.0","result":{"previousBlockhash":"previous-blockhash-fixture","blockhash":"blockhash-fixture","parentSlot":430000122,"rewards":[{"pubkey":"11111111111111111111111111111111","lamports":2500,"postBalance":1000002500,"rewardType":"Fee","commission":null,"commissionBps":1234},{"pubkey":"Vote111111111111111111111111111111111111111","lamports":1250,"postBalance":2000001250,"rewardType":"Voting","commission":5}],"numRewardPartitions":4,"blockTime":1787072400,"blockHeight":410000000,"transactions":[{"transaction":{"signatures":["sig-parsed"],"message":{"accountKeys":[],"recentBlockhash":"recent-parsed","instructions":[]}},"meta":{"err":null,"fee":5000},"version":"legacy"}]},"id":1} diff --git a/crates/ksp-onchain-transport-lib/fixtures/http/get_block.none.success.json b/crates/ksp-onchain-transport-lib/fixtures/http/get_block.none.success.json new file mode 100644 index 0000000..39da8dc --- /dev/null +++ b/crates/ksp-onchain-transport-lib/fixtures/http/get_block.none.success.json @@ -0,0 +1 @@ +{"jsonrpc":"2.0","result":{"previousBlockhash":"previous-blockhash-fixture","blockhash":"blockhash-fixture","parentSlot":430000122,"numRewardPartitions":4,"blockTime":1787072400,"blockHeight":410000000},"id":1} diff --git a/crates/ksp-onchain-transport-lib/fixtures/http/get_block.null.json b/crates/ksp-onchain-transport-lib/fixtures/http/get_block.null.json new file mode 100644 index 0000000..e00e008 --- /dev/null +++ b/crates/ksp-onchain-transport-lib/fixtures/http/get_block.null.json @@ -0,0 +1 @@ +{"jsonrpc":"2.0","result":null,"id":1} diff --git a/crates/ksp-onchain-transport-lib/fixtures/http/get_block.null_fields.success.json b/crates/ksp-onchain-transport-lib/fixtures/http/get_block.null_fields.success.json new file mode 100644 index 0000000..d789c00 --- /dev/null +++ b/crates/ksp-onchain-transport-lib/fixtures/http/get_block.null_fields.success.json @@ -0,0 +1 @@ +{"jsonrpc":"2.0","result":{"previousBlockhash":"previous-b","blockhash":"block-b","parentSlot":2,"transactions":null,"signatures":null,"rewards":null,"numRewardPartitions":null,"blockTime":null,"blockHeight":null},"id":1} diff --git a/crates/ksp-onchain-transport-lib/fixtures/http/get_block.omitted_fields.success.json b/crates/ksp-onchain-transport-lib/fixtures/http/get_block.omitted_fields.success.json new file mode 100644 index 0000000..ac3b56a --- /dev/null +++ b/crates/ksp-onchain-transport-lib/fixtures/http/get_block.omitted_fields.success.json @@ -0,0 +1 @@ +{"jsonrpc":"2.0","result":{"previousBlockhash":"previous-a","blockhash":"block-a","parentSlot":1,"blockTime":null,"blockHeight":null},"id":1} diff --git a/crates/ksp-onchain-transport-lib/fixtures/http/get_block.signatures.success.json b/crates/ksp-onchain-transport-lib/fixtures/http/get_block.signatures.success.json new file mode 100644 index 0000000..1296c74 --- /dev/null +++ b/crates/ksp-onchain-transport-lib/fixtures/http/get_block.signatures.success.json @@ -0,0 +1 @@ +{"jsonrpc":"2.0","result":{"previousBlockhash":"previous-blockhash-fixture","blockhash":"blockhash-fixture","parentSlot":430000122,"numRewardPartitions":4,"blockTime":1787072400,"blockHeight":410000000,"signatures":["sig-a","sig-b"]},"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 c198ee4..a6e78db 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: 5 +// version: 6 /// Transaction detail level accepted by modern `getBlock` requests. #[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)] @@ -93,7 +93,6 @@ impl SolanaGetBlockConfig { /// 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 { @@ -354,7 +353,6 @@ impl SolanaBlockReward { return &self.commission_bps; } - #[cfg(test)] 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 { @@ -403,7 +401,6 @@ impl SolanaBlockTransaction { return &self.version; } - #[cfg(test)] 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 { @@ -493,7 +490,6 @@ impl SolanaConfirmedBlock { } /// Decodes a confirmed block 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 { @@ -582,6 +578,74 @@ impl SolanaPerformanceSample { } impl crate::HttpTransportPool { + /// Executes the current object-form `getBlock` request through the common KSP HTTP transport path. + /// + /// `None` omits the optional second parameter. `Some(config)` sends the modern object form exactly, including an empty `{}` when the caller + /// explicitly supplies an empty modern configuration. The runtime requires an explicitly supplied commitment to be at least `confirmed`. + pub async fn get_block( + &self, + role: &crate::HttpRoleName, + slot: u64, + config: std::option::Option<&crate::SolanaGetBlockConfig>, + ) -> 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, + "getBlock commitment must be confirmed or finalized when explicitly provided", + ) + .with_context("rpc_method", "getBlock") + .with_context("commitment", "processed"), + ); + } + let mut params = std::vec![serde_json::json!(slot)]; + if let std::option::Option::Some(config) = config { + params.push((*config).to_json_value()); + } + return self.execute_get_block(role, params).await; + } + + /// Executes the deprecated bare-encoding `getBlock` request form retained by Solana RPC for backwards compatibility. + #[deprecated(note = "use HttpTransportPool::get_block with SolanaGetBlockConfig; the bare encoding request form is deprecated")] + pub async fn get_block_legacy( + &self, + role: &crate::HttpRoleName, + slot: u64, + encoding: crate::SolanaTransactionEncoding, + ) -> ksp_core_lib::Result> { + ksp_logging_lib::warn!( + target: crate::TRACING_TARGET, + rpc_method = "getBlock", + request_form = "bare_encoding", + encoding = encoding.as_str(), + "deprecated Solana HTTP RPC request form used" + ); + let params = std::vec![serde_json::json!(slot), serde_json::Value::String(encoding.as_str().to_owned())]; + return self.execute_get_block(role, params).await; + } + + async fn execute_get_block( + &self, + role: &crate::HttpRoleName, + params: std::vec::Vec, + ) -> ksp_core_lib::Result> { + let value = self.execute_blocks_rpc("getBlock", role, params).await; + let value = match value { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + if value.is_null() { + return std::result::Result::Ok(std::option::Option::None); + } + let block = crate::SolanaConfirmedBlock::decode_wire("getBlock", value); + return match block { + std::result::Result::Ok(block) => std::result::Result::Ok(std::option::Option::Some(block)), + std::result::Result::Err(error) => std::result::Result::Err(error), + }; + } + /// Executes typed `getBlockCommitment` through the common KSP HTTP transport path. pub async fn get_block_commitment(&self, role: &crate::HttpRoleName, slot: u64) -> ksp_core_lib::Result { let value = self.execute_blocks_rpc("getBlockCommitment", role, std::vec![serde_json::json!(slot)]).await; @@ -857,7 +921,6 @@ fn blocks_descriptor(method: &str) -> ksp_core_lib::Result<&'static crate::HttpR }; } -#[cfg(test)] fn decode_block_transaction_version( method: &str, field: crate::SolanaWireField, @@ -875,7 +938,6 @@ fn decode_block_transaction_version( }; } -#[cfg(test)] fn decode_block_transactions( method: &str, field: crate::SolanaWireField>, @@ -896,7 +958,6 @@ fn decode_block_transactions( return std::result::Result::Ok(crate::SolanaWireField::Value(transactions)); } -#[cfg(test)] fn decode_block_rewards( method: &str, field: crate::SolanaWireField>, @@ -944,7 +1005,6 @@ struct WireBlockProductionRpcResponse { value: serde_json::Value, } -#[cfg(test)] #[derive(serde::Deserialize)] #[serde(rename_all = "camelCase")] struct WireBlockReward { @@ -959,7 +1019,6 @@ struct WireBlockReward { commission_bps: crate::SolanaWireField, } -#[cfg(test)] #[derive(serde::Deserialize)] #[serde(rename_all = "camelCase")] struct WireBlockTransaction { @@ -970,7 +1029,6 @@ struct WireBlockTransaction { version: crate::SolanaWireField, } -#[cfg(test)] #[derive(serde::Deserialize)] #[serde(rename_all = "camelCase")] struct WireConfirmedBlock { diff --git a/crates/ksp-onchain-transport-lib/tests/public_api.rs b/crates/ksp-onchain-transport-lib/tests/public_api.rs index 7cdb0f9..8e3146b 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: 19 +// version: 20 //! Integration tests for the public `ksp-onchain-transport-lib` consumer contract. @@ -416,3 +416,22 @@ fn public_v0_2_4_pre_005_block_production_wrapper_is_available_from_crate_root() 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; } + +#[allow(deprecated)] +#[test] +fn public_v0_2_4_pre_006_get_block_complete_request_forms_are_available_from_crate_root() { + let _get_block = ksp_onchain_transport_lib::HttpTransportPool::get_block; + let _get_block_legacy = ksp_onchain_transport_lib::HttpTransportPool::get_block_legacy; + let config = ksp_onchain_transport_lib::SolanaGetBlockConfig::new( + std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Confirmed), + std::option::Option::Some(ksp_onchain_transport_lib::SolanaTransactionEncoding::JsonParsed), + std::option::Option::Some(ksp_onchain_transport_lib::SolanaTransactionDetails::Accounts), + std::option::Option::Some(1), + std::option::Option::Some(true), + ); + assert_eq!(config.commitment(), std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Confirmed)); + assert_eq!(config.encoding(), std::option::Option::Some(ksp_onchain_transport_lib::SolanaTransactionEncoding::JsonParsed)); + assert_eq!(config.transaction_details(), std::option::Option::Some(ksp_onchain_transport_lib::SolanaTransactionDetails::Accounts)); + assert_eq!(config.max_supported_transaction_version(), std::option::Option::Some(1)); + assert_eq!(config.rewards(), std::option::Option::Some(true)); +} diff --git a/crates/ksp-onchain-transport-lib/tests/release_completeness.rs b/crates/ksp-onchain-transport-lib/tests/release_completeness.rs index 2ddcf0f..f93b8a7 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: 17 +// version: 18 //! Release-level completeness canaries for the staged HTTP wrapper sequence. @@ -530,3 +530,36 @@ fn release_v0_2_4_pre_005_block_production_subset_is_exact_and_retry_safe() { assert_eq!(descriptor.operation_kind(), ksp_onchain_transport_lib::RpcOperationKind::Read); assert_eq!(descriptor.transport_retry_class(), ksp_onchain_transport_lib::TransportRetryClass::RetrySafe); } + +#[test] +fn release_v0_2_4_pre_006_get_block_completes_all_ten_block_wrappers_with_legacy_marker() { + let expected = std::vec![ + "getBlock", + "getBlockCommitment", + "getBlockHeight", + "getBlockProduction", + "getBlocks", + "getBlocksWithLimit", + "getBlockTime", + "getFirstAvailableBlock", + "getRecentPerformanceSamples", + "minimumLedgerSlot", + ]; + 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 + { + 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(); + let mut expected = expected; + expected.sort_unstable(); + assert_eq!(actual, expected); + assert_eq!(actual.len(), 10); + let get_block = ksp_onchain_transport_lib::find_http_rpc_method("getBlock").expect("getBlock descriptor must exist"); + assert!(get_block.request_form_status().has_deprecated_legacy()); +} 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 f44d29a..cd474ff 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: 4 +// version: 5 #[test] fn transaction_details_and_get_block_config_preserve_all_modern_options() { @@ -535,3 +535,259 @@ async fn typed_get_block_production_rejects_invalid_wire_identity_without_echoin assert!(!error.to_string().contains("not-a-pubkey")); handle.join().expect("fixture server must join"); } + +#[tokio::test(flavor = "current_thread")] +async fn typed_get_block_modern_full_config_preserves_rich_wire_and_simd_fields() { + let config = crate::SolanaGetBlockConfig::new( + std::option::Option::Some(crate::SolanaCommitment::Finalized), + std::option::Option::Some(crate::SolanaTransactionEncoding::JsonParsed), + std::option::Option::Some(crate::SolanaTransactionDetails::Full), + std::option::Option::Some(1), + std::option::Option::Some(true), + ); + let (url, handle) = serve_once(include_str!("../fixtures/http/get_block.full.success.json")); + let pool = pool_for_url(url.as_str()); + let block = pool + .get_block(&crate::HttpRoleName::new("default"), 430_000_123, std::option::Option::Some(&config)) + .await + .expect("modern getBlock fixture must succeed") + .expect("modern getBlock fixture must contain a block"); + assert_eq!(block.parent_slot(), 430_000_122); + assert_eq!(block.num_reward_partitions().value(), std::option::Option::Some(&4)); + let transactions = block.transactions().value().expect("full block transactions must be present"); + assert_eq!(transactions.len(), 2); + assert!(matches!(transactions[0].version(), crate::SolanaWireField::Value(crate::SolanaTransactionVersion::Number(1)))); + let first_meta = transactions[0].meta().value().expect("full transaction metadata must be present"); + assert_eq!(first_meta["rewards"][0]["commissionBps"], serde_json::json!(1234)); + let rewards = block.rewards().value().expect("block rewards must be present"); + assert_eq!(rewards[0].commission(), std::option::Option::None); + assert_eq!(rewards[0].commission_bps().value(), std::option::Option::Some(&1_234)); + assert!(rewards[1].commission_bps().is_omitted()); + let request = handle.join().expect("fixture server must join"); + let body = request_body(request.as_str()); + assert_eq!(body["method"], serde_json::json!("getBlock")); + assert_eq!( + body["params"], + serde_json::json!([430000123,{"commitment":"finalized","encoding":"jsonParsed","transactionDetails":"full","maxSupportedTransactionVersion":1,"rewards":true}]) + ); +} + +#[tokio::test(flavor = "current_thread")] +async fn typed_get_block_preserves_absent_and_explicit_empty_modern_config() { + let role = crate::HttpRoleName::new("default"); + for (config, expected_params) in [ + (std::option::Option::None, serde_json::json!([430000123])), + (std::option::Option::Some(crate::SolanaGetBlockConfig::default()), serde_json::json!([430000123, {}])), + ] { + let (url, handle) = serve_once(include_str!("../fixtures/http/get_block.null.json")); + let pool = pool_for_url(url.as_str()); + let block = pool.get_block(&role, 430_000_123, config.as_ref()).await.expect("nullable getBlock request must succeed"); + assert!(block.is_none()); + let request = handle.join().expect("fixture server must join"); + assert_eq!(request_body(request.as_str())["params"], expected_params); + } +} + +#[allow(deprecated)] +#[tokio::test(flavor = "current_thread")] +async fn typed_get_block_legacy_covers_all_bare_encoding_labels_and_response_shapes() { + let role = crate::HttpRoleName::new("default"); + let cases = [ + (crate::SolanaTransactionEncoding::Binary, "binary", include_str!("../fixtures/http/get_block.binary.success.json")), + (crate::SolanaTransactionEncoding::Base58, "base58", include_str!("../fixtures/http/get_block.base58.success.json")), + (crate::SolanaTransactionEncoding::Base64, "base64", include_str!("../fixtures/http/get_block.base64.success.json")), + (crate::SolanaTransactionEncoding::Json, "json", include_str!("../fixtures/http/get_block.json.success.json")), + (crate::SolanaTransactionEncoding::JsonParsed, "jsonParsed", include_str!("../fixtures/http/get_block.json_parsed.success.json")), + ]; + for (encoding, wire_label, response) in cases { + let (url, handle) = serve_once(response); + let pool = pool_for_url(url.as_str()); + let block = pool + .get_block_legacy(&role, 430_000_123, encoding) + .await + .expect("legacy getBlock encoding must succeed") + .expect("legacy getBlock encoding must return a block"); + let transactions = block.transactions().value().expect("legacy full response must contain transactions"); + assert_eq!(transactions.len(), 1); + match encoding { + crate::SolanaTransactionEncoding::Binary => assert!(matches!(transactions[0].transaction(), crate::SolanaEncodedTransaction::LegacyBinary(_))), + crate::SolanaTransactionEncoding::Base58 => assert!(matches!( + transactions[0].transaction(), + crate::SolanaEncodedTransaction::Binary { encoding: crate::SolanaTransactionBinaryEncoding::Base58, .. } + )), + crate::SolanaTransactionEncoding::Base64 => assert!(matches!( + transactions[0].transaction(), + crate::SolanaEncodedTransaction::Binary { encoding: crate::SolanaTransactionBinaryEncoding::Base64, .. } + )), + crate::SolanaTransactionEncoding::Json | crate::SolanaTransactionEncoding::JsonParsed => { + assert!(matches!(transactions[0].transaction(), crate::SolanaEncodedTransaction::Json(_))); + }, + } + let request = handle.join().expect("fixture server must join"); + assert_eq!(request_body(request.as_str())["params"], serde_json::json!([430000123, wire_label])); + } +} + +#[tokio::test(flavor = "current_thread")] +async fn typed_get_block_modern_covers_all_agave_v4_2_1_encoding_labels() { + let role = crate::HttpRoleName::new("default"); + let cases = [ + (crate::SolanaTransactionEncoding::Binary, "binary", include_str!("../fixtures/http/get_block.binary.success.json")), + (crate::SolanaTransactionEncoding::Base58, "base58", include_str!("../fixtures/http/get_block.base58.success.json")), + (crate::SolanaTransactionEncoding::Base64, "base64", include_str!("../fixtures/http/get_block.base64.success.json")), + (crate::SolanaTransactionEncoding::Json, "json", include_str!("../fixtures/http/get_block.json.success.json")), + (crate::SolanaTransactionEncoding::JsonParsed, "jsonParsed", include_str!("../fixtures/http/get_block.json_parsed.success.json")), + ]; + for (encoding, wire_label, response) in cases { + let config = crate::SolanaGetBlockConfig::new( + std::option::Option::Some(crate::SolanaCommitment::Confirmed), + std::option::Option::Some(encoding), + std::option::Option::Some(crate::SolanaTransactionDetails::Full), + std::option::Option::Some(1), + std::option::Option::Some(true), + ); + let (url, handle) = serve_once(response); + let pool = pool_for_url(url.as_str()); + pool.get_block(&role, 430_000_123, std::option::Option::Some(&config)) + .await + .expect("modern getBlock encoding must succeed") + .expect("modern getBlock encoding must return a block"); + let request = handle.join().expect("fixture server must join"); + assert_eq!( + request_body(request.as_str())["params"], + serde_json::json!([430000123,{"commitment":"confirmed","encoding":wire_label,"transactionDetails":"full","maxSupportedTransactionVersion":1,"rewards":true}]) + ); + } +} + +#[tokio::test(flavor = "current_thread")] +async fn typed_get_block_covers_full_signatures_none_and_accounts_transaction_details() { + let role = crate::HttpRoleName::new("default"); + let cases = [ + (crate::SolanaTransactionDetails::Full, "full", include_str!("../fixtures/http/get_block.full_no_rewards.success.json")), + (crate::SolanaTransactionDetails::Signatures, "signatures", include_str!("../fixtures/http/get_block.signatures.success.json")), + (crate::SolanaTransactionDetails::None, "none", include_str!("../fixtures/http/get_block.none.success.json")), + (crate::SolanaTransactionDetails::Accounts, "accounts", include_str!("../fixtures/http/get_block.accounts.success.json")), + ]; + for (details, wire_label, response) in cases { + let config = crate::SolanaGetBlockConfig::new( + std::option::Option::Some(crate::SolanaCommitment::Confirmed), + std::option::Option::Some(crate::SolanaTransactionEncoding::JsonParsed), + std::option::Option::Some(details), + std::option::Option::Some(1), + std::option::Option::Some(false), + ); + let (url, handle) = serve_once(response); + let pool = pool_for_url(url.as_str()); + let block = pool + .get_block(&role, 430_000_123, std::option::Option::Some(&config)) + .await + .expect("transactionDetails getBlock fixture must succeed") + .expect("transactionDetails getBlock fixture must return a block"); + match details { + crate::SolanaTransactionDetails::Full => { + let transactions = block.transactions().value().expect("full transaction list must be present"); + assert_eq!(transactions.len(), 1); + assert!(block.signatures().is_omitted()); + assert_eq!(transactions[0].meta().value().expect("full transaction metadata must be present")["rewards"], serde_json::Value::Null); + }, + crate::SolanaTransactionDetails::Signatures => { + assert!(block.transactions().is_omitted()); + assert_eq!(block.signatures().value().expect("signatures must be present"), &std::vec!["sig-a".to_owned(), "sig-b".to_owned()]); + }, + crate::SolanaTransactionDetails::None => { + assert!(block.transactions().is_omitted()); + assert!(block.signatures().is_omitted()); + }, + crate::SolanaTransactionDetails::Accounts => { + let transactions = block.transactions().value().expect("accounts transaction list must be present"); + assert_eq!(transactions.len(), 1); + assert!(matches!(transactions[0].transaction(), crate::SolanaEncodedTransaction::Json(_))); + if let crate::SolanaEncodedTransaction::Json(transaction) = transactions[0].transaction() { + assert_eq!(transaction["accountKeys"][0]["pubkey"], serde_json::json!("11111111111111111111111111111111")); + } + assert!(matches!(transactions[0].version(), crate::SolanaWireField::Value(crate::SolanaTransactionVersion::Number(1)))); + }, + } + assert!(block.rewards().is_omitted()); + assert_eq!(block.num_reward_partitions().value(), std::option::Option::Some(&4)); + let request = handle.join().expect("fixture server must join"); + assert_eq!( + request_body(request.as_str())["params"], + serde_json::json!([430000123,{"commitment":"confirmed","encoding":"jsonParsed","transactionDetails":wire_label,"maxSupportedTransactionVersion":1,"rewards":false}]) + ); + } +} + +#[tokio::test(flavor = "current_thread")] +async fn typed_get_block_preserves_null_and_omitted_top_level_wire_states() { + let role = crate::HttpRoleName::new("default"); + for (response, expect_null) in [ + (include_str!("../fixtures/http/get_block.omitted_fields.success.json"), false), + (include_str!("../fixtures/http/get_block.null_fields.success.json"), true), + ] { + let (url, handle) = serve_once(response); + let pool = pool_for_url(url.as_str()); + let block = pool + .get_block(&role, 430_000_123, std::option::Option::None) + .await + .expect("wire-state getBlock fixture must succeed") + .expect("wire-state getBlock fixture must return a block"); + if expect_null { + assert!(block.transactions().is_null()); + assert!(block.signatures().is_null()); + assert!(block.rewards().is_null()); + assert!(block.num_reward_partitions().is_null()); + } else { + assert!(block.transactions().is_omitted()); + assert!(block.signatures().is_omitted()); + assert!(block.rewards().is_omitted()); + assert!(block.num_reward_partitions().is_omitted()); + } + handle.join().expect("fixture server must join"); + } +} + +#[tokio::test(flavor = "current_thread")] +async fn typed_get_block_preserves_unsupported_version_rpc_error() { + let config = crate::SolanaGetBlockConfig::new( + std::option::Option::Some(crate::SolanaCommitment::Finalized), + std::option::Option::Some(crate::SolanaTransactionEncoding::Base64), + std::option::Option::Some(crate::SolanaTransactionDetails::Full), + std::option::Option::Some(0), + std::option::Option::Some(true), + ); + let (url, handle) = serve_once(include_str!("../fixtures/http/get_block.error_unsupported_version.json")); + let pool = pool_for_url(url.as_str()); + let result = pool.get_block(&crate::HttpRoleName::new("default"), 430_000_123, std::option::Option::Some(&config)).await; + let error = result.expect_err("unsupported transaction version must remain an RPC application error"); + assert_eq!(error.code(), crate::ERROR_CODE_RPC_APPLICATION_ERROR); + handle.join().expect("fixture server must join"); +} + +#[tokio::test(flavor = "current_thread")] +async fn typed_get_block_preserves_block_not_available_rpc_error() { + let (url, handle) = serve_once(include_str!("../fixtures/http/get_block.error_block_not_available.json")); + let pool = pool_for_url(url.as_str()); + let result = pool.get_block(&crate::HttpRoleName::new("default"), 430_000_123, std::option::Option::None).await; + let error = result.expect_err("unavailable block must remain an RPC application error"); + assert_eq!(error.code(), crate::ERROR_CODE_RPC_APPLICATION_ERROR); + handle.join().expect("fixture server must join"); +} + +#[tokio::test(flavor = "current_thread")] +async fn typed_get_block_rejects_processed_commitment_before_io() { + let config = crate::SolanaGetBlockConfig::new( + std::option::Option::Some(crate::SolanaCommitment::Processed), + std::option::Option::Some(crate::SolanaTransactionEncoding::Json), + std::option::Option::None, + std::option::Option::None, + std::option::Option::None, + ); + let pool = pool_for_url("http://127.0.0.1:9"); + let result = pool.get_block(&crate::HttpRoleName::new("default"), 430_000_123, std::option::Option::Some(&config)).await; + let error = result.expect_err("processed getBlock 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"); +} diff --git a/deltas/0.2.4/pre.006.md b/deltas/0.2.4/pre.006.md new file mode 100644 index 0000000..193138c --- /dev/null +++ b/deltas/0.2.4/pre.006.md @@ -0,0 +1,541 @@ + + + +# Delta `0.2.4-pre.006` — `getBlock` complet + +## Base requise + +Livraison précédente : + +```text +0.2.4-pre.005 +workspace.package.version = "0.2.4-pre.5" +``` + +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 -> 212 unit tests OK + 23 public API tests OK + 18 release-completeness tests OK + 1 smoke Devnet ignoré comme prévu + 0 échec +``` + +## Objectif + +Achever la famille Blocks de `0.2.4` avec le wrapper le plus riche de la release : + +```text +getBlock +``` + +La méthode est déjà enregistrée : + +```text +V0_2_4 / Blocks / Read / RetrySafe / StableWithDeprecatedLegacy +``` + +Le registre central n'est pas modifié. + +Après cette tranche, les dix wrappers Blocks de `0.2.4` sont matérialisés. Les cinq wrappers Economics restent volontairement pour `pre.007` et `pre.008`. + +## Réaudit primaire `getBlock` — Agave `v4.2.1` + +La source runtime stable confirme que `getBlock` reçoit : + +```text +slot +Option> +``` + +La forme wrapper conserve donc deux familles de requêtes : + +```text +moderne : [slot, { ...RpcBlockConfig }] +legacy : [slot, "encoding"] +``` + +La forme legacy bare encoding reste supportée par le runtime pour backwards compatibility, mais n'est pas la forme KSP recommandée. + +`RpcBlockConfig` expose exactement dans la baseline stable : + +```text +encoding: Option +transactionDetails: Option +rewards: Option +commitment: Option +maxSupportedTransactionVersion: Option +``` + +Aucun champ `footer` n'existe dans `RpcBlockConfig` stable `v4.2.1`. + +Défauts appliqués côté runtime lorsqu'ils ne sont pas fournis : + +```text +encoding = json +transactionDetails = full +rewards = true +``` + +Le commitment explicitement fourni doit être au moins `confirmed`; KSP rejette donc `processed` avant I/O avec le domaine d'erreur partagé. + +## Réaudit SIMD footer / transaction version + +### SIMD-0307 — Add Block Footer + +Le document est toujours en `Review` et propose : + +```text +config getBlock : footer +réponse : footer fields, dont blockProducerTimeNanos / blockUserAgent +``` + +Ces champs sont absents de la baseline Agave `v4.2.1`; ils ne sont pas inventés dans KSP. + +### SIMD-0298 — Add `bank_hash` to block footer + +Le document SIMD reste au statut `Idea`. Il propose un futur `bank_hash` dans le block footer. Ce champ n'existe pas dans le wire RPC stable audité et n'est pas ajouté spéculativement. + +Le tracker amont lié à Bankless Leader/Alpenglow rend néanmoins ce sujet important pour le réaudit final : l'existence d'une implémentation future du mécanisme ne transforme pas sa forme RPC en contrat stable `v4.2.1`. + +### SIMD-0301 — `parent_bank_hash` + +Le PR SIMD-0301 proposait de remplacer `bank_hash` par `parent_bank_hash` et de supersede SIMD-0298. Il a été fermé le 28 janvier 2026 sans merge. Il ne constitue donc ni un SIMD adopté ni un wire stable. + +Le plan vivant ajoute `0301` à la watchlist de `pre.009` avec `0298` et `0307`, afin de ne pas figer prématurément le nom ou même la présence d'un futur hash de footer. + +### SIMD-0385 — Transaction V1 + +SIMD-0385 reste en `Review`, tandis que la baseline Agave `v4.2.1` possède déjà le plumbing transaction-status nécessaire aux messages V1. + +KSP conserve une représentation générique : + +```text +SolanaTransactionVersion::Legacy +SolanaTransactionVersion::Number(u8) +``` + +Le wrapper `getBlock` ajoute une canary avec : + +```text +version = 1 +``` + +Il ne régresse donc pas vers un modèle limité à `legacy` ou `v0`. + +## Version Cargo + +Nouvelle prerelease technique : + +```text +0.2.4-pre.5 -> 0.2.4-pre.6 +``` + +Aucune dépendance ni feature Cargo n'est ajoutée ou modifiée. + +## Forme moderne + +Signature publique : + +```text +HttpTransportPool::get_block( + role, + slot, + Option<&SolanaGetBlockConfig>, +) -> Result> +``` + +Les trois états de la config restent distingués lorsque leur forme wire est utile : + +```text +None -> [slot] +Some(default config) -> [slot, {}] +Some(config) -> [slot, {...}] +``` + +La config moderne acquise en `pre.002` est réutilisée, sans DTO parallèle. + +## Forme legacy retained + +La surface publique dédiée est : + +```text +#[deprecated] +HttpTransportPool::get_block_legacy(role, slot, encoding) +``` + +Elle émet le warning KSP centralisé et encode exactement : + +```text +[slot, "binary"] +[slot, "base58"] +[slot, "base64"] +[slot, "json"] +[slot, "jsonParsed"] +``` + +Les cinq labels de `UiTransactionEncoding` stable sont couverts, y compris `binary`, lui-même legacy mais encore retenu par Agave pour compatibilité. + +## `transactionDetails` complet + +Les quatre variantes stables sont couvertes : + +```text +full +signatures +none +accounts +``` + +Leurs conséquences wire Agave sont conservées sans synthèse : + +```text +full -> transactions présent, signatures omis +signatures -> transactions omis, signatures présent +none -> transactions omis, signatures omis +accounts -> transactions présent sous représentation account-list, signatures omis +``` + +La représentation `accounts` reste un payload transaction JSON lossless; elle n'est pas forcée dans la structure plus riche d'une transaction `full`. + +## Encodings et union transaction + +Les cinq encodings modernes et legacy sont testés. Le DTO partagé `SolanaEncodedTransaction` couvre les trois formes de réponse effectivement nécessaires : + +```text +LegacyBinary(String) <- encoding "binary" +Binary { data, Base58 ou Base64 } <- encodings "base58" / "base64" +Json(serde_json::Value) <- encodings "json" / "jsonParsed" et transactionDetails=accounts +``` + +Aucun décodage binaire, Program-specific ou message-specific n'est introduit dans Transport. + +## Résultat bloc riche + +`getBlock` retourne : + +```text +Option +``` + +Le `null` top-level du RPC est conservé comme `None`. + +Pour un bloc présent, KSP conserve : + +```text +previousBlockhash +blockhash +parentSlot +transactions : SolanaWireField> +signatures : SolanaWireField> +rewards : SolanaWireField> +numRewardPartitions : SolanaWireField +blockTime : Option +blockHeight : Option +``` + +Les champs dont la présence dépend du mode de réponse gardent donc leurs états wire `omitted` / `null` / `value` au lieu d'être normalisés. + +## Transactions de bloc + +Un élément `transactions[]` possède son propre DTO, distinct de `SolanaConfirmedTransaction` : + +```text +transaction +meta +version +``` + +Le DTO de `getTransaction` n'est pas réutilisé car son `slot` et son `blockTime` sont top-level et n'appartiennent pas à chaque transaction d'un bloc. + +`meta` reste : + +```text +SolanaWireField +``` + +Cela préserve sans perte les champs transaction-status actuels et leurs extensions, notamment les rewards imbriqués, sans transférer de logique métier dans Transport. + +## Rewards et SIMD-0291 + +Le reward de bloc conserve : + +```text +pubkey: Pubkey +lamports: i64 +postBalance: u64 +rewardType: Option +commission: Option +commissionBps: SolanaWireField +``` + +`commission` et `commissionBps` restent indépendants : KSP ne dérive jamais l'un depuis l'autre. + +Les fixtures couvrent : + +```text +commission = null + commissionBps présent +commission présent + commissionBps omis +commissionBps imbriqué dans transaction.meta +``` + +## Reward partitions et SIMD-0118 + +`numRewardPartitions` reste un champ wire propre : + +```text +SolanaWireField +``` + +Les fixtures couvrent `value`, omission et `null` de compatibilité. Aucun `0` n'est synthétisé. + +Point important confirmé dans l'encodeur Agave : `numRewardPartitions` est renseigné depuis le bloc indépendamment de `show_rewards`. Avec : + +```text +rewards: false +``` + +KSP accepte donc correctement : + +```text +rewards omis +numRewardPartitions présent +``` + +sans lier artificiellement les deux champs. + +## Rewards désactivés dans `meta` + +Le runtime Agave n'utilise pas exactement la même sérialisation interne selon les chemins `full/jsonParsed`, `accounts`, etc. pour masquer les rewards de metadata. + +Transport ne normalise pas ces sous-arbres. La fixture `full` avec `rewards:false` conserve explicitement un `meta.rewards = null`, tandis que les formes qui omettent ce champ restent également préservées grâce au `serde_json::Value` lossless. + +## Validation déterministe avant I/O + +La seule validation locale ajoutée à `getBlock` est : + +```text +commitment = processed -> ERROR_CODE_INVALID_RPC_PARAMETERS avant I/O +commitment = confirmed -> autorisé +commitment = finalized -> autorisé +``` + +KSP ne tente pas de prévalider : + +```text +disponibilité du bloc +slot cleaned/skipped +historique local ou long-term storage +support réel d'une version de transaction par le caller/runtime +``` + +Ces états appartiennent au runtime RPC. + +## Erreurs RPC runtime préservées + +Deux erreurs Agave caractéristiques sont couvertes par fixtures : + +```text +-32004 BlockNotAvailable +-32015 UnsupportedTransactionVersion +``` + +Toutes deux restent des `ERROR_CODE_RPC_APPLICATION_ERROR` dans la façade KSP; elles ne sont ni transformées en erreurs de validation locale ni avalées en `None`. + +Le `null` success result reste distinct d'une erreur RPC. + +## Discipline `#[cfg(test)]` + +La règle introduite par `pre.002-fix.001` reste appliquée. + +Deviennent runtime dans cette tranche parce que `getBlock` les consomme réellement : + +```text +SolanaGetBlockConfig::to_json_value +SolanaBlockReward::decode_wire +SolanaBlockTransaction::decode_wire +SolanaConfirmedBlock::decode_wire +decode_block_transaction_version +decode_block_transactions +decode_block_rewards +WireBlockReward +WireBlockTransaction +WireConfirmedBlock +``` + +`SolanaGetBlockConfig::is_empty` reste test-only : le wrapper doit justement préserver un `Some(default config)` explicite comme `{}` et n'a aucun besoin runtime de le canoniser. + +Les helpers Economics restent test-only jusqu'à `pre.007`/`pre.008`. + +Aucun `#[allow(dead_code)]` n'est introduit. + +## Fixtures HTTP ajoutées + +```text +get_block.full.success.json +get_block.full_no_rewards.success.json +get_block.signatures.success.json +get_block.none.success.json +get_block.accounts.success.json +get_block.binary.success.json +get_block.base58.success.json +get_block.base64.success.json +get_block.json.success.json +get_block.json_parsed.success.json +get_block.null.json +get_block.omitted_fields.success.json +get_block.null_fields.success.json +get_block.error_block_not_available.json +get_block.error_unsupported_version.json +``` + +Elles couvrent notamment : + +- rich full wire ; +- cinq encodings ; +- quatre `transactionDetails` ; +- legacy bare encoding ; +- result `null` ; +- états omitted/null/value des champs top-level conditionnels ; +- `rewards:false` sans disparition de `numRewardPartitions` ; +- SIMD-0118 ; +- SIMD-0291 ; +- transaction version numérique `1` pour la compatibilité préparée SIMD-0385 ; +- erreurs RPC bloc/version. + +## Tests ajoutés + +Neuf unit tests HTTP sont ajoutés : + +```text +typed_get_block_modern_full_config_preserves_rich_wire_and_simd_fields +typed_get_block_preserves_absent_and_explicit_empty_modern_config +typed_get_block_legacy_covers_all_bare_encoding_labels_and_response_shapes +typed_get_block_modern_covers_all_agave_v4_2_1_encoding_labels +typed_get_block_covers_full_signatures_none_and_accounts_transaction_details +typed_get_block_preserves_null_and_omitted_top_level_wire_states +typed_get_block_preserves_unsupported_version_rpc_error +typed_get_block_preserves_block_not_available_rpc_error +typed_get_block_rejects_processed_commitment_before_io +``` + +Une canary public API est ajoutée pour les deux formes publiques moderne/legacy et les getters de config. + +Une canary release-completeness vérifie désormais que la famille Blocks `V0_2_4` possède exactement ses dix descriptors `Read / RetrySafe` et que `getBlock` conserve son marqueur `StableWithDeprecatedLegacy`. + +Compteurs attendus après validation : + +```text +221 unit tests +24 public API tests +19 release-completeness tests +1 smoke Devnet ignoré +``` + +## Documentation vivante + +`docs/plans/011-V0_2_4_HTTP_BLOCKS_ECONOMICS_PLAN.md` passe en version 4. + +Modifications : + +- réaudit `pre.006` de SIMD-0298/0307 ; +- ajout de SIMD-0301 à la watchlist finale après constat de fermeture sans merge ; +- ajout des sources Agave transaction-status/custom-error effectivement utilisées pour vérifier le wire riche et les erreurs ; +- `pre.009` réauditera ensemble au minimum `0298/0301/0307` pour le futur footer. + +Les deux tableaux du plan conservent leur structure source alignée : + +```text +19 lignes au total +4 colonnes +5 caractères `|` par ligne +largeurs 31 / 152 / 163 / 302 +``` + +Aucun ancien delta n'est modifié. + +## Frontières architecturales + +Cette tranche ne change aucune frontière : + +```text +Transport -X-> Config +Transport -X-> Store +Transport -X-> Program +Transport -X-> tracing direct +``` + +Le wrapper passe uniquement par : + +```text +getBlock typed + -> descriptor central + -> execute_standard_rpc + -> admission/pool/executor HTTP commun + -> parser JSON-RPC central + -> decode typed/lossless +``` + +Aucun client RPC Solana haut niveau, reqwest parallèle, retry local ou décodage Program n'est ajouté. + +## Validation attendue + +Après application : + +```bash +cargo fmt --all +cargo check --workspace +cargo clippy --workspace --all-targets +cargo test -p ksp-onchain-transport-lib +``` + +Attendus : + +```text +0 warning introduit +221 unit tests OK +24 public API tests OK +19 release-completeness tests OK +1 smoke Devnet ignoré comme prévu +0 échec +``` + +Le sandbox d'échange ne possède pas de toolchain Rust ; ces validations compilées doivent donc être confirmées localement. + +## Fichiers touchés + +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 +``` + +Ajoutés : + +```text +15 fixtures get_block*.json +deltas/0.2.4/pre.006.md +``` + +## Identifiant de commit attendu + +Après validation locale : + +```text +v0.2.4-pre.006 +``` + +La tranche suivante reste : + +```text +0.2.4-pre.007 — réaudit SIMD-0490/0550 + Economics simples +``` 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 01f750a..ef34c34 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 @@ -81,6 +81,10 @@ https://github.com/anza-xyz/agave/releases/tag/v4.2.1 https://github.com/anza-xyz/agave/blob/v4.2.1/rpc/src/rpc.rs https://github.com/anza-xyz/agave/blob/v4.2.1/rpc-client-types/src/config.rs https://github.com/anza-xyz/agave/blob/v4.2.1/rpc-client-types/src/response.rs +https://github.com/anza-xyz/agave/blob/v4.2.1/rpc-client-api/src/custom_error.rs +https://github.com/anza-xyz/agave/blob/v4.2.1/transaction-status-client-types/src/lib.rs +https://github.com/anza-xyz/agave/blob/v4.2.1/transaction-status-client-types/src/option_serializer.rs +https://github.com/anza-xyz/agave/blob/v4.2.1/transaction-status/src/lib.rs ``` SIMDs primaires ciblés par l'audit `KSP-TRANSPORT-007` : @@ -94,6 +98,7 @@ https://github.com/solana-foundation/solana-improvement-documents/blob/main/prop https://github.com/solana-foundation/solana-improvement-documents/blob/main/proposals/0186-loaded-transaction-data-size-specification.md https://github.com/solana-foundation/solana-improvement-documents/blob/main/proposals/0291-commission-rate-in-basis-points.md https://github.com/solana-foundation/solana-improvement-documents/blob/main/proposals/0298-bank-hash-in-block-footer.md +https://github.com/solana-foundation/solana-improvement-documents/pull/301 https://github.com/solana-foundation/solana-improvement-documents/blob/main/proposals/0307-add-block-footer.md https://github.com/solana-foundation/solana-improvement-documents/blob/main/proposals/0385-transaction-v1.md https://github.com/solana-foundation/solana-improvement-documents/blob/main/proposals/0490-upgrade-stake-to-v5.md @@ -205,7 +210,9 @@ La surface `getTransaction` de `0.2.3` ne nécessite pas de correction SIMD-0291 **SIMD-0180 — Vote Account Address Keyed Leader Schedule** est en `Review`. Son volet RPC demande explicitement que les endpoints historiques de leader schedule et slot leader continuent à retourner l'identité validator afin de préserver la compatibilité, tout en prévoyant de nouveaux endpoints vote-account-keyed. Le wrapper KSP `getLeaderSchedule` acquis en `0.2.2` ne doit donc pas être modifié spéculativement. En revanche, le réaudit d'inventaire de `pre.009` doit détecter si de nouveaux endpoints ont rejoint la surface HTTP officielle : ce serait alors une évolution de l'inventaire RPC, pas un changement silencieux du wire de `getLeaderSchedule`. -**SIMD-0307 — Add Block Footer** est en `Review` et propose précisément une extension de `getBlock` : option de config `footer` et champs footer dans la réponse. Ces éléments sont absents de `RpcBlockConfig` et `UiConfirmedBlock` dans Agave stable `v4.2.1`; ils ne doivent donc pas être inventés dans `pre.002`. **SIMD-0298 — Add `bank_hash` to block footer** est encore au statut `Idea` et prévoit d'étendre ce même footer avec `bank_hash`. Les deux SIMDs doivent être réaudités ensemble au début de la tranche `getBlock` et à `pre.009`. Si une baseline stable expose alors le footer, `KSP-TRANSPORT-007` impose de couvrir toutes ses options et tous ses champs stables, y compris `bankHash` s'il fait partie du wire effectivement livré. +**SIMD-0307 — Add Block Footer** est en `Review` et propose précisément une extension de `getBlock` : option de config `footer` et champs footer dans la réponse. Ces éléments sont absents de `RpcBlockConfig` et `UiConfirmedBlock` dans Agave stable `v4.2.1`; ils ne doivent donc pas être inventés. **SIMD-0298 — Add `bank_hash` to block footer** reste au statut documentaire `Idea` et prévoit d'étendre ce même footer avec `bank_hash`. Le réaudit `pre.006` du 18 août 2026 confirme que le wire RPC stable `v4.2.1` n'expose toujours aucun de ces champs. Le tracker Bankless Leader amont signale néanmoins l'implémentation Agave du mécanisme de block footer/bank hash pour la future migration Alpenglow : cela renforce la nécessité du réaudit final sans justifier une extension spéculative du wrapper actuel. + +**SIMD-0301 — Replace `bank_hash` with `parent_bank_hash`** est également surveillé comme évolution directement dépendante de ce footer. La proposition amont a été fermée sans merge le 28 janvier 2026; elle n'est donc pas un contrat SIMD adopté ni un champ RPC stable. `pre.009` doit réauditer ensemble `0298`, `0301` et `0307`. Si une baseline stable expose alors le footer, `KSP-TRANSPORT-007` impose de couvrir toutes ses options et tous ses champs effectivement livrés, sans supposer à l'avance si le hash final est `bankHash`, `parentBankHash` ou absent. **SIMD-0490 — Upgrade BPF Stake Program to v5.0.0** est en `Review` et prévoit notamment une hausse du minimum de délégation ainsi que son exposition par le RPC de minimum delegation. Pour `getStakeMinimumDelegation`, KSP doit donc traiter la valeur contextualisée comme une valeur runtime opaque en lamports : aucun minimum `1 lamport`, `1 SOL` ou autre ne doit être codé en dur ou validé côté Transport. Le SIMD sera réaudité avant `pre.007` et à `pre.009`. @@ -419,7 +426,7 @@ historical Deprecated/Removed == 14/14 KSP-TRANSPORT-007 audited current == 52/52 ``` -La compliance `KSP-TRANSPORT-007` des 37 wrappers de `0.2.1`–`0.2.3` reste acquise par `docs/validation/005-V0_2_3_KSP_TRANSPORT_007_RETRO_AUDIT.md`; la clôture `0.2.4` vérifie qu'aucune régression n'a été introduite et applique la même règle aux 15 nouveaux wrappers. Le réaudit final inclut explicitement l'état des SIMD susceptibles de modifier la surface HTTP ou sa sémantique observable, au minimum SIMD-0180, SIMD-0298, SIMD-0307, SIMD-0385, SIMD-0490, SIMD-0550 et SIMD-0553, afin qu'une évolution devenue stable pendant la session ne soit pas oubliée. Il réaudite aussi l'inventaire officiel lui-même afin de détecter d'éventuels nouveaux endpoints issus de SIMD-0180 ou d'une autre évolution d'interface. +La compliance `KSP-TRANSPORT-007` des 37 wrappers de `0.2.1`–`0.2.3` reste acquise par `docs/validation/005-V0_2_3_KSP_TRANSPORT_007_RETRO_AUDIT.md`; la clôture `0.2.4` vérifie qu'aucune régression n'a été introduite et applique la même règle aux 15 nouveaux wrappers. Le réaudit final inclut explicitement l'état des SIMD susceptibles de modifier la surface HTTP ou sa sémantique observable, au minimum SIMD-0180, SIMD-0298, SIMD-0301, SIMD-0307, SIMD-0385, SIMD-0490, SIMD-0550 et SIMD-0553, afin qu'une évolution devenue stable pendant la session ne soit pas oubliée. Il réaudite aussi l'inventaire officiel lui-même afin de détecter d'éventuels nouveaux endpoints issus de SIMD-0180 ou d'une autre évolution d'interface. ## Smokes live @@ -444,7 +451,7 @@ pre.006 réaudit SIMD-0298/0307 + getBlock moderne + bare encoding legacy + tra pre.007 réaudit SIMD-0490/0550 + Economics simples : getInflationGovernor, getInflationRate, getStakeMinimumDelegation, getSupply pre.008 getInflationReward + null positionnels + commissionBps SIMD-0291 + invariants -pre.009 réaudit SIMD HTTP (0180/0298/0307/0385/0490/0550/0553 minimum) +pre.009 réaudit SIMD HTTP (0180/0298/0301/0307/0385/0490/0550/0553 minimum) + réaudit inventaire officiel + compliance finale 52/52 + 14/14 + KSP-TRANSPORT-007 + docs + smokes + graphes + prompt 0.2.5 — Wallet foundation rel.001 publication strictement publicationnelle