diff --git a/Cargo.lock b/Cargo.lock index e46df1e07d..fed2590a48 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4685,6 +4685,7 @@ dependencies = [ "serde", "serde_derive", "solana-logger 1.4.0", + "solana-runtime", "solana-sdk 1.4.0", "solana-sdk-macro-frozen-abi 1.4.0", ] diff --git a/cli/src/cluster_query.rs b/cli/src/cluster_query.rs index 4b036406a6..1e04780130 100644 --- a/cli/src/cluster_query.rs +++ b/cli/src/cluster_query.rs @@ -17,7 +17,7 @@ use solana_client::{ pubsub_client::PubsubClient, rpc_client::{GetConfirmedSignaturesForAddress2Config, RpcClient}, rpc_config::{RpcLargestAccountsConfig, RpcLargestAccountsFilter}, - rpc_response::{RpcVersionInfo, SlotInfo}, + rpc_response::SlotInfo, }; use solana_remote_wallet::remote_wallet::RemoteWalletManager; use solana_sdk::{ @@ -1314,12 +1314,9 @@ pub fn process_show_validators( for contact_info in rpc_client.get_cluster_nodes()? { node_version.insert( contact_info.pubkey, - RpcVersionInfo { - solana_core: contact_info - .version - .unwrap_or_else(|| unknown_version.clone()), - } - .to_string(), + contact_info + .version + .unwrap_or_else(|| unknown_version.clone()), ); } diff --git a/client/src/rpc_response.rs b/client/src/rpc_response.rs index b98bc57a1f..94ef19c449 100644 --- a/client/src/rpc_response.rs +++ b/client/src/rpc_response.rs @@ -121,6 +121,7 @@ pub enum ReceivedSignatureResult { } #[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] pub struct RpcContactInfo { /// Pubkey of the node as a base-58 string pub pubkey: String, @@ -132,6 +133,8 @@ pub struct RpcContactInfo { pub rpc: Option, /// Software version pub version: Option, + /// First 4 bytes of the FeatureSet identifier + pub feature_set: Option, } /// Map of leader base58 identity pubkeys to the slot indices relative to the first epoch slot @@ -142,6 +145,8 @@ pub type RpcLeaderSchedule = HashMap>; pub struct RpcVersionInfo { /// The current version of solana-core pub solana_core: String, + /// first 4 bytes of the FeatureSet identifier + pub feature_set: Option, } impl fmt::Debug for RpcVersionInfo { diff --git a/core/src/cluster_info.rs b/core/src/cluster_info.rs index 52711867da..7d1b6140cf 100644 --- a/core/src/cluster_info.rs +++ b/core/src/cluster_info.rs @@ -359,7 +359,7 @@ pub fn make_accounts_hashes_message( } // TODO These messages should go through the gpu pipeline for spam filtering -#[frozen_abi(digest = "CnN1gW2K2TRydGc84eYnQJwdTADPjQf6LJLZ4RP1QeoH")] +#[frozen_abi(digest = "3ZHQscZ9SgxKh45idzHv3hiagyyPRtDgeySmJn171PTi")] #[derive(Serialize, Deserialize, Debug, AbiEnumVisitor, AbiExample)] #[allow(clippy::large_enum_variant)] enum Protocol { @@ -573,7 +573,7 @@ impl ClusterInfo { } let ip_addr = node.gossip.ip(); Some(format!( - "{:15} {:2}| {:5} | {:44} |{:^15}| {:5}| {:5}| {:5}| {:5}| {:5}| {:5}| {:5}| {:5}| {:5}| {:5}| {}\n", + "{:15} {:2}| {:5} | {:44} |{:^9}| {:5}| {:5}| {:5}| {:5}| {:5}| {:5}| {:5}| {:5}| {:5}| {:5}| {}\n", if ContactInfo::is_valid_address(&node.gossip) { ip_addr.to_string() } else { @@ -605,8 +605,8 @@ impl ClusterInfo { format!( "IP Address |Age(ms)| Node identifier \ - | Version |Gossip| TPU |TPUfwd| TVU |TVUfwd|Repair|ServeR| RPC |PubSub|ShredVer\n\ - ------------------+-------+----------------------------------------------+---------------+\ + | Version |Gossip| TPU |TPUfwd| TVU |TVUfwd|Repair|ServeR| RPC |PubSub|ShredVer\n\ + ------------------+-------+----------------------------------------------+---------+\ ------+------+------+------+------+------+------+------+------+--------\n\ {}\ Nodes: {}{}{}", @@ -894,7 +894,8 @@ impl ClusterInfo { } pub fn get_node_version(&self, pubkey: &Pubkey) -> Option { - self.gossip + let version = self + .gossip .read() .unwrap() .crds @@ -902,7 +903,21 @@ impl ClusterInfo { .get(&CrdsValueLabel::Version(*pubkey)) .map(|x| x.value.version()) .flatten() - .map(|version| version.version.clone()) + .map(|version| version.version.clone()); + + if version.is_none() { + self.gossip + .read() + .unwrap() + .crds + .table + .get(&CrdsValueLabel::LegacyVersion(*pubkey)) + .map(|x| x.value.legacy_version()) + .flatten() + .map(|version| version.version.clone().into()) + } else { + version + } } /// all validators that have a valid rpc port regardless of `shred_version`. diff --git a/core/src/crds_value.rs b/core/src/crds_value.rs index 52fb8ebaaf..de925bcffb 100644 --- a/core/src/crds_value.rs +++ b/core/src/crds_value.rs @@ -75,6 +75,7 @@ pub enum CrdsData { SnapshotHashes(SnapshotHash), AccountsHashes(SnapshotHash), EpochSlots(EpochSlotsIndex, EpochSlots), + LegacyVersion(LegacyVersion), Version(Version), } @@ -102,6 +103,7 @@ impl Sanitize for CrdsData { } val.sanitize() } + CrdsData::LegacyVersion(version) => version.sanitize(), CrdsData::Version(version) => version.sanitize(), } } @@ -208,6 +210,23 @@ impl Vote { } } +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, AbiExample)] +pub struct LegacyVersion { + pub from: Pubkey, + pub wallclock: u64, + pub version: solana_version::LegacyVersion, +} + +impl Sanitize for LegacyVersion { + fn sanitize(&self) -> Result<(), SanitizeError> { + if self.wallclock >= MAX_WALLCLOCK { + return Err(SanitizeError::ValueOutOfBounds); + } + self.from.sanitize()?; + self.version.sanitize() + } +} + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, AbiExample)] pub struct Version { pub from: Pubkey, @@ -245,6 +264,7 @@ pub enum CrdsValueLabel { SnapshotHashes(Pubkey), EpochSlots(EpochSlotsIndex, Pubkey), AccountsHashes(Pubkey), + LegacyVersion(Pubkey), Version(Pubkey), } @@ -257,6 +277,7 @@ impl fmt::Display for CrdsValueLabel { CrdsValueLabel::SnapshotHashes(_) => write!(f, "SnapshotHash({})", self.pubkey()), CrdsValueLabel::EpochSlots(ix, _) => write!(f, "EpochSlots({}, {})", ix, self.pubkey()), CrdsValueLabel::AccountsHashes(_) => write!(f, "AccountsHashes({})", self.pubkey()), + CrdsValueLabel::LegacyVersion(_) => write!(f, "LegacyVersion({})", self.pubkey()), CrdsValueLabel::Version(_) => write!(f, "Version({})", self.pubkey()), } } @@ -271,6 +292,7 @@ impl CrdsValueLabel { CrdsValueLabel::SnapshotHashes(p) => *p, CrdsValueLabel::EpochSlots(_, p) => *p, CrdsValueLabel::AccountsHashes(p) => *p, + CrdsValueLabel::LegacyVersion(p) => *p, CrdsValueLabel::Version(p) => *p, } } @@ -300,6 +322,7 @@ impl CrdsValue { CrdsData::SnapshotHashes(hash) => hash.wallclock, CrdsData::AccountsHashes(hash) => hash.wallclock, CrdsData::EpochSlots(_, p) => p.wallclock, + CrdsData::LegacyVersion(version) => version.wallclock, CrdsData::Version(version) => version.wallclock, } } @@ -311,6 +334,7 @@ impl CrdsValue { CrdsData::SnapshotHashes(hash) => hash.from, CrdsData::AccountsHashes(hash) => hash.from, CrdsData::EpochSlots(_, p) => p.from, + CrdsData::LegacyVersion(version) => version.from, CrdsData::Version(version) => version.from, } } @@ -322,6 +346,7 @@ impl CrdsValue { CrdsData::SnapshotHashes(_) => CrdsValueLabel::SnapshotHashes(self.pubkey()), CrdsData::AccountsHashes(_) => CrdsValueLabel::AccountsHashes(self.pubkey()), CrdsData::EpochSlots(ix, _) => CrdsValueLabel::EpochSlots(*ix, self.pubkey()), + CrdsData::LegacyVersion(_) => CrdsValueLabel::LegacyVersion(self.pubkey()), CrdsData::Version(_) => CrdsValueLabel::Version(self.pubkey()), } } @@ -373,6 +398,13 @@ impl CrdsValue { } } + pub fn legacy_version(&self) -> Option<&LegacyVersion> { + match &self.data { + CrdsData::LegacyVersion(legacy_version) => Some(legacy_version), + _ => None, + } + } + pub fn version(&self) -> Option<&Version> { match &self.data { CrdsData::Version(version) => Some(version), @@ -387,6 +419,7 @@ impl CrdsValue { CrdsValueLabel::LowestSlot(*key), CrdsValueLabel::SnapshotHashes(*key), CrdsValueLabel::AccountsHashes(*key), + CrdsValueLabel::LegacyVersion(*key), CrdsValueLabel::Version(*key), ]; labels.extend((0..MAX_VOTES).map(|ix| CrdsValueLabel::Vote(ix, *key))); @@ -438,7 +471,7 @@ mod test { #[test] fn test_labels() { - let mut hits = [false; 5 + MAX_VOTES as usize + MAX_EPOCH_SLOTS as usize]; + let mut hits = [false; 6 + MAX_VOTES as usize + MAX_EPOCH_SLOTS as usize]; // this method should cover all the possible labels for v in &CrdsValue::record_labels(&Pubkey::default()) { match v { @@ -446,10 +479,11 @@ mod test { CrdsValueLabel::LowestSlot(_) => hits[1] = true, CrdsValueLabel::SnapshotHashes(_) => hits[2] = true, CrdsValueLabel::AccountsHashes(_) => hits[3] = true, - CrdsValueLabel::Version(_) => hits[4] = true, - CrdsValueLabel::Vote(ix, _) => hits[*ix as usize + 5] = true, + CrdsValueLabel::LegacyVersion(_) => hits[4] = true, + CrdsValueLabel::Version(_) => hits[5] = true, + CrdsValueLabel::Vote(ix, _) => hits[*ix as usize + 6] = true, CrdsValueLabel::EpochSlots(ix, _) => { - hits[*ix as usize + MAX_VOTES as usize + 5] = true + hits[*ix as usize + MAX_VOTES as usize + 6] = true } } } diff --git a/core/src/rpc.rs b/core/src/rpc.rs index c4d137a27b..983efe6ed1 100644 --- a/core/src/rpc.rs +++ b/core/src/rpc.rs @@ -1945,14 +1945,19 @@ impl RpcSol for RpcSolImpl { if my_shred_version == contact_info.shred_version && ContactInfo::is_valid_address(&contact_info.gossip) { + let (version, feature_set) = + if let Some(version) = cluster_info.get_node_version(&contact_info.id) { + (Some(version.to_string()), Some(version.feature_set)) + } else { + (None, None) + }; Some(RpcContactInfo { pubkey: contact_info.id.to_string(), gossip: Some(contact_info.gossip), tpu: valid_address_or_none(&contact_info.tpu), rpc: valid_address_or_none(&contact_info.rpc), - version: cluster_info - .get_node_version(&contact_info.id) - .map(|v| v.to_string()), + version, + feature_set, }) } else { None // Exclude spy nodes @@ -2299,8 +2304,10 @@ impl RpcSol for RpcSolImpl { fn get_version(&self, _: Self::Metadata) -> Result { debug!("get_version rpc request received"); + let version = solana_version::Version::default(); Ok(RpcVersionInfo { - solana_core: solana_version::Version::default().to_string(), + solana_core: version.to_string(), + feature_set: Some(version.feature_set), }) } @@ -2843,7 +2850,7 @@ pub mod tests { .expect("actual response deserialization"); let expected = format!( - r#"{{"jsonrpc":"2.0","result":[{{"pubkey": "{}", "gossip": "127.0.0.1:1235", "tpu": "127.0.0.1:1234", "rpc": "127.0.0.1:{}", "version": null}}],"id":1}}"#, + r#"{{"jsonrpc":"2.0","result":[{{"pubkey": "{}", "gossip": "127.0.0.1:1235", "tpu": "127.0.0.1:1234", "rpc": "127.0.0.1:{}", "version": null, "featureSet": null}}],"id":1}}"#, leader_pubkey, rpc_port::DEFAULT_RPC_PORT ); @@ -4394,10 +4401,12 @@ pub mod tests { let req = r#"{"jsonrpc":"2.0","id":1,"method":"getVersion"}"#; let res = io.handle_request_sync(&req, meta); + let version = solana_version::Version::default(); let expected = json!({ "jsonrpc": "2.0", "result": { - "solana-core": solana_version::version!().to_string() + "solana-core": version.to_string(), + "feature-set": version.feature_set, }, "id": 1 }); diff --git a/version/Cargo.toml b/version/Cargo.toml index 592c9834f1..e1eb088acc 100644 --- a/version/Cargo.toml +++ b/version/Cargo.toml @@ -14,6 +14,7 @@ serde = "1.0.112" serde_derive = "1.0.103" solana-logger = { path = "../logger", version = "1.4.0" } solana-sdk = { path = "../sdk", version = "1.4.0" } +solana-runtime = { path = "../runtime", version = "1.4.0" } solana-sdk-macro-frozen-abi = { path = "../sdk/macro-frozen-abi", version = "1.4.0" } [lib] diff --git a/version/src/lib.rs b/version/src/lib.rs index 7e74c60579..958b023ef5 100644 --- a/version/src/lib.rs +++ b/version/src/lib.rs @@ -3,19 +3,42 @@ extern crate serde_derive; use serde_derive::{Deserialize, Serialize}; use solana_sdk::sanitize::Sanitize; -use std::fmt; - +use std::{convert::TryInto, fmt}; #[macro_use] extern crate solana_sdk_macro_frozen_abi; +// Older version structure used by 1.3.12 and earlier releases #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, AbiExample)] -pub struct Version { +pub struct LegacyVersion { major: u16, minor: u16, patch: u16, commit: Option, // first 4 bytes of the sha1 commit hash } +impl Sanitize for LegacyVersion {} + +#[derive(Serialize, Deserialize, Clone, PartialEq, AbiExample)] +pub struct Version { + pub major: u16, + pub minor: u16, + pub patch: u16, + pub commit: Option, // first 4 bytes of the sha1 commit hash + pub feature_set: u32, // first 4 bytes of the FeatureSet identifier +} + +impl From for Version { + fn from(legacy_version: LegacyVersion) -> Self { + Self { + major: legacy_version.major, + minor: legacy_version.minor, + patch: legacy_version.patch, + commit: legacy_version.commit, + feature_set: 0, + } + } +} + fn compute_commit(sha1: Option<&'static str>) -> Option { let sha1 = sha1?; if sha1.len() < 8 { @@ -27,27 +50,42 @@ fn compute_commit(sha1: Option<&'static str>) -> Option { impl Default for Version { fn default() -> Self { + let feature_set = u32::from_le_bytes( + solana_runtime::feature_set::FeatureSet::default() + .id + .as_ref()[..4] + .try_into() + .unwrap(), + ); Self { major: env!("CARGO_PKG_VERSION_MAJOR").parse().unwrap(), minor: env!("CARGO_PKG_VERSION_MINOR").parse().unwrap(), patch: env!("CARGO_PKG_VERSION_PATCH").parse().unwrap(), commit: compute_commit(option_env!("CI_COMMIT")), + feature_set, } } } impl fmt::Display for Version { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}.{}.{}", self.major, self.minor, self.patch,) + } +} + +impl fmt::Debug for Version { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, - "{}.{}.{} {}", + "{}.{}.{} (src:{}; feat:{})", self.major, self.minor, self.patch, match self.commit { None => "devbuild".to_string(), Some(commit) => format!("{:08x}", commit), - } + }, + self.feature_set, ) } }