diff --git a/CHANGES.md b/CHANGES.md index f7a77f6b3..9aca025d8 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -17,10 +17,14 @@ - Correctly handle blocks marked `Trusted` in accordance with the specification ([#407]) - Treat `Trusted` status as a special case of `Verified` as per the spec ([#419]) - Add integration test ([#431]) +- Rework light-node CLI to use `Supervisor` / `Handle` ([#430]) +- Add `latest_status` to the supervisor `Handle` ([#449]) +- Add JSONRPC endpoints to query the light-node ([#363], [#449]) [#302]: https://github.com/informalsystems/tendermint-rs/pull/302 [#336]: https://github.com/informalsystems/tendermint-rs/pull/336 [#360]: https://github.com/informalsystems/tendermint-rs/pull/360 +[#363]: https://github.com/informalsystems/tendermint-rs/pull/363 [#364]: https://github.com/informalsystems/tendermint-rs/pull/364 [#365]: https://github.com/informalsystems/tendermint-rs/pull/365 [#371]: https://github.com/informalsystems/tendermint-rs/pull/371 @@ -32,7 +36,9 @@ [#403]: https://github.com/informalsystems/tendermint-rs/pull/403 [#407]: https://github.com/informalsystems/tendermint-rs/pull/407 [#419]: https://github.com/informalsystems/tendermint-rs/pull/419 +[#430]: https://github.com/informalsystems/tendermint-rs/pull/430 [#431]: https://github.com/informalsystems/tendermint-rs/pull/431 +[#449]: https://github.com/informalsystems/tendermint-rs/pull/449 [ADR-007]: https://github.com/informalsystems/tendermint-rs/blob/master/docs/architecture/adr-007-light-client-supervisor-ergonomics.md diff --git a/light-client/src/light_client.rs b/light-client/src/light_client.rs index 235a8517c..8a1e6f4f9 100644 --- a/light-client/src/light_client.rs +++ b/light-client/src/light_client.rs @@ -88,7 +88,7 @@ impl LightClient { /// Attempt to update the light client to the highest block of the primary node. /// - /// Note: This functin delegates the actual work to `verify_to_target`. + /// Note: This function delegates the actual work to `verify_to_target`. pub fn verify_to_highest(&mut self, state: &mut State) -> Result { let target_block = match self.io.fetch_light_block(self.peer, AtHeight::Highest) { Ok(last_block) => last_block, diff --git a/light-client/src/peer_list.rs b/light-client/src/peer_list.rs index 77b7776c3..10f026379 100644 --- a/light-client/src/peer_list.rs +++ b/light-client/src/peer_list.rs @@ -159,7 +159,7 @@ pub struct PeerListBuilder { } // This instance must be derived manually because the automatically -// derived instancce constrains T to be Default. +// derived instance constrains T to be Default. // See https://github.com/rust-lang/rust/issues/26925 impl Default for PeerListBuilder { fn default() -> Self { diff --git a/light-client/src/store/memory.rs b/light-client/src/store/memory.rs index e5a4fc7d6..c91c34cd6 100644 --- a/light-client/src/store/memory.rs +++ b/light-client/src/store/memory.rs @@ -65,8 +65,8 @@ impl LightStore for MemoryStore { fn latest(&self, status: Status) -> Option { self.store .iter() - .rev() - .find(|(_, e)| e.status == status) + .filter(|(_, e)| e.status == status) + .max_by_key(|(&height, _)| height) .map(|(_, e)| e.light_block.clone()) } diff --git a/light-client/src/store/sled.rs b/light-client/src/store/sled.rs index 456b9f2b0..5e5869b3c 100644 --- a/light-client/src/store/sled.rs +++ b/light-client/src/store/sled.rs @@ -72,7 +72,9 @@ impl LightStore for SledStore { } fn latest(&self, status: Status) -> Option { - self.db(status).iter(&self.db).next_back() + self.db(status) + .iter(&self.db) + .max_by(|first, second| first.height().cmp(&second.height())) } fn all(&self, status: Status) -> Box> { diff --git a/light-client/src/supervisor.rs b/light-client/src/supervisor.rs index 3f0cffdb0..6100b8528 100644 --- a/light-client/src/supervisor.rs +++ b/light-client/src/supervisor.rs @@ -9,7 +9,8 @@ use crate::fork_detector::{Fork, ForkDetection, ForkDetector}; use crate::light_client::LightClient; use crate::peer_list::PeerList; use crate::state::State; -use crate::types::{Height, LightBlock, PeerId, Status}; +use crate::types::{Height, LatestStatus, LightBlock, PeerId, Status}; +use tendermint::lite::{Header, ValidatorSet}; pub trait Handle { /// Get latest trusted block from the [`Supervisor`]. @@ -17,6 +18,10 @@ pub trait Handle { todo!() } + fn latest_status(&self) -> Result { + todo!() + } + /// Verify to the highest block. fn verify_to_highest(&self) -> Result { todo!() @@ -45,6 +50,8 @@ enum HandleInput { VerifyToTarget(Height, channel::Sender>), /// Get the latest trusted block. LatestTrusted(channel::Sender>), + /// Get the current status of the LightClient + GetStatus(channel::Sender), } /// A light client `Instance` packages a `LightClient` together with its `State`. @@ -166,6 +173,25 @@ impl Supervisor { self.verify(None) } + /// Return latest trusted status summary. + fn latest_status(&mut self) -> LatestStatus { + let latest_trusted = self.peers.primary().latest_trusted(); + let mut connected_nodes: Vec = Vec::new(); + connected_nodes.push(self.peers.primary_id()); + connected_nodes.append(&mut self.peers.witnesses_ids().iter().copied().collect()); + + match latest_trusted { + Some(trusted) => LatestStatus::new( + Some(trusted.signed_header.header.height()), + Some(trusted.signed_header.header.hash()), + Some(trusted.next_validators.hash()), + connected_nodes, + ), + // only return connected nodes to see what is going on: + None => LatestStatus::new(None, None, None, connected_nodes), + } + } + /// Verify to the block at the given height. pub fn verify_to_target(&mut self, height: Height) -> Result { self.verify(Some(height)) @@ -322,6 +348,10 @@ impl Supervisor { let outcome = self.verify_to_highest(); sender.send(outcome).map_err(ErrorKind::from)?; } + HandleInput::GetStatus(sender) => { + let outcome = self.latest_status(); + sender.send(outcome).map_err(ErrorKind::from)?; + } } } } @@ -363,6 +393,14 @@ impl Handle for SupervisorHandle { Ok(receiver.recv().map_err(ErrorKind::from)?) } + fn latest_status(&self) -> Result { + let (sender, receiver) = channel::bounded::(1); + self.sender + .send(HandleInput::GetStatus(sender)) + .map_err(ErrorKind::from)?; + Ok(receiver.recv().map_err(ErrorKind::from)?) + } + fn verify_to_highest(&self) -> Result { self.verify(HandleInput::VerifyToHighest) } diff --git a/light-client/src/types.rs b/light-client/src/types.rs index 5794ec9c2..e6c1c56af 100644 --- a/light-client/src/types.rs +++ b/light-client/src/types.rs @@ -116,6 +116,38 @@ impl LightBlock { } } +/// Contains the local status information, like the latest height, latest block and valset hashes, +/// list of of connected full nodes (primary and witnesses). +#[derive(Clone, Debug, Display, PartialEq, Serialize, Deserialize)] +#[display(fmt = "{:?}", self)] +pub struct LatestStatus { + /// The latest height we are trusting. + pub height: Option, + /// The latest block hash we are trusting. + pub block_hash: Option, + /// The latest validator set we are trusting. + /// Note that this potentially did not yet sign a header yet. + pub valset_hash: Option, + /// The list of fullnodes we are connected to, primary and witnesses. + pub connected_nodes: Vec, +} + +impl LatestStatus { + pub fn new( + height: Option, + block_hash: Option, + valset_hash: Option, + connected_nodes: Vec, + ) -> Self { + LatestStatus { + height, + block_hash, + valset_hash, + connected_nodes, + } + } +} + #[cfg(test)] mod tests { diff --git a/light-node/README.md b/light-node/README.md index 1097df48c..07f7a5ab8 100644 --- a/light-node/README.md +++ b/light-node/README.md @@ -106,8 +106,42 @@ $ curl localhost:8888 -X POST -H 'Content-Type: application/json' \ -d '{"jsonrpc": "2.0", "method": "state", "id": 1}' | jq ``` +Or you can query a shorter summary via the `/status` endpoint: +``` +$ curl localhost:8888 -X POST -H 'Content-Type: application/json'\ 15:58:52 + -d '{"jsonrpc": "2.0", "method": "status", "id": 1}' | jq + +``` + +
+ Click here to see an example for expected output for the status endpoint: + +``` +$ curl localhost:8888 -X POST -H 'Content-Type: application/json' \ + -d '{"jsonrpc": "2.0", "method": "status", "id": 1}' | jq + % Total % Received % Xferd Average Speed Time Time Time Current + Dload Upload Total Spent Left Speed +100 364 100 317 100 47 1843 273 --:--:-- --:--:-- --:--:-- 2104 +``` +```json +{ + "jsonrpc": "2.0", + "result": { + "block_hash": "ED745723430944215F65ED78AD7DF9ED0AA8A2A3B465BF421E0BAF66AA55AA08", + "connected_nodes": [ + "BADFADAD0BEFEEDC0C0ADEADBEEFC0FFEEFACADE", + "CEFEEDBADFADAD0C0CEEFACADE0ADEADBEEFC0FF" + ], + "height": 3850, + "valset_hash": "74F2AC2B6622504D08DD2509E28CE731985CFE4D133C9DB0CB85763EDCA95AA3" + }, + "id": 1 +} +``` +
+
- Click here to see an example for expected output: + Click here to see an example for expected output for the state endpoint: Command: ``` diff --git a/light-node/src/commands/start.rs b/light-node/src/commands/start.rs index bc85d5578..434bca908 100644 --- a/light-node/src/commands/start.rs +++ b/light-node/src/commands/start.rs @@ -35,7 +35,6 @@ use tendermint_light_client::store::sled::SledStore; use tendermint_light_client::store::LightStore; use tendermint_light_client::supervisor::Handle; use tendermint_light_client::supervisor::{Instance, Supervisor}; -use tendermint_light_client::types::Status; /// `start` subcommand /// @@ -70,7 +69,7 @@ impl Runnable for StartCmd { loop { match handle.verify_to_highest() { Ok(light_block) => { - status_info!("synced to block {}", light_block.height().to_string()); + status_info!("synced to block:", light_block.height().to_string()); } Err(err) => { status_err!("sync failed: {}", err); @@ -157,6 +156,7 @@ impl StartCmd { let laddr = app_config().rpc_config.listen_addr; // TODO(liamsi): figure out how to handle the potential error on run std::thread::spawn(move || rpc::run(server, &laddr.to_string())); + status_info!("started RPC server:", laddr.to_string()); } } @@ -189,7 +189,7 @@ impl StartCmd { Supervisor::new( peer_list, ProdForkDetector::default(), - ProdEvidenceReporter::new(peer_map.clone()), + ProdEvidenceReporter::new(peer_map), ) } } diff --git a/light-node/src/rpc.rs b/light-node/src/rpc.rs index f6b190fe2..5315c2429 100644 --- a/light-node/src/rpc.rs +++ b/light-node/src/rpc.rs @@ -9,7 +9,7 @@ pub use sealed::{Client, Rpc, Server}; /// Run the given [`Server`] on the given address and blocks until closed. /// -/// n.b. The underlying server has semantics to close on drop. Also does it not offer any way to +/// n.b. The underlying server has semantics to close on drop. Also does it does not offer any way to /// get the underlying Future to await, so we are left with this rather rudimentary way to control /// the lifecycle. Should we be interested in a more controlled way to close the server we can /// expose a handle in the future. @@ -38,6 +38,7 @@ mod sealed { use jsonrpc_derive::rpc; use tendermint_light_client::supervisor::Handle; + use tendermint_light_client::types::LatestStatus; use tendermint_light_client::types::LightBlock; #[rpc] @@ -45,6 +46,10 @@ mod sealed { /// Returns the latest trusted block. #[rpc(name = "state")] fn state(&self) -> FutureResult, Error>; + + /// Returns the latest status. + #[rpc(name = "status")] + fn status(&self) -> FutureResult; } pub use self::rpc_impl_Rpc::gen_client::Client; @@ -79,6 +84,17 @@ mod sealed { future::result(res) } + + fn status(&self) -> FutureResult { + let res = self.handle.latest_status().map_err(|e| { + let mut err = Error::internal_error(); + err.message = e.to_string(); + err.data = serde_json::to_value(e.kind()).ok(); + err + }); + + future::result(res) + } } } @@ -92,6 +108,7 @@ mod test { use tendermint_light_client::errors::Error; use tendermint_light_client::supervisor::Handle; + use tendermint_light_client::types::LatestStatus; use tendermint_light_client::types::LightBlock; use super::{Client, Rpc as _, Server}; @@ -111,6 +128,21 @@ mod test { assert_eq!(have, want); } + #[tokio::test] + async fn status() { + let server = Server::new(MockHandle {}); + let fut = { + let mut io = IoHandler::new(); + io.extend_with(server.to_delegate()); + let (client, server) = local::connect::(io); + client.status().join(server) + }; + let (have, _) = fut.compat().await.unwrap(); + let want = serde_json::from_str(STATUS_JSON).unwrap(); + + assert_eq!(have, want); + } + struct MockHandle; impl Handle for MockHandle { @@ -119,6 +151,11 @@ mod test { Ok(Some(block)) } + fn latest_status(&self) -> Result { + let status: LatestStatus = serde_json::from_str(STATUS_JSON).unwrap(); + + Ok(status) + } } const LIGHTBLOCK_JSON: &str = r#" @@ -239,4 +276,14 @@ mod test { "provider": "9D61B19DEFFD5A60BA844AF492EC2CC44449C569" } "#; + const STATUS_JSON: &str = r#" +{ + "block_hash": "5A55D7AF2DF9AE4BF4B46FDABBBAD1B66D37B5E044A4843AB0FB0EBEC3E0422C", + "connected_nodes": [ + "BADFADAD0BEFEEDC0C0ADEADBEEFC0FFEEFACADE", + "CEFEEDBADFADAD0C0CEEFACADE0ADEADBEEFC0FF" + ], + "height": 1565, + "valset_hash": "74F2AC2B6622504D08DD2509E28CE731985CFE4D133C9DB0CB85763EDCA95AA3" +}"#; }