openethereum/crates/net/network/src/client_version.rs

570 lines
18 KiB
Rust

// Copyright 2015-2020 Parity Technologies (UK) Ltd.
// This file is part of OpenEthereum.
// OpenEthereum is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// OpenEthereum is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with OpenEthereum. If not, see <http://www.gnu.org/licenses/>.
#![warn(missing_docs)]
//! Parse ethereum client ID strings and provide querying functionality
use semver::Version;
use std::fmt;
/// Parity client string prefix
const LEGACY_CLIENT_ID_PREFIX: &str = "Parity-Ethereum";
const CURRENT_CLIENT_ID_PREFIX: &str = "OpenEthereum";
lazy_static! {
/// Parity versions starting from this will accept block bodies requests
/// of 256 bodies
static ref PARITY_CLIENT_LARGE_REQUESTS_VERSION: Version = Version::parse("2.4.0").unwrap();
}
/// Description of the software version running in a peer
/// according to https://github.com/ethereum/wiki/wiki/Client-Version-Strings
/// This structure as it is represents the format used by Parity clients. Other
/// vendors may provide additional fields.
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct ParityClientData {
name: String,
identity: Option<String>,
semver: Version,
os: String,
compiler: String,
// Capability flags, should be calculated in constructor
can_handle_large_requests: bool,
}
/// Accessor methods for ParityClientData. This will probably
/// need to be abstracted away into a trait.
impl ParityClientData {
fn new(
name: String,
identity: Option<String>,
semver: Version,
os: String,
compiler: String,
) -> Self {
// Flags logic
let can_handle_large_requests = &semver >= &PARITY_CLIENT_LARGE_REQUESTS_VERSION;
// Instantiate and return
ParityClientData {
name: name,
identity: identity,
semver: semver,
os: os,
compiler: compiler,
can_handle_large_requests: can_handle_large_requests,
}
}
fn name(&self) -> &str {
self.name.as_str()
}
fn identity(&self) -> Option<&str> {
self.identity.as_ref().map(String::as_str)
}
fn semver(&self) -> &Version {
&self.semver
}
fn os(&self) -> &str {
self.os.as_str()
}
fn compiler(&self) -> &str {
self.compiler.as_str()
}
fn can_handle_large_requests(&self) -> bool {
self.can_handle_large_requests
}
}
/// Enum describing the version of the software running on a peer.
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub enum ClientVersion {
/// The peer runs software from parity and the string format is known
ParityClient(
/// The actual information fields: name, version, os, ...
ParityClientData,
),
/// The string ID is recognized as Parity but the overall format
/// could not be parsed
ParityUnknownFormat(String),
/// Other software vendors than Parity
Other(String),
}
impl Default for ClientVersion {
fn default() -> Self {
ClientVersion::Other("".to_owned())
}
}
/// Provide information about what a particular version of a
/// peer software can do
pub trait ClientCapabilities {
/// Parity versions before PARITY_CLIENT_LARGE_REQUESTS_VERSION would not
/// check the accumulated size of a packet when building a response to a
/// GET_BLOCK_BODIES request. If the packet was larger than a given limit,
/// instead of sending fewer blocks no packet would get sent at all. Query
/// if this version can handle requests for a large number of block bodies.
fn can_handle_large_requests(&self) -> bool;
/// Service transactions are specific to parity and nethermind. Query if
/// this version accepts them.
fn accepts_service_transaction(&self) -> bool;
}
impl ClientCapabilities for ClientVersion {
fn can_handle_large_requests(&self) -> bool {
match self {
ClientVersion::ParityClient(data) => data.can_handle_large_requests(),
ClientVersion::ParityUnknownFormat(_) => false, // Play it safe
ClientVersion::Other(_) => true, // As far as we know
}
}
fn accepts_service_transaction(&self) -> bool {
match self {
ClientVersion::ParityClient(_) => true,
ClientVersion::ParityUnknownFormat(_) => true,
ClientVersion::Other(client_id) => is_nethermind(client_id),
}
}
}
fn is_parity(client_id: &str) -> bool {
client_id.starts_with(LEGACY_CLIENT_ID_PREFIX)
|| client_id.starts_with(CURRENT_CLIENT_ID_PREFIX)
}
fn is_nethermind(client_id: &str) -> bool {
client_id.starts_with("Nethermind")
}
/// Parse known parity formats. Recognizes either a short format with four fields
/// or a long format which includes the same fields and an identity one.
fn parse_parity_format(client_version: &str) -> Result<ParityClientData, ()> {
const PARITY_ID_STRING_MINIMUM_TOKENS: usize = 4;
let tokens: Vec<&str> = client_version.split("/").collect();
if tokens.len() < PARITY_ID_STRING_MINIMUM_TOKENS {
return Err(());
}
let name = tokens[0];
let identity = if tokens.len() - 3 > 1 {
Some(tokens[1..(tokens.len() - 3)].join("/"))
} else {
None
};
let compiler = tokens[tokens.len() - 1];
let os = tokens[tokens.len() - 2];
// If version is in the right position and valid format return a valid
// result. Otherwise return an error.
get_number_from_version(tokens[tokens.len() - 3])
.and_then(|v| Version::parse(v).ok())
.map(|semver| {
ParityClientData::new(
name.to_owned(),
identity,
semver,
os.to_owned(),
compiler.to_owned(),
)
})
.ok_or(())
}
/// Parse a version string and return the corresponding
/// ClientVersion. Only Parity clients are destructured right now, other
/// strings will just get wrapped in a variant so that the information is
/// not lost.
/// The parsing for parity may still fail, in which case return a ParityUnknownFormat with
/// the original version string. TryFrom would be a better trait to implement.
impl<T> From<T> for ClientVersion
where
T: AsRef<str>,
{
fn from(client_version: T) -> Self {
let client_version_str: &str = client_version.as_ref();
if !is_parity(client_version_str) {
return ClientVersion::Other(client_version_str.to_owned());
}
if let Ok(data) = parse_parity_format(client_version_str) {
ClientVersion::ParityClient(data)
} else {
ClientVersion::ParityUnknownFormat(client_version_str.to_owned())
}
}
}
fn format_parity_version_string(
client_version: &ParityClientData,
f: &mut fmt::Formatter,
) -> std::fmt::Result {
let name = client_version.name();
let semver = client_version.semver();
let os = client_version.os();
let compiler = client_version.compiler();
match client_version.identity() {
None => write!(f, "{}/v{}/{}/{}", name, semver, os, compiler),
Some(identity) => write!(f, "{}/{}/v{}/{}/{}", name, identity, semver, os, compiler),
}
}
impl fmt::Display for ClientVersion {
fn fmt(&self, f: &mut fmt::Formatter) -> std::fmt::Result {
match self {
ClientVersion::ParityClient(data) => format_parity_version_string(data, f),
ClientVersion::ParityUnknownFormat(id) => write!(f, "{}", id),
ClientVersion::Other(id) => write!(f, "{}", id),
}
}
}
fn get_number_from_version(version: &str) -> Option<&str> {
if version.starts_with("v") {
return version.get(1..);
}
None
}
#[cfg(test)]
pub mod tests {
use super::*;
const PARITY_CLIENT_SEMVER: &str = "2.4.0";
const PARITY_CLIENT_OLD_SEMVER: &str = "2.2.0";
const PARITY_CLIENT_OS: &str = "linux";
const PARITY_CLIENT_COMPILER: &str = "rustc";
const PARITY_CLIENT_IDENTITY: &str = "ExpanseSOLO";
const PARITY_CLIENT_MULTITOKEN_IDENTITY: &str = "ExpanseSOLO/abc/v1.2.3";
fn make_default_version_string() -> String {
format!(
"{}/v{}/{}/{}",
CURRENT_CLIENT_ID_PREFIX,
PARITY_CLIENT_SEMVER,
PARITY_CLIENT_OS,
PARITY_CLIENT_COMPILER
)
}
fn make_default_long_version_string() -> String {
format!(
"{}/{}/v{}/{}/{}",
CURRENT_CLIENT_ID_PREFIX,
PARITY_CLIENT_IDENTITY,
PARITY_CLIENT_SEMVER,
PARITY_CLIENT_OS,
PARITY_CLIENT_COMPILER
)
}
fn make_multitoken_identity_long_version_string() -> String {
format!(
"{}/{}/v{}/{}/{}",
CURRENT_CLIENT_ID_PREFIX,
PARITY_CLIENT_MULTITOKEN_IDENTITY,
PARITY_CLIENT_SEMVER,
PARITY_CLIENT_OS,
PARITY_CLIENT_COMPILER
)
}
fn make_old_semver_version_string() -> String {
format!(
"{}/v{}/{}/{}",
CURRENT_CLIENT_ID_PREFIX,
PARITY_CLIENT_OLD_SEMVER,
PARITY_CLIENT_OS,
PARITY_CLIENT_COMPILER
)
}
#[test]
pub fn client_version_when_from_empty_string_then_default() {
let default = ClientVersion::default();
assert_eq!(ClientVersion::from(""), default);
}
#[test]
pub fn get_number_from_version_when_valid_then_number() {
let version_string = format!("v{}", PARITY_CLIENT_SEMVER);
assert_eq!(
get_number_from_version(&version_string).unwrap(),
PARITY_CLIENT_SEMVER
);
}
#[test]
pub fn client_version_when_str_parity_format_and_valid_then_all_fields_match() {
let client_version_string = make_default_version_string();
if let ClientVersion::ParityClient(client_version) =
ClientVersion::from(client_version_string.as_str())
{
assert_eq!(client_version.name(), CURRENT_CLIENT_ID_PREFIX);
assert_eq!(
*client_version.semver(),
Version::parse(PARITY_CLIENT_SEMVER).unwrap()
);
assert_eq!(client_version.os(), PARITY_CLIENT_OS);
assert_eq!(client_version.compiler(), PARITY_CLIENT_COMPILER);
} else {
panic!("shouldn't be here");
}
}
#[test]
pub fn client_version_when_str_parity_long_format_and_valid_then_all_fields_match() {
let client_version_string = make_default_long_version_string();
if let ClientVersion::ParityClient(client_version) =
ClientVersion::from(client_version_string.as_str())
{
assert_eq!(client_version.name(), CURRENT_CLIENT_ID_PREFIX);
assert_eq!(client_version.identity().unwrap(), PARITY_CLIENT_IDENTITY);
assert_eq!(
*client_version.semver(),
Version::parse(PARITY_CLIENT_SEMVER).unwrap()
);
assert_eq!(client_version.os(), PARITY_CLIENT_OS);
assert_eq!(client_version.compiler(), PARITY_CLIENT_COMPILER);
} else {
panic!("shouldnt be here");
}
}
#[test]
pub fn client_version_when_str_parity_long_format_and_valid_and_identity_multiple_tokens_then_all_fields_match(
) {
let client_version_string = make_multitoken_identity_long_version_string();
if let ClientVersion::ParityClient(client_version) =
ClientVersion::from(client_version_string.as_str())
{
assert_eq!(client_version.name(), CURRENT_CLIENT_ID_PREFIX);
assert_eq!(
client_version.identity().unwrap(),
PARITY_CLIENT_MULTITOKEN_IDENTITY
);
assert_eq!(
*client_version.semver(),
Version::parse(PARITY_CLIENT_SEMVER).unwrap()
);
assert_eq!(client_version.os(), PARITY_CLIENT_OS);
assert_eq!(client_version.compiler(), PARITY_CLIENT_COMPILER);
} else {
panic!("shouldnt be here");
}
}
#[test]
pub fn client_version_when_string_parity_format_and_valid_then_all_fields_match() {
let client_version_string: String = make_default_version_string();
if let ClientVersion::ParityClient(client_version) =
ClientVersion::from(client_version_string.as_str())
{
assert_eq!(client_version.name(), CURRENT_CLIENT_ID_PREFIX);
assert_eq!(
*client_version.semver(),
Version::parse(PARITY_CLIENT_SEMVER).unwrap()
);
assert_eq!(client_version.os(), PARITY_CLIENT_OS);
assert_eq!(client_version.compiler(), PARITY_CLIENT_COMPILER);
} else {
panic!("shouldn't be here");
}
}
#[test]
pub fn client_version_when_parity_format_and_invalid_then_equals_parity_unknown_client_version_string(
) {
// This is invalid because version has no leading 'v'
let client_version_string = format!(
"{}/{}/{}/{}",
CURRENT_CLIENT_ID_PREFIX,
PARITY_CLIENT_SEMVER,
PARITY_CLIENT_OS,
PARITY_CLIENT_COMPILER
);
let client_version = ClientVersion::from(client_version_string.as_str());
let parity_unknown = ClientVersion::ParityUnknownFormat(client_version_string.to_string());
assert_eq!(client_version, parity_unknown);
}
#[test]
pub fn client_version_when_parity_format_without_identity_and_missing_compiler_field_then_equals_parity_unknown_client_version_string(
) {
let client_version_string = format!(
"{}/v{}/{}",
CURRENT_CLIENT_ID_PREFIX, PARITY_CLIENT_SEMVER, PARITY_CLIENT_OS,
);
let client_version = ClientVersion::from(client_version_string.as_str());
let parity_unknown = ClientVersion::ParityUnknownFormat(client_version_string.to_string());
assert_eq!(client_version, parity_unknown);
}
#[test]
pub fn client_version_when_parity_format_with_identity_and_missing_compiler_field_then_equals_parity_unknown_client_version_string(
) {
let client_version_string = format!(
"{}/{}/v{}/{}",
CURRENT_CLIENT_ID_PREFIX,
PARITY_CLIENT_IDENTITY,
PARITY_CLIENT_SEMVER,
PARITY_CLIENT_OS,
);
let client_version = ClientVersion::from(client_version_string.as_str());
let parity_unknown = ClientVersion::ParityUnknownFormat(client_version_string.to_string());
assert_eq!(client_version, parity_unknown);
}
#[test]
pub fn client_version_when_not_parity_format_and_valid_then_other_with_client_version_string() {
let client_version_string = "Geth/main.jnode.network/v1.8.21-stable-9dc5d1a9/linux";
let client_version = ClientVersion::from(client_version_string);
assert_eq!(
client_version,
ClientVersion::Other(client_version_string.to_string())
);
}
#[test]
pub fn client_version_when_parity_format_and_valid_then_to_string_equal() {
let client_version_string: String = make_default_version_string();
let client_version = ClientVersion::from(client_version_string.as_str());
assert_eq!(client_version.to_string(), client_version_string);
}
#[test]
pub fn client_version_when_other_then_to_string_equal_input_string() {
let client_version_string: String = "Other".to_string();
let client_version = ClientVersion::from("Other");
assert_eq!(client_version.to_string(), client_version_string);
}
#[test]
pub fn client_capabilities_when_parity_old_version_then_handles_large_requests_false() {
let client_version_string: String = make_old_semver_version_string();
let client_version = ClientVersion::from(client_version_string.as_str());
assert!(!client_version.can_handle_large_requests());
}
#[test]
pub fn client_capabilities_when_parity_beta_version_then_not_handles_large_requests_true() {
let client_version_string: String = format!(
"{}/v{}/{}/{}",
"Parity-Ethereum", "2.4.0-beta", "x86_64-linux-gnu", "rustc1.31.1"
)
.to_string();
let client_version = ClientVersion::from(client_version_string.as_str());
assert!(!client_version.can_handle_large_requests());
}
#[test]
pub fn client_version_when_to_owned_then_both_objects_equal() {
let client_version_string: String = make_old_semver_version_string();
let origin = ClientVersion::from(client_version_string.as_str());
let borrowed = &origin;
let owned = origin.to_owned();
assert_eq!(*borrowed, owned);
}
#[test]
fn client_version_accepts_service_transaction_for_different_versions() {
assert!(!ClientVersion::from("Geth").accepts_service_transaction());
assert!(
ClientVersion::from("Parity-Ethereum/v2.6.0/linux/rustc").accepts_service_transaction()
);
assert!(
ClientVersion::from("Parity-Ethereum/ABCDEFGH/v2.7.3/linux/rustc")
.accepts_service_transaction()
);
assert!(
ClientVersion::from("OpenEthereum//v3.2.0/x86_64-linux-gnu/rustc1.49.0")
.accepts_service_transaction()
);
assert!(ClientVersion::from("OpenEthereum/ABCDEFGH").accepts_service_transaction());
assert!(
ClientVersion::from("Nethermind/v1.10.37-0-068e5c399-20210311/X64-Linux/5.0.4")
.accepts_service_transaction()
)
}
#[test]
fn is_parity_when_parity_then_true() {
let client_id = format!("{}/", CURRENT_CLIENT_ID_PREFIX);
assert!(is_parity(&client_id));
}
#[test]
fn is_parity_when_empty_then_false() {
let client_id = "";
assert!(!is_parity(&client_id));
}
#[test]
fn is_parity_when_other_then_false() {
let client_id = "other";
assert!(!is_parity(&client_id));
}
}