Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

light-client: add missing RPC endpoint and extend handler/supervisor #449

Merged
merged 15 commits into from
Jul 16, 2020
Merged
6 changes: 6 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
2 changes: 1 addition & 1 deletion light-client/src/light_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<LightBlock, Error> {
let target_block = match self.io.fetch_light_block(self.peer, AtHeight::Highest) {
Ok(last_block) => last_block,
Expand Down
2 changes: 1 addition & 1 deletion light-client/src/peer_list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ pub struct PeerListBuilder<T> {
}

// 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<T> Default for PeerListBuilder<T> {
fn default() -> Self {
Expand Down
4 changes: 2 additions & 2 deletions light-client/src/store/memory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ impl LightStore for MemoryStore {
fn latest(&self, status: Status) -> Option<LightBlock> {
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())
}

Expand Down
4 changes: 3 additions & 1 deletion light-client/src/store/sled.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,9 @@ impl LightStore for SledStore {
}

fn latest(&self, status: Status) -> Option<LightBlock> {
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<dyn Iterator<Item = LightBlock>> {
Expand Down
40 changes: 39 additions & 1 deletion light-client/src/supervisor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,19 @@ 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`].
fn latest_trusted(&self) -> Result<Option<LightBlock>, Error> {
todo!()
}

fn latest_status(&self) -> Result<Option<LatestStatus>, Error> {
todo!()
}

/// Verify to the highest block.
fn verify_to_highest(&self) -> Result<LightBlock, Error> {
todo!()
Expand Down Expand Up @@ -45,6 +50,8 @@ enum HandleInput {
VerifyToTarget(Height, channel::Sender<Result<LightBlock, Error>>),
/// Get the latest trusted block.
LatestTrusted(channel::Sender<Option<LightBlock>>),
/// Get the current status of the LightClient
GetStatus(channel::Sender<Option<LatestStatus>>),
}

/// A light client `Instance` packages a `LightClient` together with its `State`.
Expand Down Expand Up @@ -166,6 +173,25 @@ impl Supervisor {
self.verify(None)
}

/// Return latest trusted status summary.
fn latest_status(&mut self) -> Option<LatestStatus> {
let latest_trusted = self.peers.primary().latest_trusted();
match latest_trusted {
Some(trusted) => {
let mut connected_nodes: Vec<PeerId> = Vec::new();
connected_nodes.push(self.peers.primary_id());
connected_nodes.append(&mut self.peers.witnesses_ids().iter().copied().collect());
Some(LatestStatus::new(
trusted.signed_header.header.height(),
trusted.signed_header.header.hash(),
trusted.next_validators.hash(),
connected_nodes,
))
}
None => None,
}
}

/// Verify to the block at the given height.
pub fn verify_to_target(&mut self, height: Height) -> Result<LightBlock, Error> {
self.verify(Some(height))
Expand Down Expand Up @@ -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)?;
}
}
}
}
Expand Down Expand Up @@ -363,6 +393,14 @@ impl Handle for SupervisorHandle {
Ok(receiver.recv().map_err(ErrorKind::from)?)
}

fn latest_status(&self) -> Result<Option<LatestStatus>, Error> {
let (sender, receiver) = channel::bounded::<Option<LatestStatus>>(1);
self.sender
.send(HandleInput::GetStatus(sender))
.map_err(ErrorKind::from)?;
Ok(receiver.recv().map_err(ErrorKind::from)?)
}

fn verify_to_highest(&self) -> Result<LightBlock, Error> {
self.verify(HandleInput::VerifyToHighest)
}
Expand Down
32 changes: 32 additions & 0 deletions light-client/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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: Height,
/// The latest block hash we are trusting.
pub block_hash: Hash,
/// The latest validator set we are trusting.
/// Note that this potentially did not yet sign a header yet.
pub valset_hash: Hash,
/// The list of fullnodes we are connected to, primary and witnesses.
pub connected_nodes: Vec<PeerId>,
}

impl LatestStatus {
pub fn new(
height: u64,
block_hash: Hash,
valset_hash: Hash,
connected_nodes: Vec<PeerId>,
) -> Self {
LatestStatus {
height,
block_hash,
valset_hash,
connected_nodes,
}
}
}

#[cfg(test)]
mod tests {

Expand Down
35 changes: 34 additions & 1 deletion light-node/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,41 @@ $ 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

```

<details>
<summary><b>Click here</b> to see an example for expected output for the status endpoint:</summary>

```
$ 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
}
```

<details>
<summary><b>Click here</b> to see an example for expected output:</summary>
<summary><b>Click here</b> to see an example for expected output for the state endpoint:</summary>

Command:
```
Expand Down
6 changes: 3 additions & 3 deletions light-node/src/commands/start.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
///
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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());
}
}

Expand Down Expand Up @@ -189,7 +189,7 @@ impl StartCmd {
Supervisor::new(
peer_list,
ProdForkDetector::default(),
ProdEvidenceReporter::new(peer_map.clone()),
ProdEvidenceReporter::new(peer_map),
)
}
}
49 changes: 48 additions & 1 deletion light-node/src/rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -38,13 +38,18 @@ 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]
pub trait Rpc {
/// Returns the latest trusted block.
#[rpc(name = "state")]
fn state(&self) -> FutureResult<Option<LightBlock>, Error>;

/// Returns the latest status.
#[rpc(name = "status")]
fn status(&self) -> FutureResult<Option<LatestStatus>, Error>;
liamsi marked this conversation as resolved.
Show resolved Hide resolved
}

pub use self::rpc_impl_Rpc::gen_client::Client;
Expand Down Expand Up @@ -79,6 +84,17 @@ mod sealed {

future::result(res)
}

fn status(&self) -> FutureResult<Option<LatestStatus>, Error> {
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)
}
}
}

Expand All @@ -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};
Expand All @@ -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::<Client, _, _>(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 {
Expand All @@ -119,6 +151,11 @@ mod test {

Ok(Some(block))
}
fn latest_status(&self) -> Result<Option<LatestStatus>, Error> {
let status: LatestStatus = serde_json::from_str(STATUS_JSON).unwrap();

Ok(Some(status))
}
}

const LIGHTBLOCK_JSON: &str = r#"
Expand Down Expand Up @@ -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"
}"#;
}