From ea751ae1a1ffd6469663c5bfd0c72ac6567f4470 Mon Sep 17 00:00:00 2001 From: Tony Arcieri Date: Fri, 19 Apr 2019 18:49:48 -0700 Subject: [PATCH] tendermint-rs: Initial "rpc" feature and JSONRPC types Adds a set of JSONRPC request and response types, designed to construct JSON requests to the Tendermint RPC (HTTP) API, and parse the returned JSON responses. --- .circleci/config.yml | 1 + src/client.rs | 18 ++-- src/config/validator.rs | 4 +- tendermint-rs/Cargo.toml | 7 +- tendermint-rs/src/lib.rs | 15 ++-- tendermint-rs/src/{address.rs => net.rs} | 0 tendermint-rs/src/rpc.rs | 6 ++ tendermint-rs/src/rpc/request.rs | 36 ++++++++ tendermint-rs/src/rpc/response.rs | 104 +++++++++++++++++++++++ 9 files changed, 171 insertions(+), 20 deletions(-) rename tendermint-rs/src/{address.rs => net.rs} (100%) create mode 100644 tendermint-rs/src/rpc.rs create mode 100644 tendermint-rs/src/rpc/request.rs create mode 100644 tendermint-rs/src/rpc/response.rs diff --git a/.circleci/config.yml b/.circleci/config.yml index a071148..109f156 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -40,6 +40,7 @@ jobs: rustc --version cargo --version cargo test --all --all-features -- --test-threads 1 + cd tendermint-rs && cargo test --release --features=rpc - run: name: audit command: | diff --git a/src/client.rs b/src/client.rs index e338ade..83ce8fd 100644 --- a/src/client.rs +++ b/src/client.rs @@ -13,15 +13,17 @@ use crate::{ }; use signatory::{ed25519, Decode, Encode, PublicKeyed}; use signatory_dalek::Ed25519Signer; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::Arc; use std::{ panic, path::Path, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, thread::{self, JoinHandle}, time::Duration, }; -use tendermint::{chain, node, secret_connection, Address}; +use tendermint::{chain, net, node, secret_connection}; /// How long to wait after a crash before respawning (in seconds) pub const RESPAWN_DELAY: u64 = 1; @@ -67,13 +69,13 @@ fn client_loop(config: ValidatorConfig, should_term: &Arc) { return; } - let session_result = match &addr { - Address::Tcp { + let session_result = match addr { + net::Address::Tcp { peer_id, - host, + ref host, port, } => match &secret_key { - Some(path) => tcp_session(chain_id, *peer_id, host, *port, path, should_term), + Some(path) => tcp_session(chain_id, peer_id, host, port, path, should_term), None => { error!( "config error: missing field `secret_key` for validator {}", @@ -82,7 +84,7 @@ fn client_loop(config: ValidatorConfig, should_term: &Arc) { return; } }, - Address::Unix { path } => unix_session(chain_id, path, should_term), + net::Address::Unix { ref path } => unix_session(chain_id, path, should_term), }; if let Err(e) = session_result { diff --git a/src/config/validator.rs b/src/config/validator.rs index e41e1ca..d704ff5 100644 --- a/src/config/validator.rs +++ b/src/config/validator.rs @@ -1,11 +1,11 @@ use std::path::PathBuf; -use tendermint::{chain, Address}; +use tendermint::{chain, net}; /// Validator configuration #[derive(Clone, Deserialize, Debug)] pub struct ValidatorConfig { /// Address of the validator (`tcp://` or `unix://`) - pub addr: Address, + pub addr: net::Address, /// Chain ID of the Tendermint network this validator is part of pub chain_id: chain::Id, diff --git a/tendermint-rs/Cargo.toml b/tendermint-rs/Cargo.toml index e32bff5..6e936a1 100644 --- a/tendermint-rs/Cargo.toml +++ b/tendermint-rs/Cargo.toml @@ -12,8 +12,9 @@ edition = "2018" description = """ Tendermint is a high-performance blockchain consensus engine that powers Byzantine fault tolerant applications written in any programming language. - This crate provides types for representing information about Tendermint - blockchain networks, including chain IDs, block IDs, and block heights. + This crate provides core types for representing information about Tendermint + blockchain networks, including chain information types, secret connections, + and remote procedure calls (JSONRPC). """ authors = [ @@ -38,6 +39,7 @@ prost-amino-derive = { version = "0.4.0", optional = true } rand_os = { version = "0.1", optional = true } ring = { version = "0.14", optional = true } serde = { version = "1", optional = true, features = ["derive"] } +serde_json = { version = "1", optional = true } signatory = { version = "0.11.2", features = ["ed25519", "ecdsa"] } signatory-dalek = { version = "0.11", optional = true } sha2 = { version = "0.8", default-features = false } @@ -53,6 +55,7 @@ serde_json = "1" [features] default = ["serde", "tai64"] amino-types = ["prost-amino", "prost-amino-derive"] +rpc = ["serde", "serde_json"] secret-connection = [ "amino-types", "byteorder", diff --git a/tendermint-rs/src/lib.rs b/tendermint-rs/src/lib.rs index af7effb..6582671 100644 --- a/tendermint-rs/src/lib.rs +++ b/tendermint-rs/src/lib.rs @@ -26,7 +26,6 @@ extern crate prost_amino as prost; extern crate prost_amino_derive as prost_derive; pub mod account; -pub mod address; pub mod algorithm; #[cfg(feature = "amino-types")] pub mod amino_types; @@ -35,8 +34,11 @@ pub mod chain; pub mod error; pub mod hash; pub mod moniker; +pub mod net; pub mod node; pub mod public_keys; +#[cfg(feature = "rpc")] +pub mod rpc; #[cfg(feature = "secret-connection")] pub mod secret_connection; pub mod timestamp; @@ -44,13 +46,10 @@ pub mod timestamp; #[cfg(feature = "secret-connection")] pub use crate::secret_connection::SecretConnection; pub use crate::{ - address::*, - algorithm::*, - block::{ParseHeight as ParseBlockHeight, ParseId as ParseBlockId}, - chain::ParseId as ParseChainId, + algorithm::{HashAlgorithm, SignatureAlgorithm}, error::Error, - hash::*, + hash::Hash, moniker::Moniker, - public_keys::*, - timestamp::*, + public_keys::{PublicKey, TendermintKey}, + timestamp::Timestamp, }; diff --git a/tendermint-rs/src/address.rs b/tendermint-rs/src/net.rs similarity index 100% rename from tendermint-rs/src/address.rs rename to tendermint-rs/src/net.rs diff --git a/tendermint-rs/src/rpc.rs b/tendermint-rs/src/rpc.rs new file mode 100644 index 0000000..87ed2c6 --- /dev/null +++ b/tendermint-rs/src/rpc.rs @@ -0,0 +1,6 @@ +//! Tendermint RPC: JSONRPC over HTTP support +//! +//! Wraps the RPC API described at: + +pub mod request; +pub mod response; diff --git a/tendermint-rs/src/rpc/request.rs b/tendermint-rs/src/rpc/request.rs new file mode 100644 index 0000000..04c94a7 --- /dev/null +++ b/tendermint-rs/src/rpc/request.rs @@ -0,0 +1,36 @@ +//! JSONRPC requests + +use failure::Error; +use serde::{Deserialize, Serialize}; +use std::{ + fmt::{self, Display}, + str::FromStr, +}; + +/// JSONRPC requests +pub trait Request { + /// Response type for this command + type Response: super::response::Response; + + /// Path for this request + fn path(&self) -> Path; +} + +/// JSONRPC request paths +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct Path(String); + +impl Display for Path { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl FromStr for Path { + type Err = Error; + + /// Parse a request path from a string + fn from_str(path: &str) -> Result { + Ok(Path(path.to_owned())) + } +} diff --git a/tendermint-rs/src/rpc/response.rs b/tendermint-rs/src/rpc/response.rs new file mode 100644 index 0000000..1f40a98 --- /dev/null +++ b/tendermint-rs/src/rpc/response.rs @@ -0,0 +1,104 @@ +//! JSONRPC response types + +// TODO(tarcieri): remove this when functions below are all used +#![allow(dead_code)] + +use failure::{format_err, Error}; +use serde::{ + de::{DeserializeOwned, Error as DeError}, + Deserialize, Deserializer, Serialize, Serializer, +}; +use std::{ + fmt::{self, Display}, + time::Duration, +}; + +/// JSONRPC responses +pub trait Response: Serialize + DeserializeOwned + Sized { + /// Parse a JSONRPC response from a JSON string + fn from_json(response: &str) -> Result { + let wrapper: ResponseWrapper = + serde_json::from_str(response).map_err(|e| format_err!("error parsing JSON: {}", e))?; + + // TODO(tarcieri): check JSONRPC version/ID? + Ok(wrapper.result) + } +} + +/// JSONRPC response wrapper (i.e. message envelope) +#[derive(Debug, Deserialize, Serialize)] +pub struct ResponseWrapper { + /// JSONRPC version + pub jsonrpc: Version, + + /// ID + pub id: Id, + + /// Result + pub result: R, +} + +/// JSONRPC version +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct Version(String); + +impl Display for Version { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +/// JSONRPC ID +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct Id(String); + +impl AsRef for Id { + fn as_ref(&self) -> &str { + self.0.as_ref() + } +} + +/// Parse `u64` from a JSON string +pub(crate) fn parse_u64<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + String::deserialize(deserializer)? + .parse::() + .map_err(|e| D::Error::custom(format!("{}", e))) +} + +/// Serialize `u64` as a JSON string +#[allow(clippy::trivially_copy_pass_by_ref)] +pub(crate) fn serialize_u64(value: &u64, serializer: S) -> Result +where + S: Serializer, +{ + format!("{}", value).serialize(serializer) +} + +/// Parse `Duration` from a JSON string containing a nanosecond count +fn parse_duration<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + // TODO(tarcieri): handle 64-bit overflow? + let nanos = String::deserialize(deserializer)? + .parse::() + .map_err(|e| D::Error::custom(format!("{}", e)))?; + + Ok(Duration::from_nanos(nanos)) +} + +/// Serialize `Duration` as a JSON string containing a nanosecond count +fn serialize_duration(duration: &Duration, serializer: S) -> Result +where + S: Serializer, +{ + // TODO(tarcieri): use `as_nanos` when we're Rust 1.33+ + format!( + "{}", + (duration.as_secs() * 1_000_000_000) + duration.subsec_nanos() as u64 + ) + .serialize(serializer) +}