diff --git a/Cargo.lock b/Cargo.lock index d8014cd55..7c5af8bc9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6103,6 +6103,7 @@ version = "1.11.0" dependencies = [ "log", "rustc_version", + "semver", "serde", "serde_derive", "solana-frozen-abi 1.11.0", diff --git a/cli/src/checks.rs b/cli/src/checks.rs index d3cf03a54..1ec88fbb9 100644 --- a/cli/src/checks.rs +++ b/cli/src/checks.rs @@ -163,7 +163,10 @@ mod tests { fn test_check_account_for_fees() { let account_balance = 1; let account_balance_response = json!(Response { - context: RpcResponseContext { slot: 1 }, + context: RpcResponseContext { + slot: 1, + api_version: None + }, value: json!(account_balance), }); let pubkey = solana_sdk::pubkey::new_rand(); @@ -183,7 +186,10 @@ mod tests { check_account_for_fee(&rpc_client, &pubkey, &message0).expect("unexpected result"); let check_fee_response = json!(Response { - context: RpcResponseContext { slot: 1 }, + context: RpcResponseContext { + slot: 1, + api_version: None + }, value: json!(2), }); let mut mocks = HashMap::new(); @@ -193,7 +199,10 @@ mod tests { assert!(check_account_for_fee(&rpc_client, &pubkey, &message1).is_err()); let check_fee_response = json!(Response { - context: RpcResponseContext { slot: 1 }, + context: RpcResponseContext { + slot: 1, + api_version: None + }, value: json!(2), }); let mut mocks = HashMap::new(); @@ -206,11 +215,17 @@ mod tests { let account_balance = 2; let account_balance_response = json!(Response { - context: RpcResponseContext { slot: 1 }, + context: RpcResponseContext { + slot: 1, + api_version: None + }, value: json!(account_balance), }); let check_fee_response = json!(Response { - context: RpcResponseContext { slot: 1 }, + context: RpcResponseContext { + slot: 1, + api_version: None + }, value: json!(1), }); @@ -227,7 +242,10 @@ mod tests { fn test_check_account_for_balance() { let account_balance = 50; let account_balance_response = json!(Response { - context: RpcResponseContext { slot: 1 }, + context: RpcResponseContext { + slot: 1, + api_version: None + }, value: json!(account_balance), }); let pubkey = solana_sdk::pubkey::new_rand(); @@ -244,7 +262,10 @@ mod tests { #[test] fn test_get_fee_for_messages() { let check_fee_response = json!(Response { - context: RpcResponseContext { slot: 1 }, + context: RpcResponseContext { + slot: 1, + api_version: None + }, value: json!(1), }); let mut mocks = HashMap::new(); @@ -263,7 +284,10 @@ mod tests { // No signatures, no fee. let check_fee_response = json!(Response { - context: RpcResponseContext { slot: 1 }, + context: RpcResponseContext { + slot: 1, + api_version: None + }, value: json!(0), }); let mut mocks = HashMap::new(); diff --git a/cli/src/cli.rs b/cli/src/cli.rs index d0ffad4cb..2ff505039 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -1986,7 +1986,10 @@ mod tests { assert!(result.is_ok()); let vote_account_info_response = json!(Response { - context: RpcResponseContext { slot: 1 }, + context: RpcResponseContext { + slot: 1, + api_version: None + }, value: json!({ "data": ["KLUv/QBYNQIAtAIBAAAAbnoc3Smwt4/ROvTFWY/v9O8qlxZuPKby5Pv8zYBQW/EFAAEAAB8ACQD6gx92zAiAAecDP4B2XeEBSIx7MQeung==", "base64+zstd"], "lamports": 42, @@ -2272,7 +2275,10 @@ mod tests { // Success case let mut config = CliConfig::default(); let account_info_response = json!(Response { - context: RpcResponseContext { slot: 1 }, + context: RpcResponseContext { + slot: 1, + api_version: None + }, value: Value::Null, }); let mut mocks = HashMap::new(); diff --git a/client/src/blockhash_query.rs b/client/src/blockhash_query.rs index ca7522a12..716715aab 100644 --- a/client/src/blockhash_query.rs +++ b/client/src/blockhash_query.rs @@ -355,7 +355,10 @@ mod tests { let rpc_blockhash = hash(&[1u8]); let rpc_fee_calc = FeeCalculator::new(42); let get_recent_blockhash_response = json!(Response { - context: RpcResponseContext { slot: 1 }, + context: RpcResponseContext { + slot: 1, + api_version: None + }, value: json!(RpcFees { blockhash: rpc_blockhash.to_string(), fee_calculator: rpc_fee_calc.clone(), @@ -364,7 +367,10 @@ mod tests { }), }); let get_fee_calculator_for_blockhash_response = json!(Response { - context: RpcResponseContext { slot: 1 }, + context: RpcResponseContext { + slot: 1, + api_version: None + }, value: json!(RpcFeeCalculator { fee_calculator: rpc_fee_calc.clone() }), @@ -428,7 +434,10 @@ mod tests { None, ); let get_account_response = json!(Response { - context: RpcResponseContext { slot: 1 }, + context: RpcResponseContext { + slot: 1, + api_version: None + }, value: json!(Some(rpc_nonce_account)), }); diff --git a/client/src/mock_sender.rs b/client/src/mock_sender.rs index e4b49270e..5ac3fb5bd 100644 --- a/client/src/mock_sender.rs +++ b/client/src/mock_sender.rs @@ -108,15 +108,15 @@ impl RpcSender for MockSender { let val = match method.as_str().unwrap() { "getAccountInfo" => serde_json::to_value(Response { - context: RpcResponseContext { slot: 1 }, + context: RpcResponseContext { slot: 1, api_version: None }, value: Value::Null, })?, "getBalance" => serde_json::to_value(Response { - context: RpcResponseContext { slot: 1 }, + context: RpcResponseContext { slot: 1, api_version: None }, value: Value::Number(Number::from(50)), })?, "getRecentBlockhash" => serde_json::to_value(Response { - context: RpcResponseContext { slot: 1 }, + context: RpcResponseContext { slot: 1, api_version: None }, value: ( Value::String(PUBKEY.to_string()), serde_json::to_value(FeeCalculator::default()).unwrap(), @@ -137,16 +137,16 @@ impl RpcSender for MockSender { serde_json::to_value(Some(FeeCalculator::default())).unwrap() }; serde_json::to_value(Response { - context: RpcResponseContext { slot: 1 }, + context: RpcResponseContext { slot: 1, api_version: None }, value, })? } "getFeeRateGovernor" => serde_json::to_value(Response { - context: RpcResponseContext { slot: 1 }, + context: RpcResponseContext { slot: 1, api_version: None }, value: serde_json::to_value(FeeRateGovernor::default()).unwrap(), })?, "getFees" => serde_json::to_value(Response { - context: RpcResponseContext { slot: 1 }, + context: RpcResponseContext { slot: 1, api_version: None }, value: serde_json::to_value(RpcFees { blockhash: PUBKEY.to_string(), fee_calculator: FeeCalculator::default(), @@ -185,7 +185,7 @@ impl RpcSender for MockSender { .map(|_| status.clone()) .collect(); serde_json::to_value(Response { - context: RpcResponseContext { slot: 1 }, + context: RpcResponseContext { slot: 1, api_version: None }, value: statuses, })? } @@ -248,7 +248,7 @@ impl RpcSender for MockSender { "getBlockProduction" => { if params.is_null() { json!(Response { - context: RpcResponseContext { slot: 1 }, + context: RpcResponseContext { slot: 1, api_version: None }, value: RpcBlockProduction { by_identity: HashMap::new(), range: RpcBlockProductionRange { @@ -266,7 +266,7 @@ impl RpcSender for MockSender { let config_range = config.range.unwrap_or_default(); json!(Response { - context: RpcResponseContext { slot: 1 }, + context: RpcResponseContext { slot: 1, api_version: None }, value: RpcBlockProduction { by_identity, range: RpcBlockProductionRange { @@ -289,7 +289,7 @@ impl RpcSender for MockSender { inactive: 12, }), "getSupply" => json!(Response { - context: RpcResponseContext { slot: 1 }, + context: RpcResponseContext { slot: 1, api_version: None }, value: RpcSupply { total: 100000000, circulating: 50000, @@ -304,7 +304,7 @@ impl RpcSender for MockSender { }; json!(Response { - context: RpcResponseContext { slot: 1 }, + context: RpcResponseContext { slot: 1, api_version: None }, value: vec![rpc_account_balance], }) } @@ -335,7 +335,7 @@ impl RpcSender for MockSender { Value::String(signature) } "simulateTransaction" => serde_json::to_value(Response { - context: RpcResponseContext { slot: 1 }, + context: RpcResponseContext { slot: 1, api_version: None }, value: RpcSimulateTransactionResult { err: None, logs: None, @@ -353,14 +353,14 @@ impl RpcSender for MockSender { }) } "getLatestBlockhash" => serde_json::to_value(Response { - context: RpcResponseContext { slot: 1 }, + context: RpcResponseContext { slot: 1, api_version: None }, value: RpcBlockhash { blockhash: PUBKEY.to_string(), last_valid_block_height: 1234, }, })?, "getFeeForMessage" => serde_json::to_value(Response { - context: RpcResponseContext { slot: 1 }, + context: RpcResponseContext { slot: 1, api_version: None }, value: json!(Some(0)), })?, "getClusterNodes" => serde_json::to_value(vec![RpcContactInfo { @@ -441,7 +441,7 @@ impl RpcSender for MockSender { "minimumLedgerSlot" => json![123], "getMaxRetransmitSlot" => json![123], "getMultipleAccounts" => serde_json::to_value(Response { - context: RpcResponseContext { slot: 1 }, + context: RpcResponseContext { slot: 1, api_version: None }, value: vec![Value::Null, Value::Null] })?, "getProgramAccounts" => { diff --git a/client/src/nonblocking/rpc_client.rs b/client/src/nonblocking/rpc_client.rs index 5980beb1f..49fad8603 100644 --- a/client/src/nonblocking/rpc_client.rs +++ b/client/src/nonblocking/rpc_client.rs @@ -419,7 +419,7 @@ impl RpcClient { /// // Create a mock with a custom repsonse to the `GetBalance` request /// let account_balance = 50; /// let account_balance_response = json!(Response { - /// context: RpcResponseContext { slot: 1 }, + /// context: RpcResponseContext { slot: 1, api_version: None }, /// value: json!(account_balance), /// }); /// @@ -5394,7 +5394,10 @@ pub fn create_rpc_client_mocks() -> crate::mock_sender::Mocks { let get_account_request = RpcRequest::GetAccountInfo; let get_account_response = serde_json::to_value(Response { - context: RpcResponseContext { slot: 1 }, + context: RpcResponseContext { + slot: 1, + api_version: None, + }, value: { let pubkey = Pubkey::from_str("BgvYtJEfmZYdVKiptmMjxGzv8iQoo4MWjsP3QsTkhhxa").unwrap(); let account = Account { diff --git a/client/src/rpc_client.rs b/client/src/rpc_client.rs index 2985c0ca7..3012b18ff 100644 --- a/client/src/rpc_client.rs +++ b/client/src/rpc_client.rs @@ -448,7 +448,7 @@ impl RpcClient { /// // Create a mock with a custom repsonse to the `GetBalance` request /// let account_balance = 50; /// let account_balance_response = json!(Response { - /// context: RpcResponseContext { slot: 1 }, + /// context: RpcResponseContext { slot: 1, api_version: None }, /// value: json!(account_balance), /// }); /// @@ -4061,7 +4061,10 @@ pub fn create_rpc_client_mocks() -> crate::mock_sender::Mocks { let get_account_request = RpcRequest::GetAccountInfo; let get_account_response = serde_json::to_value(Response { - context: RpcResponseContext { slot: 1 }, + context: RpcResponseContext { + slot: 1, + api_version: None, + }, value: { let pubkey = Pubkey::from_str("BgvYtJEfmZYdVKiptmMjxGzv8iQoo4MWjsP3QsTkhhxa").unwrap(); let account = Account { diff --git a/client/src/rpc_response.rs b/client/src/rpc_response.rs index 4eab16e56..cba3eed99 100644 --- a/client/src/rpc_response.rs +++ b/client/src/rpc_response.rs @@ -1,5 +1,6 @@ use { crate::client_error, + serde::{Deserialize, Deserializer, Serialize, Serializer}, solana_account_decoder::{parse_token::UiTokenAmount, UiAccount}, solana_sdk::{ clock::{Epoch, Slot, UnixTimestamp}, @@ -12,15 +13,64 @@ use { solana_transaction_status::{ ConfirmedTransactionStatusWithSignature, TransactionConfirmationStatus, UiConfirmedBlock, }, - std::{collections::HashMap, fmt, net::SocketAddr}, + std::{collections::HashMap, fmt, net::SocketAddr, str::FromStr}, thiserror::Error, }; pub type RpcResult = client_error::Result>; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct RpcResponseContext { - pub slot: u64, + pub slot: Slot, + #[serde(skip_serializing_if = "Option::is_none")] + pub api_version: Option, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct RpcApiVersion(semver::Version); + +impl std::ops::Deref for RpcApiVersion { + type Target = semver::Version; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Default for RpcApiVersion { + fn default() -> Self { + Self(solana_version::Version::default().as_semver_version()) + } +} + +impl Serialize for RpcApiVersion { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +impl<'de> Deserialize<'de> for RpcApiVersion { + fn deserialize(deserializer: D) -> std::result::Result + where + D: Deserializer<'de>, + { + let s: String = Deserialize::deserialize(deserializer)?; + Ok(RpcApiVersion( + semver::Version::from_str(&s).map_err(serde::de::Error::custom)?, + )) + } +} + +impl RpcResponseContext { + pub fn new(slot: Slot) -> Self { + Self { + slot, + api_version: Some(RpcApiVersion::default()), + } + } } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] diff --git a/programs/bpf/Cargo.lock b/programs/bpf/Cargo.lock index e07cd6f0f..6331f861d 100644 --- a/programs/bpf/Cargo.lock +++ b/programs/bpf/Cargo.lock @@ -5379,6 +5379,7 @@ version = "1.11.0" dependencies = [ "log", "rustc_version", + "semver", "serde", "serde_derive", "solana-frozen-abi 1.11.0", diff --git a/rpc/src/rpc.rs b/rpc/src/rpc.rs index fcd893412..26563fdea 100644 --- a/rpc/src/rpc.rs +++ b/rpc/src/rpc.rs @@ -116,8 +116,10 @@ pub const PERFORMANCE_SAMPLES_LIMIT: usize = 720; const MAX_RPC_EPOCH_CREDITS_HISTORY: usize = 5; fn new_response(bank: &Bank, value: T) -> RpcResponse { - let context = RpcResponseContext { slot: bank.slot() }; - RpcResponse { context, value } + RpcResponse { + context: RpcResponseContext::new(bank.slot()), + value, + } } /// Wrapper for rpc return types of methods that provide responses both with and without context. @@ -774,7 +776,7 @@ impl JsonRpcRequestProcessor { if let Some((slot, accounts)) = self.get_cached_largest_accounts(&config.filter) { Ok(RpcResponse { - context: RpcResponseContext { slot }, + context: RpcResponseContext::new(slot), value: accounts, }) } else { @@ -4814,7 +4816,7 @@ pub mod tests { let expected = json!({ "jsonrpc": "2.0", "result": { - "context":{"slot":0}, + "context": {"slot": 0, "apiVersion": RpcApiVersion::default()}, "value":20, }, "id": 1, @@ -5211,7 +5213,7 @@ pub mod tests { ); let result: Value = parse_success_result(rpc.handle_request_sync(request)); let expected = json!({ - "context": {"slot": 0}, + "context": {"slot": 0, "apiVersion": RpcApiVersion::default()}, "value":{ "owner": "11111111111111111111111111111111", "lamports": 1_000_000, @@ -5396,7 +5398,7 @@ pub mod tests { let result: RpcResponse> = parse_success_result(rpc.handle_request_sync(request)); let expected = RpcResponse { - context: RpcResponseContext { slot: 0 }, + context: RpcResponseContext::new(0), value: expected_value, }; assert_eq!(result, expected); @@ -5562,7 +5564,7 @@ pub mod tests { let expected = json!({ "jsonrpc": "2.0", "result": { - "context":{"slot":0}, + "context": {"slot": 0, "apiVersion": RpcApiVersion::default()}, "value":{ "accounts": [ null, @@ -5658,7 +5660,7 @@ pub mod tests { let expected = json!({ "jsonrpc": "2.0", "result": { - "context":{"slot":0}, + "context": {"slot": 0, "apiVersion": RpcApiVersion::default()}, "value":{ "accounts":null, "err":null, @@ -5687,7 +5689,7 @@ pub mod tests { let expected = json!({ "jsonrpc": "2.0", "result": { - "context":{"slot":0}, + "context": {"slot": 0, "apiVersion": RpcApiVersion::default()}, "value":{ "accounts":null, "err":null, @@ -5740,7 +5742,7 @@ pub mod tests { let expected = json!({ "jsonrpc":"2.0", "result": { - "context":{"slot":0}, + "context": {"slot": 0, "apiVersion": RpcApiVersion::default()}, "value":{ "err":"BlockhashNotFound", "accounts":null, @@ -5767,7 +5769,7 @@ pub mod tests { let expected = json!({ "jsonrpc": "2.0", "result": { - "context":{"slot":0}, + "context": {"slot": 0, "apiVersion": RpcApiVersion::default()}, "value":{ "accounts":null, "err":null, @@ -5906,13 +5908,14 @@ pub mod tests { let expected = json!({ "jsonrpc": "2.0", "result": { - "context":{"slot":0}, - "value":{ - "blockhash": recent_blockhash.to_string(), - "feeCalculator": { - "lamportsPerSignature": 0, - } - }}, + "context": {"slot": 0, "apiVersion": RpcApiVersion::default()}, + "value":{ + "blockhash": recent_blockhash.to_string(), + "feeCalculator": { + "lamportsPerSignature": 0, + } + }, + }, "id": 1 }); let expected: Response = @@ -5934,7 +5937,7 @@ pub mod tests { let expected = json!({ "jsonrpc": "2.0", "result": { - "context": {"slot": 0}, + "context": {"slot": 0, "apiVersion": RpcApiVersion::default()}, "value": { "blockhash": recent_blockhash.to_string(), "feeCalculator": { @@ -5973,7 +5976,7 @@ pub mod tests { let expected = json!({ "jsonrpc": "2.0", "result": { - "context":{"slot":0}, + "context": {"slot": 0, "apiVersion": RpcApiVersion::default()}, "value":fee_calculator, }, "id": 1 @@ -5993,7 +5996,7 @@ pub mod tests { let expected = json!({ "jsonrpc": "2.0", "result": { - "context":{"slot":0}, + "context": {"slot": 0, "apiVersion": RpcApiVersion::default()}, "value":Value::Null, }, "id": 1 @@ -6014,16 +6017,17 @@ pub mod tests { let expected = json!({ "jsonrpc": "2.0", "result": { - "context":{"slot":0}, - "value":{ - "feeRateGovernor": { - "burnPercent": DEFAULT_BURN_PERCENT, - "maxLamportsPerSignature": 0, - "minLamportsPerSignature": 0, - "targetLamportsPerSignature": 0, - "targetSignaturesPerSlot": 0 - } - }}, + "context": {"slot": 0, "apiVersion": RpcApiVersion::default()}, + "value":{ + "feeRateGovernor": { + "burnPercent": DEFAULT_BURN_PERCENT, + "maxLamportsPerSignature": 0, + "minLamportsPerSignature": 0, + "targetLamportsPerSignature": 0, + "targetSignaturesPerSlot": 0 + } + }, + }, "id": 1 }); let expected: Response = diff --git a/rpc/src/rpc_subscriptions.rs b/rpc/src/rpc_subscriptions.rs index 6b2c401ff..e7459a957 100644 --- a/rpc/src/rpc_subscriptions.rs +++ b/rpc/src/rpc_subscriptions.rs @@ -1,4 +1,5 @@ //! The `pubsub` module implements a threaded subscription service on client RPC request + use { crate::{ optimistically_confirmed_bank_tracker::OptimisticallyConfirmedBank, @@ -16,7 +17,7 @@ use { serde::Serialize, solana_account_decoder::{parse_token::is_known_spl_token_id, UiAccount, UiAccountEncoding}, solana_client::rpc_response::{ - ProcessedSignatureResult, ReceivedSignatureResult, Response, RpcBlockUpdate, + ProcessedSignatureResult, ReceivedSignatureResult, Response as RpcResponse, RpcBlockUpdate, RpcBlockUpdateError, RpcKeyedAccount, RpcLogsResponse, RpcResponseContext, RpcSignatureResult, RpcVote, SlotInfo, SlotUpdate, }, @@ -153,10 +154,10 @@ where filter_results(results, params, *w_last_notified_slot, bank); for result in filter_results { notifier.notify( - Response { - context: RpcResponseContext { slot }, + RpcResponse::from(RpcNotificationResponse { + context: RpcNotificationContext { slot }, value: result, - }, + }), subscription, is_final, ); @@ -176,6 +177,33 @@ pub struct RpcNotification { pub created_at: Instant, } +#[derive(Debug, Clone, PartialEq)] +struct RpcNotificationResponse { + context: RpcNotificationContext, + value: T, +} + +impl From> for RpcResponse { + fn from(notification: RpcNotificationResponse) -> Self { + let RpcNotificationResponse { + context: RpcNotificationContext { slot }, + value, + } = notification; + Self { + context: RpcResponseContext { + slot, + api_version: None, + }, + value, + } + } +} + +#[derive(Debug, Clone, PartialEq)] +struct RpcNotificationContext { + slot: Slot, +} + const RPC_NOTIFICATIONS_METRICS_SUBMISSION_INTERVAL_MS: Duration = Duration::from_millis(2_000); struct RecentItems { @@ -839,12 +867,12 @@ impl RpcSubscriptions { { if params.enable_received_notification { notifier.notify( - Response { - context: RpcResponseContext { slot }, + RpcResponse::from(RpcNotificationResponse { + context: RpcNotificationContext { slot }, value: RpcSignatureResult::ReceivedSignature( ReceivedSignatureResult::ReceivedSignature, ), - }, + }), subscription, false, ); @@ -987,10 +1015,10 @@ impl RpcSubscriptions { Ok(block_update) => { if let Some(block_update) = block_update { notifier.notify( - Response { - context: RpcResponseContext { slot: s }, + RpcResponse::from(RpcNotificationResponse { + context: RpcNotificationContext { slot: s }, value: block_update, - }, + }), subscription, false, ); @@ -1004,14 +1032,14 @@ impl RpcSubscriptions { // we don't advance `w_last_unnotified_slot` so that // it'll retry on the next notification trigger notifier.notify( - Response { - context: RpcResponseContext { slot: s }, + RpcResponse::from(RpcNotificationResponse { + context: RpcNotificationContext { slot: s }, value: RpcBlockUpdate { slot, block: None, err: Some(err), }, - }, + }), subscription, false, ); diff --git a/version/Cargo.toml b/version/Cargo.toml index 0752ee88e..3d8619e97 100644 --- a/version/Cargo.toml +++ b/version/Cargo.toml @@ -11,6 +11,7 @@ edition = "2021" [dependencies] log = "0.4.17" +semver = "1.0.9" serde = "1.0.137" serde_derive = "1.0.103" solana-frozen-abi = { path = "../frozen-abi", version = "=1.11.0" } diff --git a/version/src/lib.rs b/version/src/lib.rs index 25d8fb256..8ab09ece5 100644 --- a/version/src/lib.rs +++ b/version/src/lib.rs @@ -29,6 +29,12 @@ pub struct Version { pub feature_set: u32, // first 4 bytes of the FeatureSet identifier } +impl Version { + pub fn as_semver_version(&self) -> semver::Version { + semver::Version::new(self.major as u64, self.minor as u64, self.patch as u64) + } +} + impl From for Version { fn from(legacy_version: LegacyVersion) -> Self { Self {