From 2df4219c2cfb25c16d6d392f983b86c008b36b47 Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Mon, 29 Mar 2021 12:45:24 -0400 Subject: [PATCH] Add TLS support for the Light Client (#842) * Remove unused file Signed-off-by: Thane Thomson * Refactor validators RPC endpoint interface This commit adds pagination to the `validators` method on the `Client` trait (BREAKING). Signed-off-by: Thane Thomson * Ensure "total" response field is a string Signed-off-by: Thane Thomson * Add serializer for optional types that need to be converted to/from a string (like page numbers/per page counts) Signed-off-by: Thane Thomson * Refactor to ensure page numbers and per-page values are converted to/from strings first Signed-off-by: Thane Thomson * Convert tcp:// scheme to http:// for RPC addresses Signed-off-by: Thane Thomson * Add Light Client support for RPC URLs instead of net::Address Signed-off-by: Thane Thomson * Revert 14ad69f for now Signed-off-by: Thane Thomson * Revert f0c26f7 Signed-off-by: Thane Thomson * Add CHANGELOG Signed-off-by: Thane Thomson * Convert tcp:// scheme to http:// for RPC addresses Signed-off-by: Thane Thomson * Add Light Client support for RPC URLs instead of net::Address Signed-off-by: Thane Thomson * Comment not needed Signed-off-by: Thane Thomson * Expose rpc::Url type Signed-off-by: Thane Thomson * Update kvstore integration test to use rpc::Url Signed-off-by: Thane Thomson * Update CHANGELOG Signed-off-by: Thane Thomson * Remove debug output from height log Signed-off-by: Thane Thomson * Attach serialization directly to tendermint_rpc::Url Signed-off-by: Thane Thomson * Add some happy path tests for tendermint_rpc::Url parsing Signed-off-by: Thane Thomson --- CHANGELOG.md | 10 ++ light-client/examples/light_client.rs | 4 +- light-client/src/builder/supervisor.rs | 12 +- light-client/src/evidence.rs | 4 +- light-client/src/supervisor.rs | 2 +- light-node/Cargo.toml | 1 + light-node/src/commands/initialize.rs | 30 +++-- light-node/src/commands/start.rs | 30 +++-- light-node/src/config.rs | 2 +- rpc/Cargo.toml | 11 +- rpc/src/endpoint/validators.rs | 2 + rpc/src/error.rs | 1 - rpc/src/lib.rs | 20 ++-- rpc/src/rpc_url.rs | 146 +++++++++++++++++++++-- tools/kvstore-test/tests/light-client.rs | 9 +- 15 files changed, 223 insertions(+), 61 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c21acf8f..ef743ec67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ * `[tendermint]` The `tendermint::block::CommitSig` enum's members have been renamed to be consistent with Rust's naming conventions. For example, `BlockIDFlagAbsent` is now renamed to `BlockIdFlagAbsent` ([#839]) +* `[tendermint-light-client]` The Light Client no longer uses + `tendermint::net::Address` to refer to peers, and instead uses the + `tendermint_rpc::Url` type ([#835]) +* `[tendermint-rpc]` The `Client::validators` method now requires a `Paging` + parameter. Previously, this wasn't possible and, if the network had more than + 30 validators (the default for the RPC endpoint), it only returned a subset + of the validators ([#831]) * `[tendermint-rpc]` The `Client::validators` method now requires a `Paging` parameter. Previously, this wasn't possible and, if the network had more than 30 validators (the default for the RPC endpoint), it only returned a subset @@ -27,6 +34,8 @@ * `[tendermint-abci]` Release minimal framework for building ABCI applications in Rust ([#794]) +* `[tendermint-light-client]` The Light Client now provides support for secure + (HTTPS) connections to nodes ([#835]) * `[tendermint-light-client-js]` First release of the `tendermint-light-client-js` crate to provide access to Tendermint Light Client functionality from WASM. This only provides access to the `verify` @@ -55,6 +64,7 @@ [#812]: https://github.com/informalsystems/tendermint-rs/pull/812 [#820]: https://github.com/informalsystems/tendermint-rs/pull/820 [#831]: https://github.com/informalsystems/tendermint-rs/issues/831 +[#835]: https://github.com/informalsystems/tendermint-rs/issues/835 [#839]: https://github.com/informalsystems/tendermint-rs/pull/839 ## v0.18.1 diff --git a/light-client/examples/light_client.rs b/light-client/examples/light_client.rs index dc14fcd66..9c3b7ce36 100644 --- a/light-client/examples/light_client.rs +++ b/light-client/examples/light_client.rs @@ -43,7 +43,7 @@ struct SyncOpts { meta = "ADDR", default = "tcp://127.0.0.1:26657" )] - address: tendermint::net::Address, + address: tendermint_rpc::Url, #[options( help = "height of the initial trusted state (optional if store already initialized)", meta = "HEIGHT" @@ -81,7 +81,7 @@ fn main() { fn make_instance( peer_id: PeerId, - addr: tendermint::net::Address, + addr: tendermint_rpc::Url, db_path: impl AsRef, opts: &SyncOpts, ) -> Result { diff --git a/light-client/src/builder/supervisor.rs b/light-client/src/builder/supervisor.rs index 18469f682..0cb8ba647 100644 --- a/light-client/src/builder/supervisor.rs +++ b/light-client/src/builder/supervisor.rs @@ -1,7 +1,5 @@ use std::time::Duration; -use tendermint::net; - use crate::builder::error::{self, Error}; use crate::peer_list::{PeerList, PeerListBuilder}; use crate::supervisor::Instance; @@ -21,7 +19,7 @@ pub struct Done; #[must_use] pub struct SupervisorBuilder { instances: PeerListBuilder, - addresses: PeerListBuilder, + addresses: PeerListBuilder, evidence_reporting_timeout: Option, #[allow(dead_code)] state: State, @@ -66,7 +64,7 @@ impl SupervisorBuilder { pub fn primary( mut self, peer_id: PeerId, - address: net::Address, + address: tendermint_rpc::Url, instance: Instance, ) -> SupervisorBuilder { self.instances.primary(peer_id, instance); @@ -81,7 +79,7 @@ impl SupervisorBuilder { pub fn witness( mut self, peer_id: PeerId, - address: net::Address, + address: tendermint_rpc::Url, instance: Instance, ) -> SupervisorBuilder { self.instances.witness(peer_id, instance); @@ -93,7 +91,7 @@ impl SupervisorBuilder { /// Add multiple witnesses at once. pub fn witnesses( mut self, - witnesses: impl IntoIterator, + witnesses: impl IntoIterator, ) -> Result, Error> { let mut iter = witnesses.into_iter().peekable(); if iter.peek().is_none() { @@ -126,7 +124,7 @@ impl SupervisorBuilder { /// Get the underlying list of instances and addresses. #[must_use] - pub fn inner(self) -> (PeerList, PeerList) { + pub fn inner(self) -> (PeerList, PeerList) { (self.instances.build(), self.addresses.build()) } } diff --git a/light-client/src/evidence.rs b/light-client/src/evidence.rs index d18b10c7e..2bfd7c411 100644 --- a/light-client/src/evidence.rs +++ b/light-client/src/evidence.rs @@ -34,7 +34,7 @@ mod prod { /// nodes via RPC. #[derive(Clone, Debug)] pub struct ProdEvidenceReporter { - peer_map: HashMap, + peer_map: HashMap, timeout: Option, } @@ -61,7 +61,7 @@ mod prod { /// /// A peer map which maps peer IDS to their network address must be supplied. pub fn new( - peer_map: HashMap, + peer_map: HashMap, timeout: Option, ) -> Self { Self { peer_map, timeout } diff --git a/light-client/src/supervisor.rs b/light-client/src/supervisor.rs index 542daa545..fea174c97 100644 --- a/light-client/src/supervisor.rs +++ b/light-client/src/supervisor.rs @@ -142,7 +142,7 @@ impl std::fmt::Debug for Supervisor { static_assertions::assert_impl_all!(Supervisor: Send); impl Supervisor { - /// Constructs a new supevisor from the given list of peers and fork detector instance. + /// Constructs a new supervisor from the given list of peers and fork detector instance. pub fn new( peers: PeerList, fork_detector: impl ForkDetector + 'static, diff --git a/light-node/Cargo.toml b/light-node/Cargo.toml index 18a1b8b3c..29dbe1474 100644 --- a/light-node/Cargo.toml +++ b/light-node/Cargo.toml @@ -40,6 +40,7 @@ thiserror = "1.0" tendermint = { version = "0.18.1", path = "../tendermint" } tendermint-light-client = { version = "0.18.1", path = "../light-client", features = ["lightstore-sled"] } +tendermint-proto = { version = "0.18.1", path = "../proto" } tendermint-rpc = { version = "0.18.1", path = "../rpc", features = ["http-client"] } [dependencies.abscissa_core] diff --git a/light-node/src/commands/initialize.rs b/light-node/src/commands/initialize.rs index b1013b288..1895ff81e 100644 --- a/light-node/src/commands/initialize.rs +++ b/light-node/src/commands/initialize.rs @@ -4,14 +4,9 @@ use std::ops::Deref; use std::time::Duration; use crate::application::app_config; -use crate::config::LightClientConfig; -use crate::config::LightNodeConfig; +use crate::config::{LightClientConfig, LightNodeConfig}; -use abscissa_core::status_err; -use abscissa_core::status_warn; -use abscissa_core::Command; -use abscissa_core::Options; -use abscissa_core::Runnable; +use abscissa_core::{status_err, status_info, status_warn, Command, Options, Runnable}; use tendermint::{hash, Hash}; @@ -57,6 +52,8 @@ impl Runnable for InitCmd { ) { status_err!("failed to initialize light client: {}", e); // TODO: Set exit code to 1 + } else { + status_info!("init", "done"); } } } @@ -68,6 +65,20 @@ fn initialize_subjectively( config: &LightClientConfig, timeout: Option, ) -> Result { + status_info!( + "init", + "starting subjective initialization for height: {}", + height, + ); + status_info!("init", "subjective header hash: {}", subjective_header_hash,); + status_info!( + "init", + "using sled store located at: {}", + config + .db_path + .to_str() + .ok_or("unable to obtain sled db path")? + ); let light_store = SledStore::open(&config.db_path).map_err(|e| format!("could not open database: {}", e))?; @@ -79,6 +90,11 @@ fn initialize_subjectively( ); } + status_info!( + "init", + "Tendermint RPC address: {}", + config.address.to_string() + ); let rpc_client = rpc::HttpClient::new(config.address.clone()).map_err(|e| e.to_string())?; let builder = LightClientBuilder::prod( diff --git a/light-node/src/commands/start.rs b/light-node/src/commands/start.rs index dcde6e008..14a2058b2 100644 --- a/light-node/src/commands/start.rs +++ b/light-node/src/commands/start.rs @@ -5,14 +5,8 @@ use crate::config::{LightClientConfig, LightNodeConfig}; use crate::rpc; use crate::rpc::Server; -use abscissa_core::config; use abscissa_core::path::PathBuf; -use abscissa_core::status_err; -use abscissa_core::status_info; -use abscissa_core::Command; -use abscissa_core::FrameworkError; -use abscissa_core::Options; -use abscissa_core::Runnable; +use abscissa_core::{config, status_err, status_info, Command, FrameworkError, Options, Runnable}; use std::net::SocketAddr; use std::ops::Deref; @@ -125,12 +119,27 @@ impl StartCmd { options: light_client::Options, timeout: Option, ) -> Result { + status_info!( + "start", + "constructing Light Client for peer {}", + light_config.peer_id.to_string() + ); + status_info!("start", "RPC address: {}", light_config.address.to_string()); let rpc_client = tendermint_rpc::HttpClient::new(light_config.address.clone()) .map_err(|e| format!("failed to create HTTP client: {}", e))?; let light_store = SledStore::open(&light_config.db_path) .map_err(|e| format!("could not open database: {}", e))?; + status_info!( + "start", + "highest trusted or verified height: {}", + light_store + .highest_trusted_or_verified() + .map(|b| b.signed_header.header.height.to_string()) + .unwrap_or_else(|| "(none)".to_owned()), + ); + let builder = LightClientBuilder::prod( light_config.peer_id, rpc_client, @@ -161,6 +170,12 @@ impl StartCmd { let builder = SupervisorBuilder::new(); + status_info!( + "start", + "primary: {} @ {}", + primary_conf.peer_id, + primary_conf.address + ); let primary_instance = self.make_instance(primary_conf, options, Some(timeout))?; let builder = builder.primary( primary_conf.peer_id, @@ -173,6 +188,7 @@ impl StartCmd { let instance = self.make_instance(witness_conf, options, Some(timeout))?; witnesses.push((witness_conf.peer_id, witness_conf.address.clone(), instance)); } + status_info!("start", "{} witness(es)", witnesses.len()); let builder = builder .witnesses(witnesses) diff --git a/light-node/src/config.rs b/light-node/src/config.rs index bf2ac1944..b75080b5c 100644 --- a/light-node/src/config.rs +++ b/light-node/src/config.rs @@ -41,7 +41,7 @@ pub struct LightNodeConfig { pub struct LightClientConfig { /// Address of the Tendermint fullnode to connect to and /// fetch LightBlock data from. - pub address: tendermint::net::Address, + pub address: tendermint_rpc::Url, /// PeerID of the same Tendermint fullnode. pub peer_id: PeerId, /// The data base folder for this instance's store. diff --git a/rpc/Cargo.toml b/rpc/Cargo.toml index 2320291e4..22b03f079 100644 --- a/rpc/Cargo.toml +++ b/rpc/Cargo.toml @@ -45,8 +45,7 @@ http-client = [ "hyper-rustls", "tokio/fs", "tokio/macros", - "tracing", - "url" + "tracing" ] secp256k1 = [ "tendermint/secp256k1" ] websocket-client = [ @@ -58,8 +57,7 @@ websocket-client = [ "tokio/macros", "tokio/sync", "tokio/time", - "tracing", - "url" + "tracing" ] [dependencies] @@ -78,6 +76,7 @@ tendermint-proto = { version = "0.18.1", path = "../proto" } thiserror = "1" uuid = { version = "0.8", default-features = false } subtle-encoding = { version = "0.5", features = ["bech32-preview"] } +url = "2.2" walkdir = "2.3" async-trait = { version = "0.1", optional = true } @@ -91,4 +90,6 @@ structopt = { version = "0.3", optional = true } tokio = { version = "1.0", optional = true } tracing = { version = "0.1", optional = true } tracing-subscriber = { version = "0.2", optional = true } -url = { version = "2.2", optional = true } + +[dev-dependencies] +lazy_static = "1.4.0" diff --git a/rpc/src/endpoint/validators.rs b/rpc/src/endpoint/validators.rs index a5529a158..efe3f0c31 100644 --- a/rpc/src/endpoint/validators.rs +++ b/rpc/src/endpoint/validators.rs @@ -9,6 +9,7 @@ pub const DEFAULT_VALIDATORS_PER_PAGE: u8 = 30; /// List validators for a specific block #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[non_exhaustive] pub struct Request { /// The height at which to retrieve the validator set. If not specified, /// defaults to the latest height. @@ -65,6 +66,7 @@ impl crate::SimpleRequest for Request {} /// Validator responses #[derive(Clone, Debug, Deserialize, Serialize)] +#[non_exhaustive] pub struct Response { /// Block height pub block_height: block::Height, diff --git a/rpc/src/error.rs b/rpc/src/error.rs index eeddaabf6..7a9596ac4 100644 --- a/rpc/src/error.rs +++ b/rpc/src/error.rs @@ -117,7 +117,6 @@ impl From for Error { } } -#[cfg(any(feature = "http-client", feature = "websocket-client"))] impl From for Error { fn from(e: url::ParseError) -> Self { Error::invalid_params(&e.to_string()) diff --git a/rpc/src/lib.rs b/rpc/src/lib.rs index a6e419e1d..b629778f0 100644 --- a/rpc/src/lib.rs +++ b/rpc/src/lib.rs @@ -30,14 +30,10 @@ #[cfg(any(feature = "http-client", feature = "websocket-client"))] mod client; #[cfg(any(feature = "http-client", feature = "websocket-client"))] -mod rpc_url; -#[cfg(any(feature = "http-client", feature = "websocket-client"))] pub use client::{ Client, MockClient, MockRequestMatcher, MockRequestMethodMatcher, Subscription, SubscriptionClient, }; -#[cfg(any(feature = "http-client", feature = "websocket-client"))] -pub use rpc_url::{Scheme, Url}; #[cfg(feature = "http-client")] pub use client::{HttpClient, HttpClientUrl}; @@ -55,11 +51,17 @@ pub mod query; pub mod request; pub mod response; mod result; +mod rpc_url; mod utils; mod version; -pub use self::{ - error::Error, id::Id, method::Method, order::Order, paging::PageNumber, paging::Paging, - paging::PerPage, request::Request, request::SimpleRequest, response::Response, result::Result, - version::Version, -}; +pub use error::Error; +pub use id::Id; +pub use method::Method; +pub use order::Order; +pub use paging::{PageNumber, Paging, PerPage}; +pub use request::{Request, SimpleRequest}; +pub use response::Response; +pub use result::Result; +pub use rpc_url::{Scheme, Url}; +pub use version::Version; diff --git a/rpc/src/rpc_url.rs b/rpc/src/rpc_url.rs index f727a5fb6..e909ed41f 100644 --- a/rpc/src/rpc_url.rs +++ b/rpc/src/rpc_url.rs @@ -1,12 +1,13 @@ //! URL representation for RPC clients. -use crate::{Error, Result}; +use serde::de::Error; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::convert::TryFrom; use std::fmt; use std::str::FromStr; /// The various schemes supported by Tendermint RPC clients. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, Eq, PartialEq)] pub enum Scheme { Http, Https, @@ -26,15 +27,20 @@ impl fmt::Display for Scheme { } impl FromStr for Scheme { - type Err = Error; + type Err = crate::Error; - fn from_str(s: &str) -> Result { + fn from_str(s: &str) -> Result { Ok(match s { - "http" => Scheme::Http, + "http" | "tcp" => Scheme::Http, "https" => Scheme::Https, "ws" => Scheme::WebSocket, "wss" => Scheme::SecureWebSocket, - _ => return Err(Error::invalid_params(&format!("unsupported scheme: {}", s))), + _ => { + return Err(crate::Error::invalid_params(&format!( + "unsupported scheme: {}", + s + ))) + } }) } } @@ -53,17 +59,22 @@ pub struct Url { } impl FromStr for Url { - type Err = Error; + type Err = crate::Error; - fn from_str(s: &str) -> Result { + fn from_str(s: &str) -> Result { let inner: url::Url = s.parse()?; let scheme: Scheme = inner.scheme().parse()?; let host = inner .host_str() - .ok_or_else(|| Error::invalid_params(&format!("URL is missing its host: {}", s)))? + .ok_or_else(|| { + crate::Error::invalid_params(&format!("URL is missing its host: {}", s)) + })? .to_owned(); let port = inner.port_or_known_default().ok_or_else(|| { - Error::invalid_params(&format!("cannot determine appropriate port for URL: {}", s)) + crate::Error::invalid_params(&format!( + "cannot determine appropriate port for URL: {}", + s + )) })?; Ok(Self { inner, @@ -124,9 +135,120 @@ impl fmt::Display for Url { } impl TryFrom for Url { - type Error = Error; + type Error = crate::Error; - fn try_from(value: url::Url) -> Result { + fn try_from(value: url::Url) -> Result { value.to_string().parse() } } + +impl Serialize for Url { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + self.to_string().serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for Url { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + Url::from_str(&s).map_err(|e| D::Error::custom(e.to_string())) + } +} + +#[cfg(test)] +mod test { + use super::*; + use lazy_static::lazy_static; + + struct ExpectedUrl { + scheme: Scheme, + host: String, + port: u16, + path: String, + username: String, + password: Option, + } + + lazy_static! { + static ref SUPPORTED_URLS: Vec<(String, ExpectedUrl)> = vec![ + ( + "tcp://127.0.0.1:26657".to_owned(), + ExpectedUrl { + scheme: Scheme::Http, + host: "127.0.0.1".to_string(), + port: 26657, + path: "".to_string(), + username: "".to_string(), + password: None, + } + ), + ( + "http://127.0.0.1:26657".to_owned(), + ExpectedUrl { + scheme: Scheme::Http, + host: "127.0.0.1".to_string(), + port: 26657, + path: "/".to_string(), + username: "".to_string(), + password: None, + } + ), + ( + "https://127.0.0.1:26657".to_owned(), + ExpectedUrl { + scheme: Scheme::Https, + host: "127.0.0.1".to_string(), + port: 26657, + path: "/".to_string(), + username: "".to_string(), + password: None, + } + ), + ( + "ws://127.0.0.1:26657/websocket".to_owned(), + ExpectedUrl { + scheme: Scheme::WebSocket, + host: "127.0.0.1".to_string(), + port: 26657, + path: "/websocket".to_string(), + username: "".to_string(), + password: None, + } + ), + ( + "wss://127.0.0.1:26657/websocket".to_owned(), + ExpectedUrl { + scheme: Scheme::SecureWebSocket, + host: "127.0.0.1".to_string(), + port: 26657, + path: "/websocket".to_string(), + username: "".to_string(), + password: None, + } + ) + ]; + } + + #[test] + fn parsing() { + for (url_str, expected) in SUPPORTED_URLS.iter() { + let u = Url::from_str(url_str).unwrap(); + assert_eq!(expected.scheme, u.scheme(), "{}", url_str); + assert_eq!(expected.host, u.host(), "{}", url_str); + assert_eq!(expected.port, u.port(), "{}", url_str); + assert_eq!(expected.path, u.path(), "{}", url_str); + assert_eq!(expected.username, u.username()); + if let Some(pw) = u.password() { + assert_eq!(expected.password.as_ref().unwrap(), pw, "{}", url_str); + } else { + assert!(expected.password.is_none(), "{}", url_str); + } + } + } +} diff --git a/tools/kvstore-test/tests/light-client.rs b/tools/kvstore-test/tests/light-client.rs index b82a86f35..ac57bc2c5 100644 --- a/tools/kvstore-test/tests/light-client.rs +++ b/tools/kvstore-test/tests/light-client.rs @@ -25,7 +25,6 @@ use tendermint_light_client::{ }; use tendermint::abci::transaction::Hash as TxHash; -use tendermint::net; use tendermint_rpc as rpc; use std::convert::TryFrom; @@ -43,11 +42,7 @@ impl EvidenceReporter for TestEvidenceReporter { } } -fn make_instance( - peer_id: PeerId, - options: light_client::Options, - address: net::Address, -) -> Instance { +fn make_instance(peer_id: PeerId, options: light_client::Options, address: rpc::Url) -> Instance { let rpc_client = rpc::HttpClient::new(address).unwrap(); let io = ProdIo::new(peer_id, rpc_client.clone(), Some(Duration::from_secs(2))); let latest_block = io.fetch_light_block(AtHeight::Highest).unwrap(); @@ -76,7 +71,7 @@ fn make_supervisor() -> Supervisor { // In a production environment, one should make sure that the primary and witness are // different nodes, and check that the configured peer IDs match the ones returned // by the nodes. - let node_address: tendermint::net::Address = "tcp://127.0.0.1:26657".parse().unwrap(); + let node_address: rpc::Url = "http://127.0.0.1:26657".parse().unwrap(); let options = light_client::Options { trust_threshold: TrustThreshold {