diff --git a/.changelog/unreleased/features/832-header.md b/.changelog/unreleased/features/832-header.md new file mode 100644 index 000000000..1d464f653 --- /dev/null +++ b/.changelog/unreleased/features/832-header.md @@ -0,0 +1,3 @@ +- `[tendermint-rpc]` Add support for the `/header` RPC endpoint. See + <https://docs.tendermint.com/v0.35/rpc/#/Info/header> for details + ([#832](https://github.com/informalsystems/tendermint-rs/issues/832)). \ No newline at end of file diff --git a/.gitignore b/.gitignore index a066462b4..311d0d335 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,9 @@ # will have compiled files and executables target/ +# IDE specific files +.idea/ + # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html Cargo.lock diff --git a/config/src/config.rs b/config/src/config.rs index 5f254046d..e7a7e25ce 100644 --- a/config/src/config.rs +++ b/config/src/config.rs @@ -176,7 +176,7 @@ impl LogLevel { { self.components .get(key.as_ref()) - .or_else(|| self.global.as_ref()) + .or(self.global.as_ref()) .map(AsRef::as_ref) } diff --git a/light-client/src/light_client.rs b/light-client/src/light_client.rs index e90168c55..eab281b0f 100644 --- a/light-client/src/light_client.rs +++ b/light-client/src/light_client.rs @@ -2,6 +2,9 @@ //! //! [1]: https://github.com/informalsystems/tendermint-rs/blob/master/docs/spec/lightclient/verification/verification.md +// FIXME(hu55a1n1): Remove below line once clippy errors in `contracts::post` derive macro are fixed +#![allow(clippy::nonminimal_bool)] + use contracts::*; use core::fmt; diff --git a/light-client/src/peer_list.rs b/light-client/src/peer_list.rs index d1f3d58f4..4296f6345 100644 --- a/light-client/src/peer_list.rs +++ b/light-client/src/peer_list.rs @@ -1,5 +1,8 @@ //! Provides a peer list for use within the `Supervisor` +// FIXME(hu55a1n1): Remove below line once clippy errors in `contracts::post` derive macro are fixed +#![allow(clippy::nonminimal_bool)] + use contracts::{post, pre}; use std::collections::{BTreeSet, HashMap}; diff --git a/p2p/src/error.rs b/p2p/src/error.rs index a0530c68d..69f9d55ba 100644 --- a/p2p/src/error.rs +++ b/p2p/src/error.rs @@ -1,5 +1,8 @@ //! Error types +// FIXME(hu55a1n1): Remove below line once flex-error solves this clippy error +#![allow(clippy::use_self)] + use flex_error::{define_error, DisplayOnly}; use prost::DecodeError; use signature::Error as SignatureError; diff --git a/rpc/src/client.rs b/rpc/src/client.rs index a1ce9ad3a..0b91963fe 100644 --- a/rpc/src/client.rs +++ b/rpc/src/client.rs @@ -59,6 +59,14 @@ pub trait Client { .response) } + /// `/header`: get header at a given height. + async fn header<H>(&self, height: H) -> Result<header::Response, Error> + where + H: Into<Height> + Send, + { + self.perform(header::Request::new(height.into())).await + } + /// `/block`: get block at a given height. async fn block<H>(&self, height: H) -> Result<block::Response, Error> where diff --git a/rpc/src/client/bin/main.rs b/rpc/src/client/bin/main.rs index eab7664f0..935dce752 100644 --- a/rpc/src/client/bin/main.rs +++ b/rpc/src/client/bin/main.rs @@ -77,6 +77,8 @@ enum ClientRequest { #[structopt(long)] prove: bool, }, + /// Get a header at a given height. + Header { height: u32 }, /// Get a block at a given height. Block { height: u32 }, /// Get block headers between two heights (min <= height <= max). @@ -314,6 +316,9 @@ where .await?, ) .map_err(Error::serde)?, + ClientRequest::Header { height } => { + serde_json::to_string_pretty(&client.header(height).await?).map_err(Error::serde)? + } ClientRequest::Block { height } => { serde_json::to_string_pretty(&client.block(height).await?).map_err(Error::serde)? } diff --git a/rpc/src/endpoint.rs b/rpc/src/endpoint.rs index 8720be10b..8b5d987d6 100644 --- a/rpc/src/endpoint.rs +++ b/rpc/src/endpoint.rs @@ -12,6 +12,7 @@ pub mod consensus_params; pub mod consensus_state; pub mod evidence; pub mod genesis; +pub mod header; pub mod health; pub mod net_info; pub mod status; diff --git a/rpc/src/endpoint/header.rs b/rpc/src/endpoint/header.rs new file mode 100644 index 000000000..94e8416b0 --- /dev/null +++ b/rpc/src/endpoint/header.rs @@ -0,0 +1,43 @@ +//! `/header` endpoint JSON-RPC wrapper + +use serde::{Deserialize, Serialize}; + +use tendermint::block::{Header, Height}; + +/// Get information about a specific header +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +pub struct Request { + /// Height of the header to request. + /// + /// If no height is provided, it will fetch results for the latest header. + pub height: Option<Height>, +} + +impl Request { + /// Create a new request for information about a particular header + pub fn new(height: Height) -> Self { + Self { + height: Some(height), + } + } +} + +impl crate::Request for Request { + type Response = Response; + + fn method(&self) -> crate::Method { + crate::Method::Header + } +} + +impl crate::SimpleRequest for Request {} + +/// Header responses +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(transparent)] +pub struct Response { + /// Header + pub header: Header, +} + +impl crate::Response for Response {} diff --git a/rpc/src/method.rs b/rpc/src/method.rs index f2c821202..267086011 100644 --- a/rpc/src/method.rs +++ b/rpc/src/method.rs @@ -19,6 +19,9 @@ pub enum Method { /// Get ABCI query AbciQuery, + /// Get block header + Header, + /// Get block info Block, @@ -86,6 +89,7 @@ impl Method { match self { Method::AbciInfo => "abci_info", Method::AbciQuery => "abci_query", + Method::Header => "header", Method::Block => "block", Method::BlockResults => "block_results", Method::BlockSearch => "block_search", @@ -117,6 +121,7 @@ impl FromStr for Method { Ok(match s { "abci_info" => Method::AbciInfo, "abci_query" => Method::AbciQuery, + "header" => Method::Header, "block" => Method::Block, "block_results" => Method::BlockResults, "block_search" => Method::BlockSearch, diff --git a/rpc/tests/kvstore_fixtures.rs b/rpc/tests/kvstore_fixtures.rs index 1fb40251d..c8858a0e7 100644 --- a/rpc/tests/kvstore_fixtures.rs +++ b/rpc/tests/kvstore_fixtures.rs @@ -72,6 +72,12 @@ fn outgoing_fixtures() { assert!(wrapped.params().height.is_none()); assert!(!wrapped.params().prove); } + "header_at_height_1" => { + let wrapped = + serde_json::from_str::<RequestWrapper<endpoint::header::Request>>(&content) + .unwrap(); + assert_eq!(wrapped.params().height.unwrap().value(), 1); + } "block_at_height_0" => { let wrapped = serde_json::from_str::<RequestWrapper<endpoint::block::Request>>(&content) @@ -346,6 +352,37 @@ fn incoming_fixtures() { assert!(result.response.proof.is_none()); assert!(result.response.value.is_empty()); } + "header_at_height_1" => { + let result = endpoint::header::Response::from_string(content).unwrap(); + assert!(result.header.app_hash.value().is_empty()); + assert_eq!(result.header.chain_id.as_str(), CHAIN_ID); + assert!(!result.header.consensus_hash.is_empty()); + assert_eq!(result.header.data_hash, empty_merkle_root_hash); + assert_eq!(result.header.evidence_hash, empty_merkle_root_hash); + assert_eq!(result.header.height.value(), 1); + assert!(result.header.last_block_id.is_none()); + assert_eq!(result.header.last_commit_hash, empty_merkle_root_hash); + assert_eq!(result.header.last_results_hash, empty_merkle_root_hash); + assert!(!result.header.next_validators_hash.is_empty()); + assert_ne!( + result.header.proposer_address.as_bytes(), + [0u8; tendermint::account::LENGTH] + ); + assert!( + result + .header + .time + .duration_since(informal_epoch) + .unwrap() + .as_secs() + > 0 + ); + assert!(!result.header.validators_hash.is_empty()); + assert_eq!( + result.header.version, + tendermint::block::header::Version { block: 11, app: 1 } + ); + } "block_at_height_0" => { let res = endpoint::block::Response::from_string(&content); diff --git a/rpc/tests/kvstore_fixtures/incoming/header_at_height_1.json b/rpc/tests/kvstore_fixtures/incoming/header_at_height_1.json new file mode 100644 index 000000000..b9076f92f --- /dev/null +++ b/rpc/tests/kvstore_fixtures/incoming/header_at_height_1.json @@ -0,0 +1,29 @@ +{ + "id": "0166b641-4967-4b3c-a36a-73ea4e5a737a", + "jsonrpc": "2.0", + "result": { + "app_hash": "", + "chain_id": "dockerchain", + "consensus_hash": "048091BC7DDC283F77BFBF91D73C44DA58C3DF8A9CBC867405D8B7F3DAADA22F", + "data_hash": "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855", + "evidence_hash": "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855", + "height": "1", + "last_block_id": { + "hash": "", + "parts": { + "hash": "", + "total": 0 + } + }, + "last_commit_hash": "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855", + "last_results_hash": "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855", + "next_validators_hash": "ADFA3B40824D69EAD7828B9A78D16D80DFA93499D1DB0EC362916AE61182A64D", + "proposer_address": "ABA577531E6D6F4119E7E1E0EE1909B908A8346D", + "time": "2021-07-16T12:16:29.232984022Z", + "validators_hash": "ADFA3B40824D69EAD7828B9A78D16D80DFA93499D1DB0EC362916AE61182A64D", + "version": { + "app": "1", + "block": "11" + } + } +} \ No newline at end of file diff --git a/rpc/tests/kvstore_fixtures/outgoing/header_at_height_1.json b/rpc/tests/kvstore_fixtures/outgoing/header_at_height_1.json new file mode 100644 index 000000000..888325f9b --- /dev/null +++ b/rpc/tests/kvstore_fixtures/outgoing/header_at_height_1.json @@ -0,0 +1,8 @@ +{ + "id": "0166b641-4967-4b3c-a36a-73ea4e5a737a", + "jsonrpc": "2.0", + "method": "header", + "params": { + "height": "1" + } +} \ No newline at end of file diff --git a/tendermint/src/abci/transaction.rs b/tendermint/src/abci/transaction.rs index 546a1026d..3a93628ac 100644 --- a/tendermint/src/abci/transaction.rs +++ b/tendermint/src/abci/transaction.rs @@ -118,7 +118,7 @@ impl Data { impl AsRef<[Transaction]> for Data { fn as_ref(&self) -> &[Transaction] { - self.txs.as_deref().unwrap_or_else(|| &[]) + self.txs.as_deref().unwrap_or(&[]) } } diff --git a/tendermint/src/evidence.rs b/tendermint/src/evidence.rs index dac677981..77bf09460 100644 --- a/tendermint/src/evidence.rs +++ b/tendermint/src/evidence.rs @@ -205,7 +205,7 @@ impl Data { impl AsRef<[Evidence]> for Data { fn as_ref(&self) -> &[Evidence] { - self.evidence.as_deref().unwrap_or_else(|| &[]) + self.evidence.as_deref().unwrap_or(&[]) } } diff --git a/tools/kvstore-test/tests/tendermint.rs b/tools/kvstore-test/tests/tendermint.rs index 38ddca661..75dd2dd18 100644 --- a/tools/kvstore-test/tests/tendermint.rs +++ b/tools/kvstore-test/tests/tendermint.rs @@ -92,6 +92,17 @@ mod rpc { assert_eq!(abci_query.codespace, String::new()); } + /// `/header` endpoint + #[tokio::test] + async fn header() { + let height = 1u64; + let res = localhost_http_client() + .header(Height::try_from(height).unwrap()) + .await + .unwrap(); + assert_eq!(res.header.height.value(), height); + } + /// `/block` endpoint #[tokio::test] async fn block() { diff --git a/tools/rpc-probe/src/common.rs b/tools/rpc-probe/src/common.rs index 1557485f1..61517c4df 100644 --- a/tools/rpc-probe/src/common.rs +++ b/tools/rpc-probe/src/common.rs @@ -20,6 +20,16 @@ pub fn abci_query(key: &str) -> PlannedInteraction { .into() } +pub fn header(height: u64) -> PlannedInteraction { + Request::new( + "header", + json!({ + "height": format!("{}", height), + }), + ) + .into() +} + pub fn block(height: u64) -> PlannedInteraction { Request::new( "block", diff --git a/tools/rpc-probe/src/kvstore.rs b/tools/rpc-probe/src/kvstore.rs index fb0f6aec6..5d4dca67e 100644 --- a/tools/rpc-probe/src/kvstore.rs +++ b/tools/rpc-probe/src/kvstore.rs @@ -16,6 +16,7 @@ pub fn quick_probe_plan(output_path: &Path, request_wait: Duration) -> Result<Pl in_series(vec![ abci_info(), abci_query("non_existent_key").with_name("abci_query_with_non_existent_key"), + header(1).with_name("header_at_height_1"), block(0).with_name("block_at_height_0").expect_error(), block(1).with_name("block_at_height_1"), block(10)