From bd0f8e172c47f59fe0252c86ee78ab3001a9061b Mon Sep 17 00:00:00 2001 From: Tony Arcieri Date: Fri, 19 Apr 2019 19:47:43 -0700 Subject: [PATCH] tendermint-rs: /status RPC endpoint Adds initial support for parsing JSON responses from the `/status` JSONRPC endpoint. --- tendermint-rs/src/channel.rs | 58 +++++++++ tendermint-rs/src/channel/id.rs | 24 ++++ tendermint-rs/src/lib.rs | 6 +- tendermint-rs/src/node.rs | 110 +----------------- tendermint-rs/src/node/id.rs | 107 +++++++++++++++++ tendermint-rs/src/node/info.rs | 88 ++++++++++++++ tendermint-rs/src/rpc.rs | 3 + tendermint-rs/src/rpc/endpoint.rs | 5 + tendermint-rs/src/rpc/endpoint/status.rs | 101 ++++++++++++++++ tendermint-rs/src/version.rs | 14 +++ tendermint-rs/tests/rpc.rs | 25 ++++ .../{integration.rs => secret_connection.rs} | 0 tendermint-rs/tests/support/status.json | 38 ++++++ 13 files changed, 474 insertions(+), 105 deletions(-) create mode 100644 tendermint-rs/src/channel.rs create mode 100644 tendermint-rs/src/channel/id.rs create mode 100644 tendermint-rs/src/node/id.rs create mode 100644 tendermint-rs/src/node/info.rs create mode 100644 tendermint-rs/src/rpc/endpoint.rs create mode 100644 tendermint-rs/src/rpc/endpoint/status.rs create mode 100644 tendermint-rs/src/version.rs create mode 100644 tendermint-rs/tests/rpc.rs rename tendermint-rs/tests/{integration.rs => secret_connection.rs} (100%) create mode 100644 tendermint-rs/tests/support/status.json diff --git a/tendermint-rs/src/channel.rs b/tendermint-rs/src/channel.rs new file mode 100644 index 0000000..d529721 --- /dev/null +++ b/tendermint-rs/src/channel.rs @@ -0,0 +1,58 @@ +//! Channels (RPC types) + +mod id; + +pub use self::id::Id; +use crate::rpc; +pub use serde::{Deserialize, Serialize}; +use std::fmt::{self, Display}; + +/// Channels +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Channel { + /// Channel ID + #[serde(rename = "ID")] + pub id: Id, + + /// Capacity of the send queue + #[serde( + rename = "SendQueueCapacity", + serialize_with = "rpc::response::serialize_u64", + deserialize_with = "rpc::response::parse_u64" + )] + pub send_queue_capacity: u64, + + /// Size of the send queue + #[serde( + rename = "SendQueueSize", + serialize_with = "rpc::response::serialize_u64", + deserialize_with = "rpc::response::parse_u64" + )] + pub send_queue_size: u64, + + /// Priority value + #[serde( + rename = "Priority", + serialize_with = "rpc::response::serialize_u64", + deserialize_with = "rpc::response::parse_u64" + )] + pub priority: u64, + + /// Amount of data recently sent + #[serde( + rename = "RecentlySent", + serialize_with = "rpc::response::serialize_u64", + deserialize_with = "rpc::response::parse_u64" + )] + pub recently_sent: u64, +} + +/// Channel collections +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct Channels(String); + +impl Display for Channels { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} diff --git a/tendermint-rs/src/channel/id.rs b/tendermint-rs/src/channel/id.rs new file mode 100644 index 0000000..bf1f2ca --- /dev/null +++ b/tendermint-rs/src/channel/id.rs @@ -0,0 +1,24 @@ +use serde::{Deserialize, Serialize}; + +/// Channel IDs +#[derive(Copy, Clone, Debug, Deserialize, Serialize)] +pub struct Id(pub u64); + +impl Id { + /// Get the current voting power as an integer + pub fn value(self) -> u64 { + self.0 + } +} + +impl From for u64 { + fn from(id: Id) -> u64 { + id.value() + } +} + +impl From for Id { + fn from(id: u64) -> Id { + Id(id) + } +} diff --git a/tendermint-rs/src/lib.rs b/tendermint-rs/src/lib.rs index 6582671..299f180 100644 --- a/tendermint-rs/src/lib.rs +++ b/tendermint-rs/src/lib.rs @@ -31,9 +31,11 @@ pub mod algorithm; pub mod amino_types; pub mod block; pub mod chain; +#[cfg(feature = "rpc")] +pub mod channel; pub mod error; pub mod hash; -pub mod moniker; +mod moniker; pub mod net; pub mod node; pub mod public_keys; @@ -42,6 +44,7 @@ pub mod rpc; #[cfg(feature = "secret-connection")] pub mod secret_connection; pub mod timestamp; +mod version; #[cfg(feature = "secret-connection")] pub use crate::secret_connection::SecretConnection; @@ -52,4 +55,5 @@ pub use crate::{ moniker::Moniker, public_keys::{PublicKey, TendermintKey}, timestamp::Timestamp, + version::Version, }; diff --git a/tendermint-rs/src/node.rs b/tendermint-rs/src/node.rs index 4a15732..923adae 100644 --- a/tendermint-rs/src/node.rs +++ b/tendermint-rs/src/node.rs @@ -1,107 +1,9 @@ //! Nodes in Tendermint blockchain networks -use crate::error::Error; -#[cfg(feature = "serde")] -use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; -use sha2::{Digest, Sha256}; -use signatory::ed25519; -use std::{ - fmt::{self, Display}, - str::FromStr, -}; -use subtle::{self, ConstantTimeEq}; -use subtle_encoding::hex; +mod id; +#[cfg(feature = "rpc")] +pub mod info; -/// Size of a Node ID in bytes -pub const ID_LENGTH: usize = 20; - -/// Node IDs -#[derive(Copy, Clone, Debug, Hash)] -pub struct Id([u8; ID_LENGTH]); - -impl Id { - /// Create a new Node ID from raw bytes - pub fn new(bytes: [u8; ID_LENGTH]) -> Id { - Id(bytes) - } - - /// Borrow the node ID as a byte slice - pub fn as_bytes(&self) -> &[u8] { - &self.0[..] - } -} - -impl AsRef<[u8]> for Id { - fn as_ref(&self) -> &[u8] { - self.as_bytes() - } -} - -impl ConstantTimeEq for Id { - #[inline] - fn ct_eq(&self, other: &Id) -> subtle::Choice { - self.as_bytes().ct_eq(other.as_bytes()) - } -} - -impl Display for Id { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - for byte in &self.0 { - write!(f, "{:02X}", byte)?; - } - Ok(()) - } -} - -impl From for Id { - fn from(pk: ed25519::PublicKey) -> Id { - let digest = Sha256::digest(pk.as_bytes()); - let mut bytes = [0u8; ID_LENGTH]; - bytes.copy_from_slice(&digest[..ID_LENGTH]); - Id(bytes) - } -} - -/// Decode Node ID from hex -impl FromStr for Id { - type Err = Error; - - fn from_str(s: &str) -> Result { - // Accept either upper or lower case hex - let bytes = hex::decode_upper(s) - .or_else(|_| hex::decode(s)) - .map_err(|_| Error::Parse)?; - - if bytes.len() != ID_LENGTH { - return Err(Error::Parse); - } - - let mut result_bytes = [0u8; ID_LENGTH]; - result_bytes.copy_from_slice(&bytes); - Ok(Id(result_bytes)) - } -} - -#[cfg(feature = "serde")] -impl<'de> Deserialize<'de> for Id { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let s = String::deserialize(deserializer)?; - Self::from_str(&s).map_err(|_| { - de::Error::custom(format!( - "expected {}-character hex string, got {:?}", - ID_LENGTH * 2, - s - )) - }) - } -} - -#[cfg(feature = "serde")] -impl Serialize for Id { - fn serialize(&self, serializer: S) -> Result { - self.to_string().serialize(serializer) - } -} +pub use self::id::Id; +#[cfg(feature = "rpc")] +pub use self::info::Info; diff --git a/tendermint-rs/src/node/id.rs b/tendermint-rs/src/node/id.rs new file mode 100644 index 0000000..99c4f1e --- /dev/null +++ b/tendermint-rs/src/node/id.rs @@ -0,0 +1,107 @@ +//! Tendermint node IDs + +use crate::error::Error; +#[cfg(feature = "serde")] +use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; +use sha2::{Digest, Sha256}; +use signatory::ed25519; +use std::{ + fmt::{self, Display}, + str::FromStr, +}; +use subtle::{self, ConstantTimeEq}; +use subtle_encoding::hex; + +/// Size of a Node ID in bytes +pub const ID_LENGTH: usize = 20; + +/// Node IDs +#[derive(Copy, Clone, Debug, Hash)] +pub struct Id([u8; ID_LENGTH]); + +impl Id { + /// Create a new Node ID from raw bytes + pub fn new(bytes: [u8; ID_LENGTH]) -> Id { + Id(bytes) + } + + /// Borrow the node ID as a byte slice + pub fn as_bytes(&self) -> &[u8] { + &self.0[..] + } +} + +impl AsRef<[u8]> for Id { + fn as_ref(&self) -> &[u8] { + self.as_bytes() + } +} + +impl ConstantTimeEq for Id { + #[inline] + fn ct_eq(&self, other: &Id) -> subtle::Choice { + self.as_bytes().ct_eq(other.as_bytes()) + } +} + +impl Display for Id { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for byte in &self.0 { + write!(f, "{:02X}", byte)?; + } + Ok(()) + } +} + +impl From for Id { + fn from(pk: ed25519::PublicKey) -> Id { + let digest = Sha256::digest(pk.as_bytes()); + let mut bytes = [0u8; ID_LENGTH]; + bytes.copy_from_slice(&digest[..ID_LENGTH]); + Id(bytes) + } +} + +/// Decode Node ID from hex +impl FromStr for Id { + type Err = Error; + + fn from_str(s: &str) -> Result { + // Accept either upper or lower case hex + let bytes = hex::decode_upper(s) + .or_else(|_| hex::decode(s)) + .map_err(|_| Error::Parse)?; + + if bytes.len() != ID_LENGTH { + return Err(Error::Parse); + } + + let mut result_bytes = [0u8; ID_LENGTH]; + result_bytes.copy_from_slice(&bytes); + Ok(Id(result_bytes)) + } +} + +#[cfg(feature = "serde")] +impl<'de> Deserialize<'de> for Id { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + Self::from_str(&s).map_err(|_| { + de::Error::custom(format!( + "expected {}-character hex string, got {:?}", + ID_LENGTH * 2, + s + )) + }) + } +} + +#[cfg(feature = "serde")] +impl Serialize for Id { + fn serialize(&self, serializer: S) -> Result { + self.to_string().serialize(serializer) + } +} diff --git a/tendermint-rs/src/node/info.rs b/tendermint-rs/src/node/info.rs new file mode 100644 index 0000000..46e8eeb --- /dev/null +++ b/tendermint-rs/src/node/info.rs @@ -0,0 +1,88 @@ +//! Node information (used in RPC responses) + +use crate::{chain, channel::Channels, net, node, rpc, Moniker, Version}; +use serde::{Deserialize, Serialize}; + +/// Node information +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Info { + /// Protocol version information + pub protocol_version: ProtocolVersionInfo, + + /// Node ID + pub id: node::Id, + + /// Listen address + pub listen_addr: net::Address, + + /// Tendermint network / chain ID, + pub network: chain::Id, + + /// Tendermint version + pub version: Version, + + /// Channels + pub channels: Channels, + + /// Moniker + pub moniker: Moniker, + + /// Other status information + pub other: OtherInfo, +} + +/// Protocol version information +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct ProtocolVersionInfo { + /// P2P protocol version + #[serde( + serialize_with = "rpc::response::serialize_u64", + deserialize_with = "rpc::response::parse_u64" + )] + pub p2p: u64, + + /// Block version + #[serde( + serialize_with = "rpc::response::serialize_u64", + deserialize_with = "rpc::response::parse_u64" + )] + pub block: u64, + + /// App version + #[serde( + serialize_with = "rpc::response::serialize_u64", + deserialize_with = "rpc::response::parse_u64" + )] + pub app: u64, +} + +/// Other information +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct OtherInfo { + /// TX index status + pub tx_index: TxIndexStatus, + + /// RPC address + pub rpc_address: net::Address, +} + +/// Transaction index status +#[derive(Copy, Clone, Debug, Deserialize, Serialize)] +pub enum TxIndexStatus { + /// Index is on + #[serde(rename = "on")] + On, + + /// Index is off + #[serde(rename = "off")] + Off, +} + +impl From for bool { + fn from(status: TxIndexStatus) -> bool { + match status { + TxIndexStatus::On => true, + TxIndexStatus::Off => false, + } + } +} diff --git a/tendermint-rs/src/rpc.rs b/tendermint-rs/src/rpc.rs index 87ed2c6..ebf08c1 100644 --- a/tendermint-rs/src/rpc.rs +++ b/tendermint-rs/src/rpc.rs @@ -2,5 +2,8 @@ //! //! Wraps the RPC API described at: +pub mod endpoint; pub mod request; pub mod response; + +pub use self::{request::Request, response::Response}; diff --git a/tendermint-rs/src/rpc/endpoint.rs b/tendermint-rs/src/rpc/endpoint.rs new file mode 100644 index 0000000..79b9a79 --- /dev/null +++ b/tendermint-rs/src/rpc/endpoint.rs @@ -0,0 +1,5 @@ +//! Tendermint JSONRPC endpoints + +mod status; + +pub use status::{StatusRequest, StatusResponse}; diff --git a/tendermint-rs/src/rpc/endpoint/status.rs b/tendermint-rs/src/rpc/endpoint/status.rs new file mode 100644 index 0000000..ffefe34 --- /dev/null +++ b/tendermint-rs/src/rpc/endpoint/status.rs @@ -0,0 +1,101 @@ +//! `/status` endpoint JSONRPC wrapper + +use crate::{account, block, node, rpc, Hash, PublicKey, Timestamp}; +use serde::{de::Error as DeError, Deserialize, Deserializer, Serialize, Serializer}; + +/// Node status request +#[derive(Default)] +pub struct StatusRequest; + +impl rpc::Request for StatusRequest { + type Response = StatusResponse; + + fn path(&self) -> rpc::request::Path { + "/status".parse().unwrap() + } +} + +/// Status responses +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct StatusResponse { + /// Node information + pub node_info: node::Info, + + /// Sync information + pub sync_info: SyncInfo, + + /// Validator information + pub validator_info: ValidatorInfo, +} + +impl rpc::Response for StatusResponse {} + +/// Sync information +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct SyncInfo { + /// Latest block hash + pub latest_block_hash: Hash, + + /// Latest app hash + pub latest_app_hash: Hash, + + /// Latest block height + pub latest_block_height: block::Height, + + /// Latest block time + pub latest_block_time: Timestamp, + + /// Are we catching up? + pub catching_up: bool, +} + +/// Validator information +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct ValidatorInfo { + /// Validator account address + pub address: account::Id, + + /// Validator public key + pub pub_key: PublicKey, + + /// Validator voting power + pub voting_power: VotingPower, +} + +/// Voting power +#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] +pub struct VotingPower(u64); + +impl VotingPower { + /// Get the current voting power + pub fn value(self) -> u64 { + self.0 + } + + /// Is the current voting power zero? + pub fn is_zero(self) -> bool { + self.0 == 0 + } +} + +impl From for u64 { + fn from(power: VotingPower) -> u64 { + power.0 + } +} + +impl<'de> Deserialize<'de> for VotingPower { + fn deserialize>(deserializer: D) -> Result { + Ok(VotingPower( + String::deserialize(deserializer)? + .parse() + .map_err(|e| D::Error::custom(format!("{}", e)))?, + )) + } +} + +impl Serialize for VotingPower { + fn serialize(&self, serializer: S) -> Result { + self.0.to_string().serialize(serializer) + } +} diff --git a/tendermint-rs/src/version.rs b/tendermint-rs/src/version.rs new file mode 100644 index 0000000..6c00a21 --- /dev/null +++ b/tendermint-rs/src/version.rs @@ -0,0 +1,14 @@ +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; +use std::fmt::{self, Display}; + +/// Tendermint version +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Version(String); + +impl Display for Version { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} diff --git a/tendermint-rs/tests/rpc.rs b/tendermint-rs/tests/rpc.rs new file mode 100644 index 0000000..91c6817 --- /dev/null +++ b/tendermint-rs/tests/rpc.rs @@ -0,0 +1,25 @@ +//! Tendermint RPC tests + +#[cfg(feature = "rpc")] +mod endpoints { + use std::{fs, path::PathBuf}; + use tendermint::rpc::{endpoint::StatusResponse, Response}; + + fn read_json_fixture(name: &str) -> String { + fs::read_to_string(PathBuf::from("./tests/support/").join(name.to_owned() + ".json")) + .unwrap() + } + + #[test] + fn status() { + let status_json = read_json_fixture("status"); + let status_response = StatusResponse::from_json(&status_json).unwrap(); + + assert_eq!(status_response.node_info.network.as_str(), "cosmoshub-1"); + assert_eq!( + status_response.sync_info.latest_block_height.value(), + 410744 + ); + assert_eq!(status_response.validator_info.voting_power.value(), 0); + } +} diff --git a/tendermint-rs/tests/integration.rs b/tendermint-rs/tests/secret_connection.rs similarity index 100% rename from tendermint-rs/tests/integration.rs rename to tendermint-rs/tests/secret_connection.rs diff --git a/tendermint-rs/tests/support/status.json b/tendermint-rs/tests/support/status.json new file mode 100644 index 0000000..abe08d2 --- /dev/null +++ b/tendermint-rs/tests/support/status.json @@ -0,0 +1,38 @@ +{ + "jsonrpc": "2.0", + "id": "", + "result": { + "node_info": { + "protocol_version": { + "p2p": "7", + "block": "10", + "app": "0" + }, + "id": "6b90d376f9bfdd83c6d9351bf7b2f458b74deacc", + "listen_addr": "tcp://0.0.0.0:26656", + "network": "cosmoshub-1", + "version": "0.30.1", + "channels": "4020212223303800", + "moniker": "technodrome", + "other": { + "tx_index": "on", + "rpc_address": "tcp://0.0.0.0:26657" + } + }, + "sync_info": { + "latest_block_hash": "D4B11143B0C9CB1330BAED825C9FEF13979C91E137DF93C3974A17C9BED663ED", + "latest_app_hash": "38FE3F06E3EB936C2EE14DA6BEA15F97FEF8814824F022EE06635D7B2C39A0BA", + "latest_block_height": "410744", + "latest_block_time": "2019-04-15T13:16:17.316509229Z", + "catching_up": false + }, + "validator_info": { + "address": "C73833E9BD86D34EDAD4AFD571FB5D0926294CD5", + "pub_key": { + "type": "tendermint/PubKeyEd25519", + "value": "RblzMO4is5L1hZz6wo4kPbptzOyue6LTk4+lPhD1FRk=" + }, + "voting_power": "0" + } + } +}