diff --git a/.circleci/config.yml b/.circleci/config.yml index 41317f6..28ce21e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -27,7 +27,7 @@ jobs: cargo build --features=yubihsm-server cargo build --features=ledgertm cargo build --features=yubihsm-server,ledgertm,softsign - cd tendermint-rs && cargo build --no-default-features && cargo build --features=integration --tests + cd tendermint-rs && cargo build --no-default-features - run: name: build --release command: | @@ -43,7 +43,7 @@ jobs: rustc --version cargo --version cargo test --all-features -- --test-threads 1 - cd tendermint-rs && cargo test --release --features=amino-types,rpc,secret-connection + cd tendermint-rs && cargo test --release --all-features - run: name: audit command: | diff --git a/Cargo.lock b/Cargo.lock index d2b308b..166bf5e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1332,6 +1332,7 @@ dependencies = [ "subtle 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "subtle-encoding 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", "tai64 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "toml 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", "uuid 0.7.4 (registry+https://github.com/rust-lang/crates.io-index)", "x25519-dalek 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", "zeroize 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/src/error.rs b/src/error.rs index 9c4d707..2afc938 100644 --- a/src/error.rs +++ b/src/error.rs @@ -135,18 +135,18 @@ impl From for Error { impl From for Error { fn from(other: tendermint::error::Error) -> Self { - let kind = match other { - tendermint::Error::Crypto => ErrorKind::CryptoError, - tendermint::Error::InvalidKey => ErrorKind::InvalidKey, - tendermint::Error::Io => ErrorKind::IoError, - tendermint::Error::Protocol => ErrorKind::ProtocolError, - tendermint::Error::Length - | tendermint::Error::Parse - | tendermint::Error::OutOfRange => ErrorKind::ParseError, - tendermint::Error::SignatureInvalid => ErrorKind::VerificationError, + let kind = match other.kind() { + tendermint::ErrorKind::Crypto => ErrorKind::CryptoError, + tendermint::ErrorKind::InvalidKey => ErrorKind::InvalidKey, + tendermint::ErrorKind::Io => ErrorKind::IoError, + tendermint::ErrorKind::Protocol => ErrorKind::ProtocolError, + tendermint::ErrorKind::Length + | tendermint::ErrorKind::Parse + | tendermint::ErrorKind::OutOfRange => ErrorKind::ParseError, + tendermint::ErrorKind::SignatureInvalid => ErrorKind::VerificationError, }; - abscissa_core::Error::new(kind, None).into() + abscissa_core::Error::new(kind, other.msg().map(|s| s.to_owned())).into() } } diff --git a/tendermint-rs/Cargo.toml b/tendermint-rs/Cargo.toml index 3585e83..327cc14 100644 --- a/tendermint-rs/Cargo.toml +++ b/tendermint-rs/Cargo.toml @@ -47,6 +47,7 @@ sha2 = { version = "0.8", default-features = false } subtle = "2" subtle-encoding = { version = "0.3", features = ["bech32-preview"] } tai64 = { version = "2", optional = true, features = ["chrono"] } +toml = { version = "0.5", optional = true } uuid = { version = "0.7", optional = true, default-features = false } x25519-dalek = { version = "0.5", optional = true, default-features = false, features = ["u64_backend"] } zeroize = { version = "0.9", optional = true } @@ -57,7 +58,7 @@ serde_json = "1" [features] default = ["serde", "tai64"] amino-types = ["prost-amino", "prost-amino-derive"] -integration = [] +config = ["serde", "serde_json", "toml"] rpc = ["hyper", "rand_os", "serde", "serde_json", "uuid"] secret-connection = [ "amino-types", diff --git a/tendermint-rs/src/abci.rs b/tendermint-rs/src/abci.rs index 41f3631..79efc57 100644 --- a/tendermint-rs/src/abci.rs +++ b/tendermint-rs/src/abci.rs @@ -23,6 +23,8 @@ mod path; mod proof; #[cfg(feature = "rpc")] mod responses; +#[cfg(any(feature = "config", feature = "rpc"))] +pub mod tag; pub mod transaction; #[cfg(feature = "rpc")] diff --git a/tendermint-rs/src/abci/data.rs b/tendermint-rs/src/abci/data.rs index dc9c06e..a52a6e9 100644 --- a/tendermint-rs/src/abci/data.rs +++ b/tendermint-rs/src/abci/data.rs @@ -1,4 +1,4 @@ -use crate::Error; +use crate::{Error, ErrorKind}; use serde::{de::Error as _, Deserialize, Deserializer, Serialize, Serializer}; use std::{ fmt::{self, Display}, @@ -42,7 +42,7 @@ impl FromStr for Data { // Accept either upper or lower case hex let bytes = hex::decode_upper(s) .or_else(|_| hex::decode(s)) - .map_err(|_| Error::Parse)?; + .map_err(|_| ErrorKind::Parse)?; Ok(Data(bytes)) } diff --git a/tendermint-rs/src/abci/gas.rs b/tendermint-rs/src/abci/gas.rs index 7010ce4..ec59e55 100644 --- a/tendermint-rs/src/abci/gas.rs +++ b/tendermint-rs/src/abci/gas.rs @@ -5,7 +5,7 @@ //! //! -use crate::Error; +use crate::{Error, ErrorKind}; use serde::{de::Error as _, Deserialize, Deserializer, Serialize, Serializer}; use std::{ fmt::{self, Display}, @@ -45,7 +45,7 @@ impl FromStr for Gas { type Err = Error; fn from_str(s: &str) -> Result { - Ok(Self::from(s.parse::().map_err(|_| Error::Parse)?)) + Ok(Self::from(s.parse::().map_err(|_| ErrorKind::Parse)?)) } } diff --git a/tendermint-rs/src/abci/log.rs b/tendermint-rs/src/abci/log.rs index 9286f66..91c6275 100644 --- a/tendermint-rs/src/abci/log.rs +++ b/tendermint-rs/src/abci/log.rs @@ -1,5 +1,5 @@ #[cfg(feature = "serde_json")] -use crate::Error; +use crate::{Error, ErrorKind}; use serde::{Deserialize, Serialize}; use std::fmt::{self, Display}; @@ -11,7 +11,7 @@ impl Log { /// Parse the log data as JSON, returning a `serde_json::Value` #[cfg(feature = "serde_json")] pub fn parse_json(&self) -> Result { - serde_json::from_str(&self.0).map_err(|_| Error::Parse) + serde_json::from_str(&self.0).map_err(|_| ErrorKind::Parse.into()) } } diff --git a/tendermint-rs/src/abci/responses.rs b/tendermint-rs/src/abci/responses.rs index 46cb8f0..ac28b2f 100644 --- a/tendermint-rs/src/abci/responses.rs +++ b/tendermint-rs/src/abci/responses.rs @@ -1,6 +1,6 @@ //! ABCI response types used by the `/block_results` RPC endpoint. -use super::{code::Code, data::Data, gas::Gas, info::Info, log::Log}; +use super::{code::Code, data::Data, gas::Gas, info::Info, log::Log, tag::Tag}; use crate::{consensus, validator}; use serde::{Deserialize, Deserializer, Serialize}; use std::fmt::{self, Display}; @@ -117,16 +117,6 @@ where Ok(Option::deserialize(deserializer)?.unwrap_or_default()) } -/// Tags -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct Tag { - /// Key - pub key: String, - - /// Value - pub value: String, -} - /// Codespace #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] pub struct Codespace(String); diff --git a/tendermint-rs/src/abci/tag.rs b/tendermint-rs/src/abci/tag.rs new file mode 100644 index 0000000..a7b313e --- /dev/null +++ b/tendermint-rs/src/abci/tag.rs @@ -0,0 +1,63 @@ +//! Tags + +use crate::error::Error; +use serde::{Deserialize, Serialize}; +use std::{fmt, str::FromStr}; + +/// Tags +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Tag { + /// Key + pub key: Key, + + /// Value + pub value: Value, +} + +/// Tag keys +#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, PartialOrd, Ord, Serialize)] +pub struct Key(String); + +impl AsRef for Key { + fn as_ref(&self) -> &str { + self.0.as_ref() + } +} + +impl FromStr for Key { + type Err = Error; + + fn from_str(s: &str) -> Result { + Ok(Key(s.into())) + } +} + +impl fmt::Display for Key { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", &self.0) + } +} + +/// Tag values +#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +pub struct Value(String); + +impl AsRef for Value { + fn as_ref(&self) -> &str { + self.0.as_ref() + } +} + +impl FromStr for Value { + type Err = Error; + + fn from_str(s: &str) -> Result { + Ok(Value(s.into())) + } +} + +impl fmt::Display for Value { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", &self.0) + } +} diff --git a/tendermint-rs/src/abci/transaction/hash.rs b/tendermint-rs/src/abci/transaction/hash.rs index 237e5ca..3bbb316 100644 --- a/tendermint-rs/src/abci/transaction/hash.rs +++ b/tendermint-rs/src/abci/transaction/hash.rs @@ -1,6 +1,6 @@ //! Transaction hashes -use crate::error::Error; +use crate::error::{Error, ErrorKind}; #[cfg(feature = "serde")] use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; use std::{ @@ -65,10 +65,10 @@ impl FromStr for Hash { // Accept either upper or lower case hex let bytes = hex::decode_upper(s) .or_else(|_| hex::decode(s)) - .map_err(|_| Error::Parse)?; + .map_err(|_| ErrorKind::Parse)?; if bytes.len() != LENGTH { - return Err(Error::Parse); + Err(ErrorKind::Parse)?; } let mut result_bytes = [0u8; LENGTH]; diff --git a/tendermint-rs/src/account.rs b/tendermint-rs/src/account.rs index 169214f..6873729 100644 --- a/tendermint-rs/src/account.rs +++ b/tendermint-rs/src/account.rs @@ -1,6 +1,6 @@ //! Tendermint accounts -use crate::error::Error; +use crate::error::{Error, ErrorKind}; #[cfg(feature = "serde")] use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; use sha2::{Digest, Sha256}; @@ -87,10 +87,10 @@ impl FromStr for Id { // Accept either upper or lower case hex let bytes = hex::decode_upper(s) .or_else(|_| hex::decode(s)) - .map_err(|_| Error::Parse)?; + .map_err(|_| ErrorKind::Parse)?; if bytes.len() != LENGTH { - return Err(Error::Parse); + Err(ErrorKind::Parse)?; } let mut result_bytes = [0u8; LENGTH]; diff --git a/tendermint-rs/src/block/height.rs b/tendermint-rs/src/block/height.rs index 10a6557..05cb3e3 100644 --- a/tendermint-rs/src/block/height.rs +++ b/tendermint-rs/src/block/height.rs @@ -1,4 +1,4 @@ -use crate::error::Error; +use crate::error::{Error, ErrorKind}; #[cfg(feature = "serde")] use serde::{de::Error as _, Deserialize, Deserializer, Serialize, Serializer}; use std::{ @@ -20,7 +20,7 @@ impl Height { if n > 0 { Ok(Height(n)) } else { - Err(Error::OutOfRange) + Err(ErrorKind::OutOfRange.into()) } } @@ -86,7 +86,7 @@ impl FromStr for Height { type Err = Error; fn from_str(s: &str) -> Result { - Self::try_from_u64(s.parse::().map_err(|_| Error::Parse)?) + Self::try_from_u64(s.parse::().map_err(|_| ErrorKind::Parse)?) } } diff --git a/tendermint-rs/src/block/size.rs b/tendermint-rs/src/block/size.rs index f7533bf..d789f1a 100644 --- a/tendermint-rs/src/block/size.rs +++ b/tendermint-rs/src/block/size.rs @@ -29,4 +29,14 @@ pub struct Size { ) )] pub max_gas: u64, + + /// Time iota in ms + #[cfg_attr( + feature = "serde", + serde( + serialize_with = "serializers::serialize_u64", + deserialize_with = "serializers::parse_u64" + ) + )] + pub time_iota_ms: u64, } diff --git a/tendermint-rs/src/chain/id.rs b/tendermint-rs/src/chain/id.rs index adcb84f..7d0ec83 100644 --- a/tendermint-rs/src/chain/id.rs +++ b/tendermint-rs/src/chain/id.rs @@ -1,6 +1,6 @@ //! Tendermint blockchain identifiers -use crate::error::Error; +use crate::error::{Error, ErrorKind}; #[cfg(feature = "serde")] use serde::{de::Error as _, Deserialize, Deserializer, Serialize, Serializer}; use std::{ @@ -62,13 +62,13 @@ impl FromStr for Id { /// Parses string to create a new chain ID fn from_str(name: &str) -> Result { if name.is_empty() || name.len() > MAX_LENGTH { - return Err(Error::Length); + Err(ErrorKind::Length)?; } for byte in name.as_bytes() { match byte { b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'-' | b'_' => (), - _ => return Err(Error::Parse), + _ => Err(ErrorKind::Parse)?, } } @@ -144,12 +144,15 @@ mod tests { #[test] fn rejects_empty_chain_ids() { - assert_eq!("".parse::(), Err(Error::Length)) + assert_eq!(*"".parse::().err().unwrap().kind(), ErrorKind::Length); } #[test] fn rejects_overlength_chain_ids() { let overlong_id = String::from_utf8(vec![b'x'; MAX_LENGTH + 1]).unwrap(); - assert_eq!(overlong_id.parse::(), Err(Error::Length)) + assert_eq!( + *overlong_id.parse::().err().unwrap().kind(), + ErrorKind::Length + ); } } diff --git a/tendermint-rs/src/config.rs b/tendermint-rs/src/config.rs new file mode 100644 index 0000000..f9f2980 --- /dev/null +++ b/tendermint-rs/src/config.rs @@ -0,0 +1,614 @@ +//! Tendermint configuration file types (with serde parsers/serializers) +//! +//! This module contains types which correspond to the following config files: +//! +//! - `config.toml`: `config::TendermintConfig` +//! - `node_key.rs`: `config::node_key::NodeKey` +//! - `priv_validator_key.rs`: `config::priv_validator_key::PrivValidatorKey` + +mod node_key; +mod priv_validator_key; + +pub use self::{node_key::NodeKey, priv_validator_key::PrivValidatorKey}; + +use crate::{ + abci::tag, + error::{Error, ErrorKind}, + net, node, Moniker, Timeout, +}; +use serde::{de, de::Error as _, ser, Deserialize, Serialize}; +use std::{ + collections::BTreeMap, + fmt, fs, + path::{Path, PathBuf}, + str::FromStr, +}; + +/// Tendermint `config.toml` file +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct TendermintConfig { + /// TCP or UNIX socket address of the ABCI application, + /// or the name of an ABCI application compiled in with the Tendermint binary. + pub proxy_app: net::Address, + + /// A custom human readable name for this node + pub moniker: Moniker, + + /// If this node is many blocks behind the tip of the chain, FastSync + /// allows them to catchup quickly by downloading blocks in parallel + /// and verifying their commits + pub fast_sync: bool, + + /// Database backend: `leveldb | memdb | cleveldb` + pub db_backend: DbBackend, + + /// Database directory + pub db_dir: PathBuf, + + /// Output level for logging, including package level options + pub log_level: LogLevel, + + /// Output format: 'plain' (colored text) or 'json' + pub log_format: LogFormat, + + /// Path to the JSON file containing the initial validator set and other meta data + pub genesis_file: PathBuf, + + /// Path to the JSON file containing the private key to use as a validator in the consensus protocol + pub priv_validator_key_file: Option, + + /// Path to the JSON file containing the last sign state of a validator + pub priv_validator_state_file: PathBuf, + + /// TCP or UNIX socket address for Tendermint to listen on for + /// connections from an external PrivValidator process + #[serde(deserialize_with = "deserialize_optional_value")] + pub priv_validator_laddr: Option, + + /// Path to the JSON file containing the private key to use for node authentication in the p2p protocol + pub node_key_file: PathBuf, + + /// Mechanism to connect to the ABCI application: socket | grpc + pub abci: AbciMode, + + /// TCP or UNIX socket address for the profiling server to listen on + #[serde(deserialize_with = "deserialize_optional_value")] + pub prof_laddr: Option, + + /// If `true`, query the ABCI app on connecting to a new peer + /// so the app can decide if we should keep the connection or not + pub filter_peers: bool, + + /// rpc server configuration options + pub rpc: RpcConfig, + + /// peer to peer configuration options + pub p2p: P2PConfig, + + /// mempool configuration options + pub mempool: MempoolConfig, + + /// consensus configuration options + pub consensus: ConsensusConfig, + + /// transactions indexer configuration options + pub tx_index: TxIndexConfig, + + /// instrumentation configuration options + pub instrumentation: InstrumentationConfig, +} + +impl TendermintConfig { + /// Parse Tendermint `config.toml` + pub fn parse_toml>(toml_string: T) -> Result { + Ok(toml::from_str(toml_string.as_ref())?) + } + + /// Load `config.toml` from a file + pub fn load_toml_file

(path: &P) -> Result + where + P: AsRef, + { + let toml_string = fs::read_to_string(path).map_err(|e| { + err!( + ErrorKind::Parse, + "couldn't open {}: {}", + path.as_ref().display(), + e + ) + })?; + + Self::parse_toml(toml_string) + } +} + +/// Database backend +#[derive(Copy, Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +pub enum DbBackend { + /// LevelDB backend + #[serde(rename = "leveldb")] + LevelDb, + + /// MemDB backend + #[serde(rename = "memdb")] + MemDb, + + /// CLevelDB backend + #[serde(rename = "cleveldb")] + CLevelDb, +} + +/// Loglevel configuration +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct LogLevel(BTreeMap); + +impl LogLevel { + /// Get the setting for the given key + pub fn get(&self, key: S) -> Option<&str> + where + S: AsRef, + { + self.0.get(key.as_ref()).map(AsRef::as_ref) + } + + /// Iterate over the levels + pub fn iter(&self) -> LogLevelIter { + self.0.iter() + } +} + +/// Iterator over log levels +pub type LogLevelIter<'a> = std::collections::btree_map::Iter<'a, String, String>; + +impl FromStr for LogLevel { + type Err = Error; + + fn from_str(s: &str) -> Result { + let mut levels = BTreeMap::new(); + + for level in s.split(',') { + let parts = level.split(':').collect::>(); + + if parts.len() != 2 { + Err(err!(ErrorKind::Parse, "error parsing log level: {}", level))?; + } + + let key = parts[0].to_owned(); + let value = parts[1].to_owned(); + + if levels.insert(key, value).is_some() { + Err(err!( + ErrorKind::Parse, + "duplicate log level setting for: {}", + level + ))?; + } + } + + Ok(LogLevel(levels)) + } +} + +impl fmt::Display for LogLevel { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for (i, (k, v)) in self.0.iter().enumerate() { + write!(f, "{}:{}", k, v)?; + + if i < self.0.len() - 1 { + write!(f, ",")?; + } + } + + Ok(()) + } +} + +impl<'de> Deserialize<'de> for LogLevel { + fn deserialize>(deserializer: D) -> Result { + let levels = String::deserialize(deserializer)?; + Ok(Self::from_str(&levels).map_err(|e| D::Error::custom(format!("{}", e)))?) + } +} + +impl Serialize for LogLevel { + fn serialize(&self, serializer: S) -> Result { + self.to_string().serialize(serializer) + } +} + +/// Logging format +#[derive(Copy, Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +pub enum LogFormat { + /// Plain (colored text) + #[serde(rename = "plain")] + Plain, + + /// JSON + #[serde(rename = "json")] + Json, +} + +/// Mechanism to connect to the ABCI application: socket | grpc +#[derive(Copy, Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +pub enum AbciMode { + /// Socket + #[serde(rename = "socket")] + Socket, + + /// GRPC + #[serde(rename = "grpc")] + Grpc, +} + +/// Tendermint `config.toml` file's `[rpc]` section +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct RpcConfig { + /// TCP or UNIX socket address for the RPC server to listen on + pub laddr: net::Address, + + /// A list of origins a cross-domain request can be executed from + /// Default value `[]` disables cors support + /// Use `["*"]` to allow any origin + pub cors_allowed_origins: Vec, + + /// A list of methods the client is allowed to use with cross-domain requests + pub cors_allowed_methods: Vec, + + /// A list of non simple headers the client is allowed to use with cross-domain requests + pub cors_allowed_headers: Vec, + + /// TCP or UNIX socket address for the gRPC server to listen on + /// NOTE: This server only supports `/broadcast_tx_commit` + #[serde(deserialize_with = "deserialize_optional_value")] + pub grpc_laddr: Option, + + /// Maximum number of simultaneous GRPC connections. + /// Does not include RPC (HTTP&WebSocket) connections. See `max_open_connections`. + pub grpc_max_open_connections: u64, + + /// Activate unsafe RPC commands like `/dial_seeds` and `/unsafe_flush_mempool` + #[serde(rename = "unsafe")] + pub unsafe_commands: bool, + + /// Maximum number of simultaneous connections (including WebSocket). + /// Does not include gRPC connections. See `grpc_max_open_connections`. + pub max_open_connections: u64, + + /// Maximum number of unique clientIDs that can `/subscribe`. + pub max_subscription_clients: u64, + + /// Maximum number of unique queries a given client can `/subscribe` to. + pub max_subscriptions_per_client: u64, + + /// How long to wait for a tx to be committed during `/broadcast_tx_commit`. + pub timeout_broadcast_tx_commit: Timeout, + + /// The name of a file containing certificate that is used to create the HTTPS server. + #[serde(deserialize_with = "deserialize_optional_value")] + pub tls_cert_file: Option, + + /// The name of a file containing matching private key that is used to create the HTTPS server. + #[serde(deserialize_with = "deserialize_optional_value")] + pub tls_key_file: Option, +} + +/// Origin hosts allowed with CORS requests to the RPC API +// TODO(tarcieri): parse and validate this string +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct CorsOrigin(String); + +impl AsRef for CorsOrigin { + fn as_ref(&self) -> &str { + self.0.as_ref() + } +} + +impl fmt::Display for CorsOrigin { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", &self.0) + } +} + +/// HTTP methods allowed with CORS requests to the RPC API +// TODO(tarcieri): parse and validate this string +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct CorsMethod(String); + +impl AsRef for CorsMethod { + fn as_ref(&self) -> &str { + self.0.as_ref() + } +} + +impl fmt::Display for CorsMethod { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", &self.0) + } +} + +/// HTTP headers allowed to be sent via CORS to the RPC API +// TODO(tarcieri): parse and validate this string +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct CorsHeader(String); + +impl AsRef for CorsHeader { + fn as_ref(&self) -> &str { + self.0.as_ref() + } +} + +impl fmt::Display for CorsHeader { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", &self.0) + } +} + +/// peer to peer configuration options +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct P2PConfig { + /// Address to listen for incoming connections + pub laddr: net::Address, + + /// Address to advertise to peers for them to dial + /// If empty, will use the same port as the laddr, + /// and will introspect on the listener or use UPnP + /// to figure out the address. + #[serde(deserialize_with = "deserialize_optional_value")] + pub external_address: Option, + + /// Comma separated list of seed nodes to connect to + #[serde( + serialize_with = "serialize_comma_separated_list", + deserialize_with = "deserialize_comma_separated_list" + )] + pub seeds: Vec, + + /// Comma separated list of nodes to keep persistent connections to + #[serde( + serialize_with = "serialize_comma_separated_list", + deserialize_with = "deserialize_comma_separated_list" + )] + pub persistent_peers: Vec, + + /// UPNP port forwarding + pub upnp: bool, + + /// Path to address boo + pub addr_book_file: PathBuf, + + /// Set `true` for strict address routability rules + /// Set `false` for private or local networks + pub addr_book_strict: bool, + + /// Maximum number of inbound peers + pub max_num_inbound_peers: u64, + + /// Maximum number of outbound peers to connect to, excluding persistent peers + pub max_num_outbound_peers: u64, + + /// Time to wait before flushing messages out on the connection + pub flush_throttle_timeout: Timeout, + + /// Maximum size of a message packet payload, in bytes + pub max_packet_msg_payload_size: u64, + + /// Rate at which packets can be sent, in bytes/second + pub send_rate: TransferRate, + + /// Rate at which packets can be received, in bytes/second + pub recv_rate: TransferRate, + + /// Set `true` to enable the peer-exchange reactor + pub pex: bool, + + /// Seed mode, in which node constantly crawls the network and looks for + /// peers. If another node asks it for addresses, it responds and disconnects. + /// + /// Does not work if the peer-exchange reactor is disabled. + pub seed_mode: bool, + + /// Comma separated list of peer IDs to keep private (will not be gossiped to other peers) + #[serde( + serialize_with = "serialize_comma_separated_list", + deserialize_with = "deserialize_comma_separated_list" + )] + pub private_peer_ids: Vec, + + /// Toggle to disable guard against peers connecting from the same ip. + pub allow_duplicate_ip: bool, + + /// Handshake timeout + pub handshake_timeout: Timeout, + + /// Timeout when dialing other peers + pub dial_timeout: Timeout, +} + +/// mempool configuration options +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct MempoolConfig { + /// Recheck enabled + pub recheck: bool, + + /// Broadcast enabled + pub broadcast: bool, + + /// WAL dir + #[serde(deserialize_with = "deserialize_optional_value")] + pub wal_dir: Option, + + /// Maximum number of transactions in the mempool + pub size: u64, + + /// Limit the total size of all txs in the mempool. + /// This only accounts for raw transactions (e.g. given 1MB transactions and + /// `max_txs_bytes`=5MB, mempool will only accept 5 transactions). + pub max_txs_bytes: u64, + + /// Size of the cache (used to filter transactions we saw earlier) in transactions + pub cache_size: u64, +} + +/// consensus configuration options +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct ConsensusConfig { + /// Path to WAL file + pub wal_file: PathBuf, + + /// Propose timeout + pub timeout_propose: Timeout, + + /// Propose timeout delta + pub timeout_propose_delta: Timeout, + + /// Prevote timeout + pub timeout_prevote: Timeout, + + /// Prevote timeout delta + pub timeout_prevote_delta: Timeout, + + /// Precommit timeout + pub timeout_precommit: Timeout, + + /// Precommit timeout delta + pub timeout_precommit_delta: Timeout, + + /// Commit timeout + pub timeout_commit: Timeout, + + /// Make progress as soon as we have all the precommits (as if TimeoutCommit = 0) + pub skip_timeout_commit: bool, + + /// EmptyBlocks mode + pub create_empty_blocks: bool, + + /// Interval between empty blocks + pub create_empty_blocks_interval: Timeout, + + /// Reactor sleep duration + pub peer_gossip_sleep_duration: Timeout, + + /// Reactor query sleep duration + pub peer_query_maj23_sleep_duration: Timeout, +} + +/// transactions indexer configuration options +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct TxIndexConfig { + /// What indexer to use for transactions + #[serde(default)] + pub indexer: TxIndexer, + + /// Comma-separated list of tags to index (by default the only tag is `tx.hash`) + // TODO(tarcieri): switch to `tendermint::abci::Tag` + #[serde( + serialize_with = "serialize_comma_separated_list", + deserialize_with = "deserialize_comma_separated_list" + )] + pub index_tags: Vec, + + /// When set to true, tells indexer to index all tags (predefined tags: + /// `tx.hash`, `tx.height` and all tags from DeliverTx responses). + pub index_all_tags: bool, +} + +/// What indexer to use for transactions +#[derive(Copy, Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +pub enum TxIndexer { + /// "null" + // TODO(tarcieri): use an `Option` type here? + #[serde(rename = "null")] + Null, + + /// "kv" (default) - the simplest possible indexer, backed by key-value storage (defaults to levelDB; see DBBackend). + #[serde(rename = "kv")] + Kv, +} + +impl Default for TxIndexer { + fn default() -> TxIndexer { + TxIndexer::Kv + } +} + +/// instrumentation configuration options +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct InstrumentationConfig { + /// When `true`, Prometheus metrics are served under /metrics on + /// PrometheusListenAddr. + pub prometheus: bool, + + /// Address to listen for Prometheus collector(s) connections + // TODO(tarcieri): parse to `tendermint::net::Addr` + pub prometheus_listen_addr: String, + + /// Maximum number of simultaneous connections. + pub max_open_connections: u64, + + /// Instrumentation namespace + pub namespace: String, +} + +/// Rate at which bytes can be sent/received +#[derive(Copy, Clone, Debug, Deserialize, Serialize)] +pub struct TransferRate(u64); + +impl TransferRate { + /// Get the trasfer rate in bytes per second + pub fn bytes_per_sec(self) -> u64 { + self.0 + } +} + +/// Deserialize `Option` where an empty string indicates `None` +fn deserialize_optional_value<'de, D, T, E>(deserializer: D) -> Result, D::Error> +where + D: de::Deserializer<'de>, + T: FromStr, + E: fmt::Display, +{ + let string = String::deserialize(deserializer)?; + + if string.is_empty() { + return Ok(None); + } + + string + .parse() + .map(Some) + .map_err(|e| D::Error::custom(format!("{}", e))) +} + +/// Deserialize a comma separated list of types that impl `FromStr` as a `Vec` +fn deserialize_comma_separated_list<'de, D, T, E>(deserializer: D) -> Result, D::Error> +where + D: de::Deserializer<'de>, + T: FromStr, + E: fmt::Display, +{ + let mut result = vec![]; + let string = String::deserialize(deserializer)?; + + if string.is_empty() { + return Ok(result); + } + + for item in string.split(',') { + result.push( + item.parse() + .map_err(|e| D::Error::custom(format!("{}", e)))?, + ); + } + + Ok(result) +} + +/// Serialize a comma separated list types that impl `ToString` +fn serialize_comma_separated_list(list: &[T], serializer: S) -> Result +where + S: ser::Serializer, + T: ToString, +{ + let str_list = list.iter().map(|addr| addr.to_string()).collect::>(); + str_list.join(",").serialize(serializer) +} diff --git a/tendermint-rs/src/config/node_key.rs b/tendermint-rs/src/config/node_key.rs new file mode 100644 index 0000000..19b35e4 --- /dev/null +++ b/tendermint-rs/src/config/node_key.rs @@ -0,0 +1,56 @@ +//! Node keys + +use crate::{ + error::{Error, ErrorKind}, + node, + private_key::PrivateKey, + public_key::PublicKey, +}; +use serde::{Deserialize, Serialize}; +use std::{fs, path::Path}; + +/// P2P node private keys +#[derive(Serialize, Deserialize)] +pub struct NodeKey { + /// Private key + pub priv_key: PrivateKey, +} + +impl NodeKey { + /// Parse `node_key.json` + pub fn parse_json>(json_string: T) -> Result { + Ok(serde_json::from_str(json_string.as_ref())?) + } + + /// Load `node_key.json` from a file + pub fn load_json_file

(path: &P) -> Result + where + P: AsRef, + { + let json_string = fs::read_to_string(path).map_err(|e| { + err!( + ErrorKind::Parse, + "couldn't open {}: {}", + path.as_ref().display(), + e + ) + })?; + + Self::parse_json(json_string) + } + + /// Get the public key for this keypair + pub fn public_key(&self) -> PublicKey { + match &self.priv_key { + PrivateKey::Ed25519(key) => key.public_key(), + } + } + + /// Get node ID for this keypair + pub fn node_id(&self) -> node::Id { + match &self.public_key() { + PublicKey::Ed25519(key) => node::Id::from(*key), + _ => unreachable!(), + } + } +} diff --git a/tendermint-rs/src/config/priv_validator_key.rs b/tendermint-rs/src/config/priv_validator_key.rs new file mode 100644 index 0000000..225f220 --- /dev/null +++ b/tendermint-rs/src/config/priv_validator_key.rs @@ -0,0 +1,57 @@ +//! Validator private keys + +use crate::{ + account, + error::{Error, ErrorKind}, + private_key::PrivateKey, + public_key::{PublicKey, TendermintKey}, +}; +use serde::{Deserialize, Serialize}; +use std::{fs, path::Path}; + +/// Validator private key +#[derive(Serialize, Deserialize)] +pub struct PrivValidatorKey { + /// Address + pub address: account::Id, + + /// Public key + pub pub_key: PublicKey, + + /// Private key + pub priv_key: PrivateKey, +} + +impl PrivValidatorKey { + /// Parse `priv_validator_key.json` + pub fn parse_json>(json_string: T) -> Result { + let result = serde_json::from_str::(json_string.as_ref())?; + + // Validate that the parsed key type is usable as a consensus key + TendermintKey::new_consensus_key(result.priv_key.public_key())?; + + Ok(result) + } + + /// Load `node_key.json` from a file + pub fn load_json_file

(path: &P) -> Result + where + P: AsRef, + { + let json_string = fs::read_to_string(path).map_err(|e| { + err!( + ErrorKind::Parse, + "couldn't open {}: {}", + path.as_ref().display(), + e + ) + })?; + + Self::parse_json(json_string) + } + + /// Get the consensus public key for this validator private key + pub fn consensus_pubkey(&self) -> TendermintKey { + TendermintKey::new_consensus_key(self.priv_key.public_key()).unwrap() + } +} diff --git a/tendermint-rs/src/consensus/params.rs b/tendermint-rs/src/consensus/params.rs index 47752a4..ec46357 100644 --- a/tendermint-rs/src/consensus/params.rs +++ b/tendermint-rs/src/consensus/params.rs @@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, Eq, PartialEq)] pub struct Params { /// Block size parameters - pub block_size: block::Size, + pub block: block::Size, /// Evidence parameters pub evidence: evidence::Params, diff --git a/tendermint-rs/src/error.rs b/tendermint-rs/src/error.rs index daa8fd3..18e3157 100644 --- a/tendermint-rs/src/error.rs +++ b/tendermint-rs/src/error.rs @@ -1,83 +1,168 @@ //! Error types use failure::*; -use std::io; +use std::{ + fmt::{self, Display}, + io, +}; #[cfg(feature = "secret-connection")] use {chrono, prost, subtle_encoding}; -/// Kinds of errors -#[derive(Copy, Clone, Eq, PartialEq, Debug, Fail)] -pub enum Error { - /// Cryptographic operation failed - #[fail(display = "cryptographic error")] - Crypto, +/// Create a new error (of a given kind) with a formatted message +#[allow(unused_macros)] +macro_rules! err { + ($kind:path, $msg:expr) => { + $crate::error::Error::new(failure::Context::new($kind), Some($msg.to_string())) + }; + ($kind:path, $fmt:expr, $($arg:tt)+) => { + err!($kind, &format!($fmt, $($arg)+)) + }; +} - /// Malformatted or otherwise invalid cryptographic key - #[fail(display = "invalid key")] - InvalidKey, +/// Error type +#[derive(Debug)] +pub struct Error { + /// Contextual information about the error + inner: Context, - /// Input/output error - #[fail(display = "I/O error")] - Io, + /// Optional message to associate with the error + msg: Option, +} - /// Length incorrect or too long - #[fail(display = "length error")] - Length, +impl Error { + /// Create a new error from the given context and optional message + pub fn new(context: C, msg: Option) -> Self + where + C: Into>, + { + Self { + inner: context.into(), + msg, + } + } - /// Parse error - #[fail(display = "parse error")] - Parse, + /// Obtain the error's `ErrorKind` + pub fn kind(&self) -> &ErrorKind { + self.inner.get_context() + } - /// Network protocol-related errors - #[fail(display = "protocol error")] - Protocol, + /// Get the message associated with this error (if available) + pub fn msg(&self) -> Option<&str> { + self.msg.as_ref().map(AsRef::as_ref) + } +} - /// Value out-of-range - #[fail(display = "value out of range")] - OutOfRange, +impl Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(msg) = &self.msg { + write!(f, "{}: {}", self.kind(), msg) + } else { + write!(f, "{}", self.kind()) + } + } +} - /// Signature invalid - #[fail(display = "bad signature")] - SignatureInvalid, +impl Fail for Error { + fn cause(&self) -> Option<&dyn Fail> { + self.inner.cause() + } + + fn backtrace(&self) -> Option<&Backtrace> { + self.inner.backtrace() + } } -/// Result type with our error type already defined -pub type Result = std::result::Result; +impl From for Error { + fn from(kind: ErrorKind) -> Self { + Error::new(kind, None) + } +} impl From for Error { - fn from(_: chrono::ParseError) -> Error { - Error::Parse + fn from(err: chrono::ParseError) -> Error { + err!(ErrorKind::Parse, err) } } impl From for Error { - fn from(_other: io::Error) -> Self { - Error::Io + fn from(err: io::Error) -> Self { + err!(ErrorKind::Io, err) } } #[cfg(feature = "secret-connection")] impl From for Error { - fn from(_other: prost::DecodeError) -> Self { - Error::Protocol + fn from(err: prost::DecodeError) -> Self { + err!(ErrorKind::Parse, err) } } #[cfg(feature = "secret-connection")] impl From for Error { - fn from(_other: prost::EncodeError) -> Self { - Error::Protocol + fn from(err: prost::EncodeError) -> Self { + err!(ErrorKind::Parse, err) } } -impl From for Error { - fn from(_: subtle_encoding::Error) -> Error { - Error::Parse +#[cfg(feature = "serde_json")] +impl From for Error { + fn from(err: serde_json::error::Error) -> Self { + err!(ErrorKind::Parse, err) } } impl From for Error { - fn from(_other: signatory::Error) -> Self { - Error::Crypto + fn from(_err: signatory::Error) -> Self { + // `signatory::Error` is opaque + err!(ErrorKind::Crypto, "signature error") + } +} + +impl From for Error { + fn from(err: subtle_encoding::Error) -> Error { + err!(ErrorKind::Parse, err) + } +} + +#[cfg(feature = "toml")] +impl From for Error { + fn from(err: toml::de::Error) -> Self { + err!(ErrorKind::Parse, err) } } + +/// Kinds of errors +#[derive(Clone, Eq, PartialEq, Debug, Fail)] +pub enum ErrorKind { + /// Cryptographic operation failed + #[fail(display = "cryptographic error")] + Crypto, + + /// Malformatted or otherwise invalid cryptographic key + #[fail(display = "invalid key")] + InvalidKey, + + /// Input/output error + #[fail(display = "I/O error")] + Io, + + /// Length incorrect or too long + #[fail(display = "length error")] + Length, + + /// Parse error + #[fail(display = "parse error")] + Parse, + + /// Network protocol-related errors + #[fail(display = "protocol error")] + Protocol, + + /// Value out-of-range + #[fail(display = "value out of range")] + OutOfRange, + + /// Signature invalid + #[fail(display = "bad signature")] + SignatureInvalid, +} diff --git a/tendermint-rs/src/hash.rs b/tendermint-rs/src/hash.rs index 5d4774c..a3d8819 100644 --- a/tendermint-rs/src/hash.rs +++ b/tendermint-rs/src/hash.rs @@ -1,6 +1,6 @@ //! Hash functions and their outputs -use crate::error::Error; +use crate::error::{Error, ErrorKind}; #[cfg(feature = "serde")] use serde::{de::Error as _, Deserialize, Deserializer, Serialize, Serializer}; use std::{ @@ -40,7 +40,7 @@ impl Hash { h.copy_from_slice(bytes); Ok(Hash::Sha256(h)) } else { - Err(Error::Parse) + Err(ErrorKind::Parse.into()) } } } diff --git a/tendermint-rs/src/lib.rs b/tendermint-rs/src/lib.rs index 7e88d95..34a71f4 100644 --- a/tendermint-rs/src/lib.rs +++ b/tendermint-rs/src/lib.rs @@ -24,6 +24,9 @@ extern crate prost_amino as prost; #[macro_use] extern crate prost_amino_derive as prost_derive; +#[macro_use] +pub mod error; + pub mod abci; pub mod account; #[cfg(feature = "amino-types")] @@ -32,8 +35,9 @@ pub mod block; pub mod chain; #[cfg(feature = "rpc")] pub mod channel; +#[cfg(feature = "config")] +pub mod config; pub mod consensus; -pub mod error; pub mod evidence; #[cfg(feature = "rpc")] pub mod genesis; @@ -42,6 +46,8 @@ pub mod merkle; mod moniker; pub mod net; pub mod node; +#[cfg(feature = "config")] +pub mod private_key; pub mod public_key; #[cfg(feature = "rpc")] pub mod rpc; @@ -51,6 +57,7 @@ pub mod secret_connection; mod serializers; pub mod signature; pub mod time; +mod timeout; pub mod validator; mod version; pub mod vote; @@ -61,12 +68,13 @@ pub use crate::genesis::Genesis; pub use crate::secret_connection::SecretConnection; pub use crate::{ block::Block, - error::Error, + error::{Error, ErrorKind}, hash::Hash, moniker::Moniker, public_key::{PublicKey, TendermintKey}, signature::Signature, time::Time, + timeout::Timeout, version::Version, vote::Vote, }; diff --git a/tendermint-rs/src/net.rs b/tendermint-rs/src/net.rs index a94505f..350a436 100644 --- a/tendermint-rs/src/net.rs +++ b/tendermint-rs/src/net.rs @@ -1,7 +1,9 @@ //! Remote addresses (`tcp://` or `unix://`) -use crate::node; -use failure::{bail, Error}; +use crate::{ + error::{Error, ErrorKind}, + node, +}; #[cfg(feature = "serde")] use serde::{de::Error as _, Deserialize, Deserializer, Serialize, Serializer}; use std::{ @@ -10,8 +12,14 @@ use std::{ str::{self, FromStr}, }; +/// URI prefix for TCP connections +pub const TCP_PREFIX: &str = "tcp://"; + +/// URI prefix for Unix socket connections +pub const UNIX_PREFIX: &str = "unix://"; + /// Remote address (TCP or UNIX socket) -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Eq, Hash, PartialEq)] pub enum Address { /// TCP connections Tcp { @@ -43,8 +51,8 @@ impl<'de> Deserialize<'de> for Address { impl Display for Address { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Address::Tcp { host, port, .. } => write!(f, "tcp://{}:{}", host, port), - Address::Unix { path } => write!(f, "unix://{}", path.display()), + Address::Tcp { host, port, .. } => write!(f, "{}{}:{}", TCP_PREFIX, host, port), + Address::Unix { path } => write!(f, "{}{}", UNIX_PREFIX, path.display()), } } } @@ -53,41 +61,72 @@ impl FromStr for Address { type Err = Error; fn from_str(addr: &str) -> Result { - if addr.starts_with("tcp://") { - let authority_parts = addr[6..].split('@').collect::>(); - - let (peer_id, authority) = match authority_parts.len() { - 1 => (None, authority_parts[0]), - 2 => (Some(authority_parts[0].parse()?), authority_parts[1]), - _ => bail!("invalid tcp:// address: {}", addr), - }; - - let host_and_port: Vec<&str> = authority.split(':').collect(); - - if host_and_port.len() != 2 { - bail!("invalid tcp:// address: {}", addr); - } - - let host = host_and_port[0].to_owned(); - - match host_and_port[1].parse() { - Ok(port) => Ok(Address::Tcp { - peer_id, - host, - port, - }), - Err(_) => bail!("invalid tcp:// address (bad port): {}", addr), - } - } else if addr.starts_with("unix://") { + if addr.starts_with(TCP_PREFIX) { + Self::parse_tcp_addr(&addr[TCP_PREFIX.len()..]) + } else if addr.starts_with(UNIX_PREFIX) { Ok(Address::Unix { - path: PathBuf::from(&addr[7..]), + path: PathBuf::from(&addr[UNIX_PREFIX.len()..]), }) + } else if addr.contains("://") { + // The only supported URI prefixes are `tcp://` and `unix://` + Err(err!(ErrorKind::Parse, "invalid address prefix: {:?}", addr)) } else { - bail!("invalid address: {}", addr) + // If the address has no URI prefix, assume TCP + Self::parse_tcp_addr(addr) } } } +impl Address { + /// Parse a TCP address (without a `tcp://` prefix). + /// + /// This is used internally by `Address::from_str`. + fn parse_tcp_addr(addr: &str) -> Result { + // TODO(tarcieri): use the `uri` (or other) crate for this + let authority_parts = addr.split('@').collect::>(); + + let (peer_id, authority) = match authority_parts.len() { + 1 => (None, authority_parts[0]), + 2 => (Some(authority_parts[0].parse()?), authority_parts[1]), + _ => Err(err!( + ErrorKind::Parse, + "invalid {} address (bad authority): {}", + TCP_PREFIX, + addr + ))?, + }; + + let host_and_port: Vec<&str> = authority.split(':').collect(); + + if host_and_port.len() != 2 { + Err(err!( + ErrorKind::Parse, + "invalid {} address (missing port): {}", + TCP_PREFIX, + addr + ))?; + } + + // TODO(tarcieri): default for missing hostname? + let host = host_and_port[0].to_owned(); + + let port = host_and_port[1].parse::().map_err(|_| { + err!( + ErrorKind::Parse, + "invalid {} address (bad port): {}", + TCP_PREFIX, + addr + ) + })?; + + Ok(Address::Tcp { + peer_id, + host, + port, + }) + } +} + #[cfg(feature = "serde")] impl Serialize for Address { fn serialize(&self, serializer: S) -> Result { @@ -101,27 +140,31 @@ mod tests { use crate::node; /// Example TCP node address - const EXAMPLE_TCP_STR: &str = + const EXAMPLE_TCP_ADDR: &str = "tcp://abd636b766dcefb5322d8ca40011ec2cb35efbc2@35.192.61.41:26656"; #[test] fn parse_tcp_addr() { - match EXAMPLE_TCP_STR.parse::

().unwrap() { - Address::Tcp { - peer_id, - host, - port, - } => { - assert_eq!( - peer_id.unwrap(), - "abd636b766dcefb5322d8ca40011ec2cb35efbc2" - .parse::() - .unwrap() - ); - assert_eq!(host, "35.192.61.41"); - assert_eq!(port, 26656); + let tcp_addr_without_prefix = &EXAMPLE_TCP_ADDR[TCP_PREFIX.len()..]; + + for tcp_addr in &[EXAMPLE_TCP_ADDR, tcp_addr_without_prefix] { + match tcp_addr.parse::
().unwrap() { + Address::Tcp { + peer_id, + host, + port, + } => { + assert_eq!( + peer_id.unwrap(), + "abd636b766dcefb5322d8ca40011ec2cb35efbc2" + .parse::() + .unwrap() + ); + assert_eq!(host, "35.192.61.41"); + assert_eq!(port, 26656); + } + other => panic!("unexpected address type: {:?}", other), } - other => panic!("unexpected address type: {:?}", other), } } } diff --git a/tendermint-rs/src/node/id.rs b/tendermint-rs/src/node/id.rs index 3f82ec1..40355e6 100644 --- a/tendermint-rs/src/node/id.rs +++ b/tendermint-rs/src/node/id.rs @@ -1,6 +1,6 @@ //! Tendermint node IDs -use crate::error::Error; +use crate::error::{Error, ErrorKind}; #[cfg(feature = "serde")] use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; use sha2::{Digest, Sha256}; @@ -76,10 +76,10 @@ impl FromStr for Id { // Accept either upper or lower case hex let bytes = hex::decode_upper(s) .or_else(|_| hex::decode(s)) - .map_err(|_| Error::Parse)?; + .map_err(|_| ErrorKind::Parse)?; if bytes.len() != LENGTH { - return Err(Error::Parse); + Err(ErrorKind::Parse)?; } let mut result_bytes = [0u8; LENGTH]; diff --git a/tendermint-rs/src/private_key.rs b/tendermint-rs/src/private_key.rs new file mode 100644 index 0000000..04c915c --- /dev/null +++ b/tendermint-rs/src/private_key.rs @@ -0,0 +1,70 @@ +//! Cryptographic private keys + +use crate::public_key::PublicKey; +use serde::{de, de::Error as _, ser, Deserialize, Serialize}; +use signatory::{ed25519, PublicKeyed}; +use subtle_encoding::{Base64, Encoding}; +use zeroize::{Zeroize, Zeroizing}; + +/// Size of an Ed25519 keypair (private + public key) in bytes +pub const ED25519_KEYPAIR_SIZE: usize = 64; + +/// Private keys as parsed from configuration files +#[derive(Serialize, Deserialize)] +#[serde(tag = "type", content = "value")] +pub enum PrivateKey { + /// Ed25519 keys + #[serde(rename = "tendermint/PrivKeyEd25519")] + Ed25519(Ed25519Keypair), +} + +impl PrivateKey { + /// Get the public key associated with this private key + pub fn public_key(&self) -> PublicKey { + match self { + PrivateKey::Ed25519(private_key) => private_key.public_key(), + } + } +} + +/// Ed25519 keypairs +#[derive(Zeroize)] +#[zeroize(drop)] +pub struct Ed25519Keypair([u8; ED25519_KEYPAIR_SIZE]); + +impl Ed25519Keypair { + /// Get the public key associated with this keypair + pub fn public_key(&self) -> PublicKey { + let seed = ed25519::Seed::from_keypair(&self.0[..]).unwrap(); + let pk = signatory_dalek::Ed25519Signer::from(&seed) + .public_key() + .unwrap(); + + PublicKey::from(pk) + } +} + +impl<'de> Deserialize<'de> for Ed25519Keypair { + fn deserialize>(deserializer: D) -> Result { + let string = Zeroizing::new(String::deserialize(deserializer)?); + + let mut keypair_bytes = [0u8; ED25519_KEYPAIR_SIZE]; + let decoded_len = Base64::default() + .decode_to_slice(string.as_bytes(), &mut keypair_bytes) + .map_err(|_| D::Error::custom("invalid Ed25519 keypair"))?; + + if decoded_len != ED25519_KEYPAIR_SIZE { + return Err(D::Error::custom("invalid Ed25519 keypair size")); + } + + Ok(Ed25519Keypair(keypair_bytes)) + } +} + +impl Serialize for Ed25519Keypair { + fn serialize(&self, serializer: S) -> Result { + String::from_utf8(Base64::default().encode(&self.0[..])) + .unwrap() + .serialize(serializer) + } +} diff --git a/tendermint-rs/src/public_key.rs b/tendermint-rs/src/public_key.rs index 78be64e..c6fe4b4 100644 --- a/tendermint-rs/src/public_key.rs +++ b/tendermint-rs/src/public_key.rs @@ -1,6 +1,6 @@ //! Public keys used in Tendermint networks -use crate::error::Error; +use crate::error::{Error, ErrorKind}; #[cfg(feature = "serde")] use serde::{de::Error as _, Deserialize, Deserializer, Serialize, Serializer}; use signatory::{ecdsa::curve::secp256k1, ed25519}; @@ -112,6 +112,28 @@ pub enum TendermintKey { ConsensusKey(PublicKey), } +impl TendermintKey { + /// Create a new account key from a `PublicKey` + pub fn new_account_key(public_key: PublicKey) -> Result { + match public_key { + PublicKey::Ed25519(_) | PublicKey::Secp256k1(_) => { + Ok(TendermintKey::AccountKey(public_key)) + } + } + } + + /// Create a new consensus key from a `PublicKey` + pub fn new_consensus_key(public_key: PublicKey) -> Result { + match public_key { + PublicKey::Ed25519(_) => Ok(TendermintKey::AccountKey(public_key)), + _ => Err(err!( + ErrorKind::InvalidKey, + "only ed25519 consensus keys are supported" + )), + } + } +} + impl Deref for TendermintKey { type Target = PublicKey; @@ -156,7 +178,7 @@ impl FromStr for Algorithm { match s { "ed25519" => Ok(Algorithm::Ed25519), "secp256k1" => Ok(Algorithm::Secp256k1), - _ => Err(Error::Parse), + _ => Err(ErrorKind::Parse.into()), } } } diff --git a/tendermint-rs/src/secret_connection.rs b/tendermint-rs/src/secret_connection.rs index 09f9fc7..e3279f6 100644 --- a/tendermint-rs/src/secret_connection.rs +++ b/tendermint-rs/src/secret_connection.rs @@ -5,7 +5,10 @@ mod nonce; mod public_key; pub use self::{kdf::Kdf, nonce::Nonce, public_key::PublicKey}; -use crate::{amino_types::AuthSigMessage, error::Error}; +use crate::{ + amino_types::AuthSigMessage, + error::{Error, ErrorKind}, +}; use byteorder::{ByteOrder, LE}; use bytes::BufMut; use prost::{encoding::encode_varint, Message}; @@ -71,7 +74,7 @@ impl SecretConnection { // - https://github.com/tendermint/kms/issues/142 // - https://eprint.iacr.org/2019/526.pdf if shared_secret.as_bytes().ct_eq(&[0x00; 32]).unwrap_u8() == 1 { - return Err(Error::InvalidKey); + Err(ErrorKind::InvalidKey)?; } // Sort by lexical order. @@ -92,12 +95,12 @@ impl SecretConnection { recv_nonce: Nonce::default(), send_nonce: Nonce::default(), recv_secret: aead::OpeningKey::new(&aead::CHACHA20_POLY1305, &kdf.recv_secret) - .map_err(|_| Error::Crypto)?, + .map_err(|_| ErrorKind::Crypto)?, send_secret: aead::SealingKey::new(&aead::CHACHA20_POLY1305, &kdf.send_secret) - .map_err(|_| Error::Crypto)?, + .map_err(|_| ErrorKind::Crypto)?, remote_pubkey: PublicKey::from( ed25519::PublicKey::from_bytes(remote_eph_pubkey.as_bytes()) - .ok_or_else(|| Error::Crypto)?, + .ok_or_else(|| ErrorKind::Crypto)?, ), }; @@ -112,7 +115,7 @@ impl SecretConnection { }; let remote_pubkey = - ed25519::PublicKey::from_bytes(&auth_sig_msg.key).ok_or_else(|| Error::Crypto)?; + ed25519::PublicKey::from_bytes(&auth_sig_msg.key).ok_or_else(|| ErrorKind::Crypto)?; let remote_signature: &[u8] = &auth_sig_msg.sig; let remote_sig = ed25519::Signature::from_bytes(remote_signature)?; @@ -136,13 +139,13 @@ impl SecretConnection { in_out.copy_from_slice(ciphertext); aead::open_in_place(&self.recv_secret, nonce, associated_data, 0, in_out) - .map_err(|_| Error::Crypto)? + .map_err(|_| ErrorKind::Crypto)? .len() } else { let mut in_out = ciphertext.to_vec(); let out0 = aead::open_in_place(&self.recv_secret, nonce, aead::Aad::empty(), 0, &mut in_out) - .map_err(|_| Error::Crypto)?; + .map_err(|_| ErrorKind::Crypto)?; out[..out0.len()].copy_from_slice(out0); out0.len() }; @@ -169,7 +172,7 @@ impl SecretConnection { sealed_frame, TAG_SIZE, ) - .map_err(|_| Error::Crypto)?; + .map_err(|_| ErrorKind::Crypto)?; Ok(()) } @@ -312,14 +315,14 @@ fn share_eph_pubkey( // https://github.com/tendermint/tendermint/blob/013b9cef642f875634c614019ab13b17570778ad/p2p/conn/secret_connection.go#L208-L238 let mut remote_eph_pubkey_fixed: [u8; 32] = Default::default(); if buf[0] != 33 || buf[1] != 32 { - return Err(Error::Protocol); + Err(ErrorKind::Protocol)?; } // after total length (33) and byte length (32), we expect the raw bytes // of the pub key: remote_eph_pubkey_fixed.copy_from_slice(&buf[2..34]); if is_blacklisted_point(&remote_eph_pubkey_fixed) { - Err(Error::InvalidKey) + Err(ErrorKind::InvalidKey.into()) } else { Ok(EphemeralPublic::from(remote_eph_pubkey_fixed)) } @@ -389,7 +392,9 @@ fn sign_challenge( challenge: &[u8; 32], local_privkey: &dyn Signer, ) -> Result { - local_privkey.try_sign(challenge).map_err(|_| Error::Crypto) + local_privkey + .try_sign(challenge) + .map_err(|_| ErrorKind::Crypto.into()) } // TODO(ismail): change from DecodeError to something more generic diff --git a/tendermint-rs/src/secret_connection/public_key.rs b/tendermint-rs/src/secret_connection/public_key.rs index 3f6c7d9..6589183 100644 --- a/tendermint-rs/src/secret_connection/public_key.rs +++ b/tendermint-rs/src/secret_connection/public_key.rs @@ -1,6 +1,9 @@ //! Secret Connection peer public keys -use crate::{error::Error, node}; +use crate::{ + error::{Error, ErrorKind}, + node, +}; use signatory::ed25519; use std::fmt::{self, Display}; @@ -15,7 +18,7 @@ impl PublicKey { /// From raw Ed25519 public key bytes pub fn from_raw_ed25519(bytes: &[u8]) -> Result { Ok(PublicKey::Ed25519( - ed25519::PublicKey::from_bytes(bytes).ok_or_else(|| Error::Crypto)?, + ed25519::PublicKey::from_bytes(bytes).ok_or_else(|| ErrorKind::Crypto)?, )) } diff --git a/tendermint-rs/src/serializers.rs b/tendermint-rs/src/serializers.rs index 0fa06ad..f66cd0a 100644 --- a/tendermint-rs/src/serializers.rs +++ b/tendermint-rs/src/serializers.rs @@ -62,10 +62,5 @@ pub(crate) fn serialize_duration(duration: &Duration, serializer: S) -> Resul where S: Serializer, { - // TODO(tarcieri): use `as_nanos` when we're Rust 1.33+ - format!( - "{}", - (duration.as_secs() * 1_000_000_000) + u64::from(duration.subsec_nanos()) - ) - .serialize(serializer) + format!("{}", duration.as_nanos()).serialize(serializer) } diff --git a/tendermint-rs/src/time.rs b/tendermint-rs/src/time.rs index 4aef6dc..39be196 100644 --- a/tendermint-rs/src/time.rs +++ b/tendermint-rs/src/time.rs @@ -1,6 +1,6 @@ //! Timestamps used by Tendermint blockchains -use crate::error::Error; +use crate::error::{Error, ErrorKind}; use chrono::{DateTime, Utc}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; @@ -31,7 +31,7 @@ impl Time { self.0 .signed_duration_since(other.0) .to_std() - .map_err(|_| Error::OutOfRange) + .map_err(|_| ErrorKind::OutOfRange.into()) } /// Parse a timestamp from an RFC 3339 date diff --git a/tendermint-rs/src/timeout.rs b/tendermint-rs/src/timeout.rs new file mode 100644 index 0000000..aa60a9b --- /dev/null +++ b/tendermint-rs/src/timeout.rs @@ -0,0 +1,109 @@ +use crate::{Error, ErrorKind}; +#[cfg(feature = "serde")] +use serde::{de, de::Error as _, ser, Deserialize, Serialize}; +use std::{fmt, ops::Deref, str::FromStr, time::Duration}; + +/// Timeout durations +#[derive(Copy, Clone, Debug)] +pub struct Timeout(Duration); + +impl Deref for Timeout { + type Target = Duration; + + fn deref(&self) -> &Duration { + &self.0 + } +} + +impl From for Timeout { + fn from(duration: Duration) -> Timeout { + Timeout(duration) + } +} + +impl From for Duration { + fn from(timeout: Timeout) -> Duration { + timeout.0 + } +} + +impl FromStr for Timeout { + type Err = Error; + + fn from_str(s: &str) -> Result { + // Timeouts are either 'ms' or 's', and should always end with 's' + if s.len() < 2 || !s.ends_with('s') { + Err(err!(ErrorKind::Parse, "invalid units"))?; + } + + let units = match s.chars().nth(s.len() - 2) { + Some('m') => "ms", + Some('0'...'9') => "s", + _ => Err(err!(ErrorKind::Parse, "invalid units"))?, + }; + + let numeric_part = s.chars().take(s.len() - units.len()).collect::(); + + let numeric_value = numeric_part + .parse::() + .map_err(|e| err!(ErrorKind::Parse, e))?; + + let duration = match units { + "s" => Duration::from_secs(numeric_value), + "ms" => Duration::from_millis(numeric_value), + _ => unreachable!(), + }; + + Ok(Timeout(duration)) + } +} + +impl fmt::Display for Timeout { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}ms", self.as_millis()) + } +} + +#[cfg(feature = "serde")] +impl<'de> Deserialize<'de> for Timeout { + /// Parse `Timeout` from string ending in `s` or `ms` + fn deserialize>(deserializer: D) -> Result { + let string = String::deserialize(deserializer)?; + string + .parse() + .map_err(|_| D::Error::custom(format!("invalid timeout value: {:?}", &string))) + } +} + +#[cfg(feature = "serde")] +impl Serialize for Timeout { + fn serialize(&self, serializer: S) -> Result { + self.to_string().serialize(serializer) + } +} + +#[cfg(test)] +mod tests { + use super::Timeout; + use crate::error::ErrorKind; + + #[test] + fn parse_seconds() { + let timeout = "123s".parse::().unwrap(); + assert_eq!(timeout.as_secs(), 123); + } + + #[test] + fn parse_milliseconds() { + let timeout = "123ms".parse::().unwrap(); + assert_eq!(timeout.as_millis(), 123); + } + + #[test] + fn reject_no_units() { + assert_eq!( + *"123".parse::().err().unwrap().kind(), + ErrorKind::Parse + ); + } +} diff --git a/tendermint-rs/tests/config.rs b/tendermint-rs/tests/config.rs new file mode 100644 index 0000000..b324093 --- /dev/null +++ b/tendermint-rs/tests/config.rs @@ -0,0 +1,225 @@ +//! Tests for parsing configuration files. +//! +//! Test config files are located in the `tests/support/config` subdirectory. + +#[cfg(feature = "config")] +mod files { + use std::{fs, path::PathBuf, time::Duration}; + use tendermint::{config::*, net, node}; + + /// Read a fixture file from the `support/config` directory + fn read_fixture(name: &str) -> String { + fs::read_to_string(PathBuf::from("./tests/support/config/").join(name)).unwrap() + } + + /// Parse an example `config.toml` file to a `TendermintConfig` struct + #[test] + fn config_toml_parser() { + let config_toml = read_fixture("config.toml"); + let config = TendermintConfig::parse_toml(&config_toml).unwrap(); + + // main base config options + + assert_eq!( + config.proxy_app, + "tcp://127.0.0.1:26658".parse::().unwrap() + ); + assert_eq!(config.moniker.as_ref(), "technodrome"); + assert!(config.fast_sync); + assert_eq!(config.db_backend, DbBackend::LevelDb); + assert_eq!(config.db_dir, PathBuf::from("data")); + assert_eq!(config.log_level.get("main"), Some("info")); + assert_eq!(config.log_level.get("state"), Some("info")); + assert_eq!(config.log_level.get("*"), Some("error")); + assert_eq!(config.log_format, LogFormat::Plain); + assert_eq!(config.genesis_file, PathBuf::from("config/genesis.json")); + assert_eq!( + config.priv_validator_key_file, + Some(PathBuf::from("config/priv_validator_key.json")) + ); + assert_eq!( + config.priv_validator_state_file, + PathBuf::from("data/priv_validator_state.json") + ); + assert_eq!(config.priv_validator_laddr, None); + assert_eq!(config.node_key_file, PathBuf::from("config/node_key.json")); + assert_eq!(config.abci, AbciMode::Socket); + assert_eq!( + config.prof_laddr, + Some("tcp://localhost:6060".parse::().unwrap()) + ); + assert!(!config.filter_peers); + + // rpc server configuration options + + let rpc = &config.rpc; + assert_eq!( + rpc.laddr, + "tcp://0.0.0.0:26657".parse::().unwrap() + ); + assert!(rpc.cors_allowed_origins.is_empty()); + assert_eq!(rpc.cors_allowed_methods.len(), 3); + assert_eq!(rpc.cors_allowed_methods[0].as_ref(), "HEAD"); + assert_eq!(rpc.cors_allowed_methods[1].as_ref(), "GET"); + assert_eq!(rpc.cors_allowed_methods[2].as_ref(), "POST"); + assert_eq!(rpc.cors_allowed_headers.len(), 5); + assert_eq!(rpc.cors_allowed_headers[0].as_ref(), "Origin"); + assert_eq!(rpc.cors_allowed_headers[1].as_ref(), "Accept"); + assert_eq!(rpc.cors_allowed_headers[2].as_ref(), "Content-Type"); + assert_eq!(rpc.cors_allowed_headers[3].as_ref(), "X-Requested-With"); + assert_eq!(rpc.cors_allowed_headers[4].as_ref(), "X-Server-Time"); + assert_eq!(rpc.grpc_laddr, None); + assert_eq!(rpc.grpc_max_open_connections, 900); + assert!(!rpc.unsafe_commands); + assert_eq!(rpc.max_open_connections, 900); + assert_eq!(rpc.max_subscription_clients, 100); + assert_eq!(rpc.max_subscriptions_per_client, 5); + assert_eq!(*rpc.timeout_broadcast_tx_commit, Duration::from_secs(10)); + assert_eq!(rpc.tls_cert_file, None); + assert_eq!(rpc.tls_key_file, None); + + // peer to peer configuration options + + let p2p = &config.p2p; + assert_eq!( + p2p.laddr, + "tcp://0.0.0.0:26656".parse::().unwrap() + ); + assert_eq!(p2p.external_address, None); + assert_eq!(p2p.seeds.len(), 2); + assert_eq!( + p2p.seeds[0], + "tcp://c2e1bde78877975b31e6f06e77da200a38048e2b@seed-1.example.com:26656" + .parse::() + .unwrap() + ); + assert_eq!( + p2p.seeds[1], + "tcp://0eafed3e9e76f626a299e1b8a79454fffe9ca83c@seed-2.example.com:26656" + .parse::() + .unwrap() + ); + assert_eq!(p2p.persistent_peers.len(), 2); + assert_eq!( + p2p.persistent_peers[0], + "tcp://70d834561f91613153e4a873f01a2cbbf1b9678d@1.2.3.4:26656" + .parse::() + .unwrap() + ); + assert_eq!( + p2p.persistent_peers[1], + "tcp://f68ed33a0baa0c734a939a9e60659566adc725cd@peer-2.example.com:26656" + .parse::() + .unwrap() + ); + assert!(!p2p.upnp); + assert_eq!(p2p.addr_book_file, PathBuf::from("config/addrbook.json")); + assert!(p2p.addr_book_strict); + assert_eq!(p2p.max_num_inbound_peers, 40); + assert_eq!(p2p.max_num_outbound_peers, 10); + assert_eq!(*p2p.flush_throttle_timeout, Duration::from_millis(100)); + assert_eq!(p2p.max_packet_msg_payload_size, 1024); + assert_eq!(p2p.send_rate.bytes_per_sec(), 5120000); + assert_eq!(p2p.recv_rate.bytes_per_sec(), 5120000); + assert!(p2p.pex); + assert!(!p2p.seed_mode); + assert_eq!(p2p.private_peer_ids.len(), 3); + assert_eq!( + p2p.private_peer_ids[0], + "8112E5C5AB6A48ADCC0E875D58A4264A2639F6A8" + .parse::() + .unwrap() + ); + assert_eq!( + p2p.private_peer_ids[1], + "3D1B9086E48C7BDF7F0D766351EED812A75DE500" + .parse::() + .unwrap() + ); + assert_eq!( + p2p.private_peer_ids[2], + "A7306AEE50627E68177A002BADD3BA4A45301AD4" + .parse::() + .unwrap() + ); + assert!(!p2p.allow_duplicate_ip); + assert_eq!(*p2p.handshake_timeout, Duration::from_secs(20)); + assert_eq!(*p2p.dial_timeout, Duration::from_secs(3)); + + // mempool configuration options + + let mempool = &config.mempool; + assert!(mempool.recheck); + assert!(mempool.broadcast); + assert_eq!(mempool.wal_dir, None); + assert_eq!(mempool.size, 5000); + assert_eq!(mempool.max_txs_bytes, 1073741824); + assert_eq!(mempool.cache_size, 10000); + + // consensus configuration options + + let consensus = &config.consensus; + assert_eq!(consensus.wal_file, PathBuf::from("data/cs.wal/wal")); + assert_eq!(*consensus.timeout_propose, Duration::from_secs(3)); + assert_eq!(*consensus.timeout_propose_delta, Duration::from_millis(500)); + assert_eq!(*consensus.timeout_prevote, Duration::from_secs(1)); + assert_eq!(*consensus.timeout_prevote_delta, Duration::from_millis(500)); + assert_eq!(*consensus.timeout_precommit, Duration::from_secs(1)); + assert_eq!( + *consensus.timeout_precommit_delta, + Duration::from_millis(500) + ); + assert_eq!(*consensus.timeout_commit, Duration::from_secs(5)); + assert!(!consensus.skip_timeout_commit); + assert_eq!( + *consensus.create_empty_blocks_interval, + Duration::from_secs(0) + ); + assert_eq!( + *consensus.peer_gossip_sleep_duration, + Duration::from_millis(100) + ); + assert_eq!( + *consensus.peer_query_maj23_sleep_duration, + Duration::from_secs(2) + ); + + // transactions indexer configuration options + + let tx_index = &config.tx_index; + assert_eq!(tx_index.indexer, TxIndexer::Kv); + assert_eq!(tx_index.index_tags.len(), 1); + assert_eq!(tx_index.index_tags[0].as_ref(), "tx.height"); + assert!(tx_index.index_all_tags); + + // instrumentation configuration options + + let instrumentation = &config.instrumentation; + assert!(!instrumentation.prometheus); + assert_eq!(instrumentation.prometheus_listen_addr, ":26660"); + assert_eq!(instrumentation.max_open_connections, 3); + assert_eq!(instrumentation.namespace, "tendermint"); + } + + /// Parse an example `node_key.json` file to a `NodeKey` struct + #[test] + fn node_key_parser() { + let raw_node_key = read_fixture("node_key.json"); + let node_key = NodeKey::parse_json(&raw_node_key).unwrap(); + assert_eq!( + node_key.node_id().to_string(), + "1A7B6BCF3D6FB055AB3AEBCA415847531B626699" + ); + } + + /// Parse an example `priv_validator_key.json` to a `PrivValidatorKey` struct + #[test] + fn priv_validator_json_parser() { + let raw_priv_validator_key = read_fixture("priv_validator_key.json"); + let priv_validator_key = PrivValidatorKey::parse_json(&raw_priv_validator_key).unwrap(); + assert_eq!( + priv_validator_key.consensus_pubkey().to_hex(), + "1624DE6420F26BF4B2A2E84CEB7A53C3F1AE77408779B20064782FBADBDF0E365959EE4534" + ); + } +} diff --git a/tendermint-rs/tests/integration.rs b/tendermint-rs/tests/integration.rs index bb9c565..748f5a1 100644 --- a/tendermint-rs/tests/integration.rs +++ b/tendermint-rs/tests/integration.rs @@ -1,35 +1,40 @@ //! Integration tests -/// RPC integration tests +/// RPC integration tests. /// -/// NOTE: health is tested implicitly when the initial client is created -#[cfg(all(feature = "integration", feature = "rpc"))] +/// These are all ignored by default, since they test against running `gaiad`. +/// They can be run using: +/// +/// ``` +/// cargo test -- --ignored +/// ``` +#[cfg(all(feature = "rpc"))] mod rpc { use tendermint::rpc::Client; /// Get the address of the local node - #[cfg(all(feature = "integration", feature = "rpc"))] pub fn localhost_rpc_client() -> Client { Client::new(&"tcp://127.0.0.1:26657".parse().unwrap()).unwrap() } /// `/abci_info` endpoint #[test] + #[ignore] fn abci_info() { let abci_info = localhost_rpc_client().abci_info().unwrap(); - - // TODO(tarcieri): integration testing support for non-gaia apps assert_eq!(&abci_info.data, "GaiaApp"); } /// `/abci_query` endpoint #[test] + #[ignore] fn abci_query() { // TODO(tarcieri): write integration test for this endpoint } /// `/block` endpoint #[test] + #[ignore] fn block() { let height = 1u64; let block_info = localhost_rpc_client().block(height).unwrap(); @@ -38,6 +43,7 @@ mod rpc { /// `/block_results` endpoint #[test] + #[ignore] fn block_results() { let height = 1u64; let block_results = localhost_rpc_client().block_results(height).unwrap(); @@ -46,6 +52,7 @@ mod rpc { /// `/blockchain` endpoint #[test] + #[ignore] fn blockchain() { let blockchain_info = localhost_rpc_client().blockchain(1u64, 10u64).unwrap(); assert_eq!(blockchain_info.block_metas.len(), 10); @@ -53,6 +60,7 @@ mod rpc { /// `/commit` endpoint #[test] + #[ignore] fn commit() { let height = 1u64; let commit_info = localhost_rpc_client().block(height).unwrap(); @@ -61,6 +69,7 @@ mod rpc { /// `/genesis` endpoint #[test] + #[ignore] fn genesis() { let genesis = localhost_rpc_client().genesis().unwrap(); assert_eq!( @@ -71,6 +80,7 @@ mod rpc { /// `/net_info` endpoint integration test #[test] + #[ignore] fn net_info() { let net_info = localhost_rpc_client().net_info().unwrap(); assert!(net_info.listening); @@ -78,6 +88,7 @@ mod rpc { /// `/status` endpoint integration test #[test] + #[ignore] fn status_integration() { let status = localhost_rpc_client().status().unwrap(); diff --git a/tendermint-rs/tests/rpc.rs b/tendermint-rs/tests/rpc.rs index bc5b50b..994c456 100644 --- a/tendermint-rs/tests/rpc.rs +++ b/tendermint-rs/tests/rpc.rs @@ -78,11 +78,11 @@ mod endpoints { let tag = deliver_tx[0] .tags .iter() - .find(|t| t.key.eq("ZGVzdGluYXRpb24tdmFsaWRhdG9y")) + .find(|t| t.key.as_ref().eq("ZGVzdGluYXRpb24tdmFsaWRhdG9y")) .unwrap(); assert_eq!( - &tag.value, + tag.value.as_ref(), "Y29zbW9zdmFsb3BlcjFlaDVtd3UwNDRnZDVudGtrYzJ4Z2ZnODI0N21nYzU2Zno0c2RnMw==" ); @@ -160,7 +160,7 @@ mod endpoints { } = response.genesis; assert_eq!(chain_id.as_str(), EXAMPLE_CHAIN); - assert_eq!(consensus_params.block_size.max_bytes, 150000); + assert_eq!(consensus_params.block.max_bytes, 200000); } #[test] diff --git a/tendermint-rs/tests/support/config/config.toml b/tendermint-rs/tests/support/config/config.toml new file mode 100644 index 0000000..d65d0be --- /dev/null +++ b/tendermint-rs/tests/support/config/config.toml @@ -0,0 +1,281 @@ +# This is a TOML config file. +# For more information, see https://github.com/toml-lang/toml + +##### main base config options ##### + +# TCP or UNIX socket address of the ABCI application, +# or the name of an ABCI application compiled in with the Tendermint binary +proxy_app = "tcp://127.0.0.1:26658" + +# A custom human readable name for this node +moniker = "technodrome" + +# If this node is many blocks behind the tip of the chain, FastSync +# allows them to catchup quickly by downloading blocks in parallel +# and verifying their commits +fast_sync = true + +# Database backend: leveldb | memdb | cleveldb +db_backend = "leveldb" + +# Database directory +db_dir = "data" + +# Output level for logging, including package level options +log_level = "main:info,state:info,*:error" + +# Output format: 'plain' (colored text) or 'json' +log_format = "plain" + +##### additional base config options ##### + +# Path to the JSON file containing the initial validator set and other meta data +genesis_file = "config/genesis.json" + +# Path to the JSON file containing the private key to use as a validator in the consensus protocol +priv_validator_key_file = "config/priv_validator_key.json" + +# Path to the JSON file containing the last sign state of a validator +priv_validator_state_file = "data/priv_validator_state.json" + +# TCP or UNIX socket address for Tendermint to listen on for +# connections from an external PrivValidator process +priv_validator_laddr = "" + +# Path to the JSON file containing the private key to use for node authentication in the p2p protocol +node_key_file = "config/node_key.json" + +# Mechanism to connect to the ABCI application: socket | grpc +abci = "socket" + +# TCP or UNIX socket address for the profiling server to listen on +prof_laddr = "localhost:6060" + +# If true, query the ABCI app on connecting to a new peer +# so the app can decide if we should keep the connection or not +filter_peers = false + +##### advanced configuration options ##### + +##### rpc server configuration options ##### +[rpc] + +# TCP or UNIX socket address for the RPC server to listen on +laddr = "tcp://0.0.0.0:26657" + +# A list of origins a cross-domain request can be executed from +# Default value '[]' disables cors support +# Use '["*"]' to allow any origin +cors_allowed_origins = [] + +# A list of methods the client is allowed to use with cross-domain requests +cors_allowed_methods = ["HEAD", "GET", "POST", ] + +# A list of non simple headers the client is allowed to use with cross-domain requests +cors_allowed_headers = ["Origin", "Accept", "Content-Type", "X-Requested-With", "X-Server-Time", ] + +# TCP or UNIX socket address for the gRPC server to listen on +# NOTE: This server only supports /broadcast_tx_commit +grpc_laddr = "" + +# Maximum number of simultaneous connections. +# Does not include RPC (HTTP&WebSocket) connections. See max_open_connections +# If you want to accept a larger number than the default, make sure +# you increase your OS limits. +# 0 - unlimited. +# Should be < {ulimit -Sn} - {MaxNumInboundPeers} - {MaxNumOutboundPeers} - {N of wal, db and other open files} +# 1024 - 40 - 10 - 50 = 924 = ~900 +grpc_max_open_connections = 900 + +# Activate unsafe RPC commands like /dial_seeds and /unsafe_flush_mempool +unsafe = false + +# Maximum number of simultaneous connections (including WebSocket). +# Does not include gRPC connections. See grpc_max_open_connections +# If you want to accept a larger number than the default, make sure +# you increase your OS limits. +# 0 - unlimited. +# Should be < {ulimit -Sn} - {MaxNumInboundPeers} - {MaxNumOutboundPeers} - {N of wal, db and other open files} +# 1024 - 40 - 10 - 50 = 924 = ~900 +max_open_connections = 900 + +# Maximum number of unique clientIDs that can /subscribe +# If you're using /broadcast_tx_commit, set to the estimated maximum number +# of broadcast_tx_commit calls per block. +max_subscription_clients = 100 + +# Maximum number of unique queries a given client can /subscribe to +# If you're using GRPC (or Local RPC client) and /broadcast_tx_commit, set to +# the estimated # maximum number of broadcast_tx_commit calls per block. +max_subscriptions_per_client = 5 + +# How long to wait for a tx to be committed during /broadcast_tx_commit. +# WARNING: Using a value larger than 10s will result in increasing the +# global HTTP write timeout, which applies to all connections and endpoints. +# See https://github.com/tendermint/tendermint/issues/3435 +timeout_broadcast_tx_commit = "10s" + +# The name of a file containing certificate that is used to create the HTTPS server. +# If the certificate is signed by a certificate authority, +# the certFile should be the concatenation of the server's certificate, any intermediates, +# and the CA's certificate. +# NOTE: both tls_cert_file and tls_key_file must be present for Tendermint to create HTTPS server. Otherwise, HTTP server is run. +tls_cert_file = "" + +# The name of a file containing matching private key that is used to create the HTTPS server. +# NOTE: both tls_cert_file and tls_key_file must be present for Tendermint to create HTTPS server. Otherwise, HTTP server is run. +tls_key_file = "" + +##### peer to peer configuration options ##### +[p2p] + +# Address to listen for incoming connections +laddr = "tcp://0.0.0.0:26656" + +# Address to advertise to peers for them to dial +# If empty, will use the same port as the laddr, +# and will introspect on the listener or use UPnP +# to figure out the address. +external_address = "" + +# Comma separated list of seed nodes to connect to +seeds = "c2e1bde78877975b31e6f06e77da200a38048e2b@seed-1.example.com:26656,0eafed3e9e76f626a299e1b8a79454fffe9ca83c@seed-2.example.com:26656" + +# Comma separated list of nodes to keep persistent connections to +persistent_peers = "tcp://70d834561f91613153e4a873f01a2cbbf1b9678d@1.2.3.4:26656,tcp://f68ed33a0baa0c734a939a9e60659566adc725cd@peer-2.example.com:26656" + +# UPNP port forwarding +upnp = false + +# Path to address book +addr_book_file = "config/addrbook.json" + +# Set true for strict address routability rules +# Set false for private or local networks +addr_book_strict = true + +# Maximum number of inbound peers +max_num_inbound_peers = 40 + +# Maximum number of outbound peers to connect to, excluding persistent peers +max_num_outbound_peers = 10 + +# Time to wait before flushing messages out on the connection +flush_throttle_timeout = "100ms" + +# Maximum size of a message packet payload, in bytes +max_packet_msg_payload_size = 1024 + +# Rate at which packets can be sent, in bytes/second +send_rate = 5120000 + +# Rate at which packets can be received, in bytes/second +recv_rate = 5120000 + +# Set true to enable the peer-exchange reactor +pex = true + +# Seed mode, in which node constantly crawls the network and looks for +# peers. If another node asks it for addresses, it responds and disconnects. +# +# Does not work if the peer-exchange reactor is disabled. +seed_mode = false + +# Comma separated list of peer IDs to keep private (will not be gossiped to other peers) +private_peer_ids = "8112e5c5ab6a48adcc0e875d58a4264a2639f6a8,3d1b9086e48c7bdf7f0d766351eed812a75de500,a7306aee50627e68177a002badd3ba4a45301ad4" + +# Toggle to disable guard against peers connecting from the same ip. +allow_duplicate_ip = false + +# Peer connection configuration. +handshake_timeout = "20s" +dial_timeout = "3s" + +##### mempool configuration options ##### +[mempool] + +recheck = true +broadcast = true +wal_dir = "" + +# Maximum number of transactions in the mempool +size = 5000 + +# Limit the total size of all txs in the mempool. +# This only accounts for raw transactions (e.g. given 1MB transactions and +# max_txs_bytes=5MB, mempool will only accept 5 transactions). +max_txs_bytes = 1073741824 + +# Size of the cache (used to filter transactions we saw earlier) in transactions +cache_size = 10000 + +##### consensus configuration options ##### +[consensus] + +wal_file = "data/cs.wal/wal" + +timeout_propose = "3s" +timeout_propose_delta = "500ms" +timeout_prevote = "1s" +timeout_prevote_delta = "500ms" +timeout_precommit = "1s" +timeout_precommit_delta = "500ms" +timeout_commit = "5s" + +# Make progress as soon as we have all the precommits (as if TimeoutCommit = 0) +skip_timeout_commit = false + +# EmptyBlocks mode and possible interval between empty blocks +create_empty_blocks = true +create_empty_blocks_interval = "0s" + +# Reactor sleep duration parameters +peer_gossip_sleep_duration = "100ms" +peer_query_maj23_sleep_duration = "2s" + +##### transactions indexer configuration options ##### +[tx_index] + +# What indexer to use for transactions +# +# Options: +# 1) "null" +# 2) "kv" (default) - the simplest possible indexer, backed by key-value storage (defaults to levelDB; see DBBackend). +indexer = "kv" + +# Comma-separated list of tags to index (by default the only tag is "tx.hash") +# +# You can also index transactions by height by adding "tx.height" tag here. +# +# It's recommended to index only a subset of tags due to possible memory +# bloat. This is, of course, depends on the indexer's DB and the volume of +# transactions. +index_tags = "tx.height" + +# When set to true, tells indexer to index all tags (predefined tags: +# "tx.hash", "tx.height" and all tags from DeliverTx responses). +# +# Note this may be not desirable (see the comment above). IndexTags has a +# precedence over IndexAllTags (i.e. when given both, IndexTags will be +# indexed). +index_all_tags = true + +##### instrumentation configuration options ##### +[instrumentation] + +# When true, Prometheus metrics are served under /metrics on +# PrometheusListenAddr. +# Check out the documentation for the list of available metrics. +prometheus = false + +# Address to listen for Prometheus collector(s) connections +prometheus_listen_addr = ":26660" + +# Maximum number of simultaneous connections. +# If you want to accept a larger number than the default, make sure +# you increase your OS limits. +# 0 - unlimited. +max_open_connections = 3 + +# Instrumentation namespace +namespace = "tendermint" diff --git a/tendermint-rs/tests/support/config/node_key.json b/tendermint-rs/tests/support/config/node_key.json new file mode 100644 index 0000000..b3e57eb --- /dev/null +++ b/tendermint-rs/tests/support/config/node_key.json @@ -0,0 +1 @@ +{"priv_key":{"type":"tendermint/PrivKeyEd25519","value":"GRKDcf6krxXq2csRmIC0TNO/SZqoDIxN7JbxehQnjqGkBIVze7BvLGGn72mA68qvubnex30PhoJcXHGYtMl/tA=="}} \ No newline at end of file diff --git a/tendermint-rs/tests/support/config/priv_validator_key.json b/tendermint-rs/tests/support/config/priv_validator_key.json new file mode 100644 index 0000000..df24727 --- /dev/null +++ b/tendermint-rs/tests/support/config/priv_validator_key.json @@ -0,0 +1,11 @@ +{ + "address": "AD7DAE5FEC609CF02F9BDE7D81D0C3CD66141563", + "pub_key": { + "type": "tendermint/PubKeyEd25519", + "value": "8mv0sqLoTOt6U8PxrndAh3myAGR4L7rb3w42WVnuRTQ=" + }, + "priv_key": { + "type": "tendermint/PrivKeyEd25519", + "value": "skHDGUYe2pOhwfSrXZQ6KeKnmKgTOn+f++Vmj4OOqIHya/SyouhM63pTw/Gud0CHebIAZHgvutvfDjZZWe5FNA==" + } +} \ No newline at end of file diff --git a/tendermint-rs/tests/support/rpc/genesis.json b/tendermint-rs/tests/support/rpc/genesis.json index 3cbf426..f0ec878 100644 --- a/tendermint-rs/tests/support/rpc/genesis.json +++ b/tendermint-rs/tests/support/rpc/genesis.json @@ -6,9 +6,10 @@ "genesis_time": "2019-03-13T23:00:00Z", "chain_id": "cosmoshub-1", "consensus_params": { - "block_size": { - "max_bytes": "150000", - "max_gas": "1500000" + "block": { + "max_bytes": "200000", + "max_gas": "2000000", + "time_iota_ms": "1000" }, "evidence": { "max_age": "1000000"