diff --git a/CHANGELOG.md b/CHANGELOG.md index d6b81f762..70c7eda40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ * `[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-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 `SubscriptionClient` trait now requires a `close` method, since it assumes that subscription clients will, in general, use long-running connections. This should not, however, break any downstream @@ -35,9 +39,17 @@ * `[tendermint-rpc]` A `tendermint-rpc` CLI has been added to simplify interaction with RPC endpoints from the command line ([#820]) +### BUG FIXES + +* `[tendermint-light-client]` Due to the RPC client's `validators` method + sometimes only returning a subset of validators (for networks larger than 30 + validators), validator set hash calculations were failing. Now we are at + least obtaining a full validator set ([#831]) + [#794]: https://github.com/informalsystems/tendermint-rs/pull/794 [#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 [#839]: https://github.com/informalsystems/tendermint-rs/pull/839 ## v0.18.1 diff --git a/light-client-js/Cargo.toml b/light-client-js/Cargo.toml index 97e7a8753..2c2eb6e8c 100644 --- a/light-client-js/Cargo.toml +++ b/light-client-js/Cargo.toml @@ -22,6 +22,8 @@ default = ["console_error_panic_hook"] [dependencies] serde = { version = "1.0", features = [ "derive" ] } serde_json = "1.0" +# TODO(thane): Remove once https://github.com/rustwasm/wasm-bindgen/issues/2508 is resolved +syn = "=1.0.65" tendermint = { version = "0.18.1", path = "../tendermint" } tendermint-light-client = { version = "0.18.1", path = "../light-client", default-features = false } wasm-bindgen = { version = "0.2.63", features = [ "serde-serialize" ] } diff --git a/light-client/src/components/io.rs b/light-client/src/components/io.rs index 8aeef4c0a..e068edfa9 100644 --- a/light-client/src/components/io.rs +++ b/light-client/src/components/io.rs @@ -91,6 +91,7 @@ mod prod { use tendermint::account::Id as TMAccountId; use tendermint::block::signed_header::SignedHeader as TMSignedHeader; use tendermint::validator::Set as TMValidatorSet; + use tendermint_rpc::Paging; /// Production implementation of the Io component, which fetches /// light blocks from full nodes via RPC. @@ -166,8 +167,10 @@ mod prod { }; let client = self.rpc_client.clone(); - let response = block_on(self.timeout, async move { client.validators(height).await })? - .map_err(IoError::RpcError)?; + let response = block_on(self.timeout, async move { + client.validators(height, Paging::All).await + })? + .map_err(IoError::RpcError)?; let validator_set = match proposer_address { Some(proposer_address) => { diff --git a/proto/src/serializers.rs b/proto/src/serializers.rs index 20202eb83..239a8b53f 100644 --- a/proto/src/serializers.rs +++ b/proto/src/serializers.rs @@ -49,6 +49,7 @@ pub mod evidence; pub mod from_str; pub mod nullable; pub mod optional; +pub mod optional_from_str; pub mod part_set_header_total; pub mod time_duration; pub mod timestamp; diff --git a/proto/src/serializers/optional_from_str.rs b/proto/src/serializers/optional_from_str.rs new file mode 100644 index 000000000..2752be880 --- /dev/null +++ b/proto/src/serializers/optional_from_str.rs @@ -0,0 +1,31 @@ +//! De/serialize an optional type that must be converted from/to a string. + +use serde::de::Error; +use serde::{Deserialize, Deserializer, Serializer}; +use std::str::FromStr; + +pub fn serialize(value: &Option, serializer: S) -> Result +where + S: Serializer, + T: ToString, +{ + match value { + Some(t) => serializer.serialize_some(&t.to_string()), + None => serializer.serialize_none(), + } +} + +pub fn deserialize<'de, D, T>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, + T: FromStr, + T::Err: std::error::Error, +{ + let s = match Option::::deserialize(deserializer)? { + Some(s) => s, + None => return Ok(None), + }; + Ok(Some(s.parse().map_err(|e: ::Err| { + D::Error::custom(e.to_string()) + })?)) +} diff --git a/rpc/src/client.rs b/rpc/src/client.rs index 39dab71b5..cb4070f55 100644 --- a/rpc/src/client.rs +++ b/rpc/src/client.rs @@ -12,7 +12,9 @@ pub use transport::http::{HttpClient, HttpClientUrl}; #[cfg(feature = "websocket-client")] pub use transport::websocket::{WebSocketClient, WebSocketClientDriver, WebSocketClientUrl}; +use crate::endpoint::validators::DEFAULT_VALIDATORS_PER_PAGE; use crate::endpoint::*; +use crate::paging::Paging; use crate::query::Query; use crate::{Order, Result, SimpleRequest}; use async_trait::async_trait; @@ -123,12 +125,53 @@ pub trait Client { self.perform(consensus_state::Request::new()).await } + // TODO(thane): Simplify once validators endpoint removes pagination. /// `/validators`: get validators a given height. - async fn validators(&self, height: H) -> Result + async fn validators(&self, height: H, paging: Paging) -> Result where H: Into + Send, { - self.perform(validators::Request::new(height.into())).await + let height = height.into(); + match paging { + Paging::Default => { + self.perform(validators::Request::new(Some(height), None, None)) + .await + } + Paging::Specific { + page_number, + per_page, + } => { + self.perform(validators::Request::new( + Some(height), + Some(page_number), + Some(per_page), + )) + .await + } + Paging::All => { + let mut page_num = 1_usize; + let mut validators = Vec::new(); + let per_page = DEFAULT_VALIDATORS_PER_PAGE.into(); + loop { + let response = self + .perform(validators::Request::new( + Some(height), + Some(page_num.into()), + Some(per_page), + )) + .await?; + validators.extend(response.validators); + if validators.len() as i32 == response.total { + return Ok(validators::Response::new( + response.block_height, + validators, + response.total, + )); + } + page_num += 1; + } + } + } } /// `/commit`: get the latest block commit diff --git a/rpc/src/client/bin/main.rs b/rpc/src/client/bin/main.rs index ed90c5d00..4c6f06b3d 100644 --- a/rpc/src/client/bin/main.rs +++ b/rpc/src/client/bin/main.rs @@ -7,8 +7,8 @@ use structopt::StructOpt; use tendermint::abci::{Path, Transaction}; use tendermint_rpc::query::Query; use tendermint_rpc::{ - Client, Error, HttpClient, Order, Result, Scheme, Subscription, SubscriptionClient, Url, - WebSocketClient, + Client, Error, HttpClient, Order, Paging, Result, Scheme, Subscription, SubscriptionClient, + Url, WebSocketClient, }; use tracing::level_filters::LevelFilter; use tracing::{error, info, warn}; @@ -141,7 +141,19 @@ enum ClientRequest { prove: bool, }, /// Get the validators at the given height. - Validators { height: u32 }, + Validators { + /// The height at which to query the validators. + height: u32, + /// Fetch all validators. + #[structopt(long)] + all: bool, + /// The page of validators to retrieve. + #[structopt(long)] + page: Option, + /// The number of validators to retrieve per page. + #[structopt(long)] + per_page: Option, + }, } #[tokio::main] @@ -321,8 +333,24 @@ where .tx_search(query, prove, page, per_page, order) .await?, )?, - ClientRequest::Validators { height } => { - serde_json::to_string_pretty(&client.validators(height).await?)? + ClientRequest::Validators { + height, + all, + page, + per_page, + } => { + let paging = if all { + Paging::All + } else { + match page.zip(per_page) { + Some((page, per_page)) => Paging::Specific { + page_number: page.into(), + per_page: per_page.into(), + }, + None => Paging::Default, + } + }; + serde_json::to_string_pretty(&client.validators(height, paging).await?)? } }; println!("{}", result); @@ -338,7 +366,7 @@ async fn subscription_client_request( where C: SubscriptionClient, { - info!("Creating subcription for query: {}", query); + info!("Creating subscription for query: {}", query); let subs = client.subscribe(query).await?; match max_time { Some(secs) => recv_events_with_timeout(subs, max_events, secs).await, diff --git a/rpc/src/endpoint/consensus_state.rs b/rpc/src/endpoint/consensus_state.rs index 9e34b3e9a..b7110c4ce 100644 --- a/rpc/src/endpoint/consensus_state.rs +++ b/rpc/src/endpoint/consensus_state.rs @@ -14,7 +14,6 @@ const NIL_VOTE_STR: &str = "nil-Vote"; /// Get the current consensus state. #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] -#[non_exhaustive] pub struct Request; impl Request { diff --git a/rpc/src/endpoint/validators.rs b/rpc/src/endpoint/validators.rs index f0fa70865..a5529a158 100644 --- a/rpc/src/endpoint/validators.rs +++ b/rpc/src/endpoint/validators.rs @@ -1,19 +1,55 @@ //! `/validators` endpoint JSON-RPC wrapper +use crate::{PageNumber, PerPage}; use serde::{Deserialize, Serialize}; - use tendermint::{block, validator}; +/// The default number of validators to return per page. +pub const DEFAULT_VALIDATORS_PER_PAGE: u8 = 30; + /// List validators for a specific block #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] pub struct Request { - pub height: block::Height, + /// The height at which to retrieve the validator set. If not specified, + /// defaults to the latest height. + pub height: Option, + /// The number of the page to fetch. + #[serde(with = "tendermint_proto::serializers::optional_from_str")] + pub page: Option, + /// The number of validators to fetch per page. + #[serde(with = "tendermint_proto::serializers::optional_from_str")] + pub per_page: Option, } impl Request { - /// List validators for a specific block - pub fn new(height: block::Height) -> Self { - Self { height } + /// List validators for a specific block. + /// + /// See the [Tendermint RPC] for the defaults for each option when set to + /// `None`. + /// + /// [Tendermint RPC]: https://docs.tendermint.com/master/rpc/#/Info/validators + pub fn new( + height: Option, + page: Option, + per_page: Option, + ) -> Self { + Self { + height, + page, + per_page, + } + } +} + +impl Default for Request { + fn default() -> Self { + // By default we get the latest validators list, page 1, maximum 30 + // items per page (the RPC defaults). + Self { + height: None, + page: None, + per_page: None, + } } } @@ -35,6 +71,21 @@ pub struct Response { /// Validator list pub validators: Vec, + + /// Total number of validators for this block height. + #[serde(with = "tendermint_proto::serializers::from_str")] + pub total: i32, } impl crate::Response for Response {} + +impl Response { + /// Constructor. + pub fn new(block_height: block::Height, validators: Vec, total: i32) -> Self { + Self { + block_height, + validators, + total, + } + } +} diff --git a/rpc/src/error.rs b/rpc/src/error.rs index 83d23a9db..eeddaabf6 100644 --- a/rpc/src/error.rs +++ b/rpc/src/error.rs @@ -7,6 +7,8 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::fmt::{self, Display}; use thiserror::Error; +// TODO(thane): Differentiate between RPC response errors and internal crate +// errors (e.g. domain type-related errors). /// Tendermint RPC errors #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub struct Error { diff --git a/rpc/src/lib.rs b/rpc/src/lib.rs index c53ae8df6..a6e419e1d 100644 --- a/rpc/src/lib.rs +++ b/rpc/src/lib.rs @@ -50,6 +50,7 @@ pub mod event; mod id; mod method; mod order; +mod paging; pub mod query; pub mod request; pub mod response; @@ -58,6 +59,7 @@ mod utils; mod version; pub use self::{ - error::Error, id::Id, method::Method, order::Order, request::Request, request::SimpleRequest, - response::Response, result::Result, version::Version, + error::Error, id::Id, method::Method, order::Order, paging::PageNumber, paging::Paging, + paging::PerPage, request::Request, request::SimpleRequest, response::Response, result::Result, + version::Version, }; diff --git a/rpc/src/paging.rs b/rpc/src/paging.rs new file mode 100644 index 000000000..19f538d1d --- /dev/null +++ b/rpc/src/paging.rs @@ -0,0 +1,78 @@ +//! Pagination-related data structures for the Tendermint RPC. + +use crate::Error; +use serde::{Deserialize, Serialize}; +use std::convert::TryInto; +use std::str::FromStr; + +/// Pagination control for those RPC client methods supporting pagination. +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub enum Paging { + /// No explicit options set - use whatever the endpoint's defaults are. + Default, + /// Try to automatically fetch all pages' data. + All, + /// Fetch a specific page's data. + Specific { + /// The number of the page to fetch. + page_number: PageNumber, + /// The number of items to fetch per page. + per_page: PerPage, + }, +} + +/// A page number in paginated RPC responses. +#[derive(Debug, Clone, Copy, Serialize, Deserialize, Eq, PartialEq, PartialOrd, Ord)] +pub struct PageNumber(usize); + +impl FromStr for PageNumber { + type Err = Error; + + fn from_str(s: &str) -> Result { + let raw = i64::from_str(s).map_err(|e| Error::client_internal_error(e.to_string()))?; + let raw_usize: usize = raw.try_into().map_err(|_| { + Error::client_internal_error(format!("page number out of range: {}", raw)) + })?; + Ok(raw_usize.into()) + } +} + +impl std::fmt::Display for PageNumber { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl From for PageNumber { + fn from(value: usize) -> Self { + Self(value) + } +} + +/// The number of items to return per page, for paginated RPC responses. +#[derive(Debug, Clone, Copy, Serialize, Deserialize, Eq, PartialEq, PartialOrd, Ord)] +pub struct PerPage(u8); + +impl FromStr for PerPage { + type Err = Error; + + fn from_str(s: &str) -> Result { + let raw = i64::from_str(s).map_err(|e| Error::client_internal_error(e.to_string()))?; + let raw_u8: u8 = raw.try_into().map_err(|_| { + Error::client_internal_error(format!("items per page out of range: {}", raw)) + })?; + Ok(raw_u8.into()) + } +} + +impl std::fmt::Display for PerPage { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl From for PerPage { + fn from(value: u8) -> Self { + Self(value) + } +} diff --git a/rpc/tests/support/validators.json b/rpc/tests/support/validators.json index 056ddd305..1e0ef5970 100644 --- a/rpc/tests/support/validators.json +++ b/rpc/tests/support/validators.json @@ -589,6 +589,7 @@ "voting_power": "2500", "proposer_priority": "102500" } - ] + ], + "total": "65" } }