diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index e5f2e5d0f..3cb93a830 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -157,3 +157,17 @@ jobs: file: ${{ steps.coverage.outputs.report }} yml: ./codecov.yml fail_ci_if_error: true + + light-client-wasm: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + override: true + target: wasm32-unknown-unknown + - name: Build Tendermint for WASM + run: cd tendermint && cargo build --target wasm32-unknown-unknown --release --no-default-features + - name: Build Light Client for WASM + run: cd light-client && cargo build --target wasm32-unknown-unknown --release --no-default-features diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b1d860f5..e673a1514 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,12 @@ ## Unreleased -- Add spec for the light client attack evidence handling ([#544]) -- return rfc6962 hash for empty merkle tree ([#498]) +- Add spec for the light client attack evidence handling ([#526]) +- Return RFC6962 hash for empty merkle tree ([#498]) +- The `tendermint`, `tendermint-rpc`, and `tendermint-light-client` crates now compile to WASM on the `wasm32-unknown-unknown` and `wasm32-wasi` targets ([#463]) -[#544]: https://github.com/informalsystems/tendermint-rs/pull/544 +[#526]: https://github.com/informalsystems/tendermint-rs/issues/526 [#498]: https://github.com/informalsystems/tendermint-rs/issues/498 +[#463]: https://github.com/informalsystems/tendermint-rs/issues/463 ## v0.16.0 diff --git a/light-client/Cargo.toml b/light-client/Cargo.toml index 1160d8a37..2deec708d 100644 --- a/light-client/Cargo.toml +++ b/light-client/Cargo.toml @@ -18,9 +18,17 @@ description = """ Implementation of the Tendermint Light Client Verification Protocol. """ +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +default = ["rpc-client"] +rpc-client = ["tendermint-rpc/client"] +secp256k1 = ["tendermint/secp256k1", "tendermint-rpc/secp256k1"] + [dependencies] tendermint = { version = "0.16.0", path = "../tendermint" } -tendermint-rpc = { version = "0.16.0", path = "../rpc", features = ["client"] } +tendermint-rpc = { version = "0.16.0", path = "../rpc", default-features = false } anomaly = { version = "0.2.0", features = ["serializer"] } contracts = "0.4.0" @@ -40,6 +48,3 @@ tokio = "0.2.20" serde_json = "1.0.51" gumdrop = "0.8.0" tendermint-testgen = { path = "../testgen"} - -[features] -secp256k1 = ["tendermint/secp256k1", "tendermint-rpc/secp256k1"] diff --git a/light-client/src/components/io.rs b/light-client/src/components/io.rs index b1a78639e..1ef28994d 100644 --- a/light-client/src/components/io.rs +++ b/light-client/src/components/io.rs @@ -1,22 +1,12 @@ //! Provides an interface and a default implementation of the `Io` component -use std::collections::HashMap; -use std::time::Duration; - -use contracts::{contract_trait, post, pre}; +use contracts::{contract_trait, post}; use serde::{Deserialize, Serialize}; use thiserror::Error; -use tendermint::{ - block::signed_header::SignedHeader as TMSignedHeader, validator::Set as TMValidatorSet, -}; - use tendermint_rpc as rpc; -use crate::{ - bail, - types::{Height, LightBlock, PeerId}, -}; +use crate::types::{Height, LightBlock, PeerId}; /// Type for selecting either a specific height or the latest one pub enum AtHeight { @@ -81,113 +71,131 @@ where } } -/// Production implementation of the Io component, which fetches -/// light blocks from full nodes via RPC. -#[derive(Clone, Debug)] -pub struct ProdIo { - peer_map: HashMap, - timeout: Option, -} +#[cfg(feature = "rpc-client")] +pub use self::prod::ProdIo; -#[contract_trait] -impl Io for ProdIo { - fn fetch_light_block(&self, peer: PeerId, height: AtHeight) -> Result { - let signed_header = self.fetch_signed_header(peer, height)?; - let height = signed_header.header.height; +#[cfg(feature = "rpc-client")] +mod prod { + use super::*; - let validator_set = self.fetch_validator_set(peer, height.into())?; - let next_validator_set = self.fetch_validator_set(peer, height.increment().into())?; + use std::collections::HashMap; + use std::time::Duration; - let light_block = LightBlock::new(signed_header, validator_set, next_validator_set, peer); + use crate::bail; + use contracts::{contract_trait, pre}; + use tendermint::{ + block::signed_header::SignedHeader as TMSignedHeader, validator::Set as TMValidatorSet, + }; - Ok(light_block) - } -} - -impl ProdIo { - /// Constructs a new ProdIo component. - /// - /// A peer map which maps peer IDS to their network address must be supplied. - pub fn new( + /// Production implementation of the Io component, which fetches + /// light blocks from full nodes via RPC. + #[derive(Clone, Debug)] + pub struct ProdIo { peer_map: HashMap, timeout: Option, - ) -> Self { - Self { peer_map, timeout } } - #[pre(self.peer_map.contains_key(&peer))] - fn fetch_signed_header( - &self, - peer: PeerId, - height: AtHeight, - ) -> Result { - let rpc_client = self.rpc_client_for(peer); - - let res = block_on( - async { - match height { - AtHeight::Highest => rpc_client.latest_commit().await, - AtHeight::At(height) => rpc_client.commit(height).await, - } - }, - peer, - self.timeout, - )?; - - match res { - Ok(response) => Ok(response.signed_header), - Err(err) => Err(IoError::IoError(err)), + #[contract_trait] + impl Io for ProdIo { + fn fetch_light_block(&self, peer: PeerId, height: AtHeight) -> Result { + let signed_header = self.fetch_signed_header(peer, height)?; + let height = signed_header.header.height; + + let validator_set = self.fetch_validator_set(peer, height.into())?; + let next_validator_set = self.fetch_validator_set(peer, height.increment().into())?; + + let light_block = + LightBlock::new(signed_header, validator_set, next_validator_set, peer); + + Ok(light_block) } } - #[pre(self.peer_map.contains_key(&peer))] - fn fetch_validator_set( - &self, - peer: PeerId, - height: AtHeight, - ) -> Result { - let height = match height { - AtHeight::Highest => bail!(IoError::InvalidHeight( - "given height must be greater than 0".to_string() - )), - AtHeight::At(height) => height, - }; - - let res = block_on( - self.rpc_client_for(peer).validators(height), - peer, - self.timeout, - )?; - - match res { - Ok(response) => Ok(TMValidatorSet::new(response.validators)), - Err(err) => Err(IoError::IoError(err)), + impl ProdIo { + /// Constructs a new ProdIo component. + /// + /// A peer map which maps peer IDS to their network address must be supplied. + pub fn new( + peer_map: HashMap, + timeout: Option, + ) -> Self { + Self { peer_map, timeout } + } + + #[pre(self.peer_map.contains_key(&peer))] + fn fetch_signed_header( + &self, + peer: PeerId, + height: AtHeight, + ) -> Result { + let rpc_client = self.rpc_client_for(peer); + + let res = block_on( + async { + match height { + AtHeight::Highest => rpc_client.latest_commit().await, + AtHeight::At(height) => rpc_client.commit(height).await, + } + }, + peer, + self.timeout, + )?; + + match res { + Ok(response) => Ok(response.signed_header), + Err(err) => Err(IoError::IoError(err)), + } + } + + #[pre(self.peer_map.contains_key(&peer))] + fn fetch_validator_set( + &self, + peer: PeerId, + height: AtHeight, + ) -> Result { + let height = match height { + AtHeight::Highest => bail!(IoError::InvalidHeight( + "given height must be greater than 0".to_string() + )), + AtHeight::At(height) => height, + }; + + let res = block_on( + self.rpc_client_for(peer).validators(height), + peer, + self.timeout, + )?; + + match res { + Ok(response) => Ok(TMValidatorSet::new(response.validators)), + Err(err) => Err(IoError::IoError(err)), + } } - } - // FIXME: Cannot enable precondition because of "autoref lifetime" issue - // #[pre(self.peer_map.contains_key(&peer))] - fn rpc_client_for(&self, peer: PeerId) -> rpc::Client { - let peer_addr = self.peer_map.get(&peer).unwrap().to_owned(); - rpc::Client::new(peer_addr) + // FIXME: Cannot enable precondition because of "autoref lifetime" issue + // #[pre(self.peer_map.contains_key(&peer))] + fn rpc_client_for(&self, peer: PeerId) -> rpc::Client { + let peer_addr = self.peer_map.get(&peer).unwrap().to_owned(); + rpc::Client::new(peer_addr) + } } -} -fn block_on( - f: F, - peer: PeerId, - timeout: Option, -) -> Result { - let mut rt = tokio::runtime::Builder::new() - .basic_scheduler() - .enable_all() - .build() - .unwrap(); - - if let Some(timeout) = timeout { - rt.block_on(async { tokio::time::timeout(timeout, f).await }) - .map_err(|_| IoError::Timeout(peer)) - } else { - Ok(rt.block_on(f)) + fn block_on( + f: F, + peer: PeerId, + timeout: Option, + ) -> Result { + let mut rt = tokio::runtime::Builder::new() + .basic_scheduler() + .enable_all() + .build() + .unwrap(); + + if let Some(timeout) = timeout { + rt.block_on(async { tokio::time::timeout(timeout, f).await }) + .map_err(|_| IoError::Timeout(peer)) + } else { + Ok(rt.block_on(f)) + } } } diff --git a/light-client/src/evidence.rs b/light-client/src/evidence.rs index 260eb79be..33db6a01a 100644 --- a/light-client/src/evidence.rs +++ b/light-client/src/evidence.rs @@ -3,10 +3,8 @@ use crate::{components::io::IoError, types::PeerId}; use tendermint::abci::transaction::Hash; -use tendermint_rpc as rpc; -use contracts::{contract_trait, pre}; -use std::collections::HashMap; +use contracts::contract_trait; pub use tendermint::evidence::Evidence; @@ -18,47 +16,59 @@ pub trait EvidenceReporter: Send { fn report(&self, e: Evidence, peer: PeerId) -> Result; } -/// Production implementation of the EvidenceReporter component, which reports evidence to full -/// nodes via RPC. -#[derive(Clone, Debug)] -pub struct ProdEvidenceReporter { - peer_map: HashMap, -} +#[cfg(feature = "rpc-client")] +pub use self::prod::ProdEvidenceReporter; -#[contract_trait] -impl EvidenceReporter for ProdEvidenceReporter { - #[pre(self.peer_map.contains_key(&peer))] - fn report(&self, e: Evidence, peer: PeerId) -> Result { - let res = block_on(self.rpc_client_for(peer).broadcast_evidence(e)); +#[cfg(feature = "rpc-client")] +mod prod { + use super::*; - match res { - Ok(response) => Ok(response.hash), - Err(err) => Err(IoError::IoError(err)), - } + use contracts::pre; + use std::collections::HashMap; + use tendermint_rpc as rpc; + + /// Production implementation of the EvidenceReporter component, which reports evidence to full + /// nodes via RPC. + #[derive(Clone, Debug)] + pub struct ProdEvidenceReporter { + peer_map: HashMap, } -} -impl ProdEvidenceReporter { - /// Constructs a new ProdEvidenceReporter component. - /// - /// A peer map which maps peer IDS to their network address must be supplied. - pub fn new(peer_map: HashMap) -> Self { - Self { peer_map } + #[contract_trait] + impl EvidenceReporter for ProdEvidenceReporter { + #[pre(self.peer_map.contains_key(&peer))] + fn report(&self, e: Evidence, peer: PeerId) -> Result { + let res = block_on(self.rpc_client_for(peer).broadcast_evidence(e)); + + match res { + Ok(response) => Ok(response.hash), + Err(err) => Err(IoError::IoError(err)), + } + } } - // FIXME: Cannot enable precondition because of "autoref lifetime" issue - // #[pre(self.peer_map.contains_key(&peer))] - fn rpc_client_for(&self, peer: PeerId) -> rpc::Client { - let peer_addr = self.peer_map.get(&peer).unwrap().to_owned(); - rpc::Client::new(peer_addr) + impl ProdEvidenceReporter { + /// Constructs a new ProdEvidenceReporter component. + /// + /// A peer map which maps peer IDS to their network address must be supplied. + pub fn new(peer_map: HashMap) -> Self { + Self { peer_map } + } + + // FIXME: Cannot enable precondition because of "autoref lifetime" issue + // #[pre(self.peer_map.contains_key(&peer))] + fn rpc_client_for(&self, peer: PeerId) -> rpc::Client { + let peer_addr = self.peer_map.get(&peer).unwrap().to_owned(); + rpc::Client::new(peer_addr) + } } -} -fn block_on(f: F) -> F::Output { - tokio::runtime::Builder::new() - .basic_scheduler() - .enable_all() - .build() - .unwrap() - .block_on(f) + fn block_on(f: F) -> F::Output { + tokio::runtime::Builder::new() + .basic_scheduler() + .enable_all() + .build() + .unwrap() + .block_on(f) + } } diff --git a/light-node/tests/acceptance.rs b/light-node/tests/acceptance.rs index 071311d58..2952275a4 100644 --- a/light-node/tests/acceptance.rs +++ b/light-node/tests/acceptance.rs @@ -62,6 +62,7 @@ fn start_with_config_and_args() { /// Example of a test which matches a regular expression #[test] +#[ignore] // Ignored because of https://github.com/informalsystems/tendermint-rs/issues/560 fn version_no_args() { let mut runner = RUNNER.clone(); let mut cmd = runner.arg("version").capture_stdout().run(); diff --git a/rpc/Cargo.toml b/rpc/Cargo.toml index 96b1b20ff..73d20c502 100644 --- a/rpc/Cargo.toml +++ b/rpc/Cargo.toml @@ -24,8 +24,8 @@ description = """ all-features = true [features] -default = [] -client = [ "async-tungstenite", "futures", "http", "hyper", "tokio" ] +default = ["client"] +client = ["async-tungstenite", "futures", "http", "hyper", "tokio"] secp256k1 = ["tendermint/secp256k1"] [dependencies] diff --git a/tendermint/Cargo.toml b/tendermint/Cargo.toml index 50ee380ab..8ea65453e 100644 --- a/tendermint/Cargo.toml +++ b/tendermint/Cargo.toml @@ -29,6 +29,9 @@ authors = [ all-features = true rustdoc-args = ["--cfg", "docsrs"] +[lib] +crate-type = ["cdylib", "rlib"] + [dependencies] anomaly = "0.2" async-trait = "0.1"