diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f15ff46bc..284645a01 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,7 +29,7 @@ jobs: with: command: build-all - light-client-wasm: + build-light-client-wasm: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 33f7b933d..19e0916a0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -69,6 +69,15 @@ jobs: with: command: test-all-features args: -p tendermint-light-client + # From https://rustwasm.github.io/docs/wasm-bindgen/wasm-bindgen-test/continuous-integration.html#github-actions + tendermint-light-client-js: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Install wasm-pack + run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh + - run: wasm-pack test --headless --chrome ./light-client-js/ + - run: wasm-pack test --headless --firefox ./light-client-js/ tendermint-light-node: runs-on: ubuntu-latest steps: diff --git a/.gitignore b/.gitignore index 46c3ebed2..f971349c6 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,9 @@ Cargo.lock # Proptest regressions dumps **/*.proptest-regressions + +# Light Client WASM +light-client-js/pkg/ +light-client-js/examples/verifier-web/node_modules/ +light-client-js/examples/verifier-web/dist/ +light-client-js/examples/verifier-web/package-lock.json diff --git a/CHANGELOG.md b/CHANGELOG.md index a620ec1bc..9916945b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,18 @@ -## Unreleased +## Unreleased ### FEATURES * `[tendermint-abci]` Release minimal framework for building ABCI applications in Rust ([#794]) +* `[tendermint-light-client-js]` First release of the + `tendermint-light-client-js` crate to provide access to Tendermint Light + Client functionality from WASM. This only provides access to the `verify` + method at present, exclusively provides access to block verification. This + does not include network access or the Light Client's bisection algorithm + ([#812]) [#794]: https://github.com/informalsystems/tendermint-rs/pull/794 +[#812]: https://github.com/informalsystems/tendermint-rs/pull/812 ## v0.18.1 diff --git a/Cargo.toml b/Cargo.toml index 034bac036..9f6ff41bb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ members = [ "abci", "light-client", + "light-client-js", "light-node", "p2p", "proto", diff --git a/light-client-js/Cargo.toml b/light-client-js/Cargo.toml new file mode 100644 index 000000000..97e7a8753 --- /dev/null +++ b/light-client-js/Cargo.toml @@ -0,0 +1,47 @@ +[package] +name = "tendermint-light-client-js" +version = "0.18.1" +authors = [ + "Romain Ruetschi ", + "Thane Thomson " +] +edition = "2018" +license = "Apache-2.0" +description = """ + tendermint-light-client-js provides a lightweight, WASM-based interface to + the Tendermint Light Client's verification functionality. + """ +repository = "https://github.com/informalsystems/tendermint-rs" + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +default = ["console_error_panic_hook"] + +[dependencies] +serde = { version = "1.0", features = [ "derive" ] } +serde_json = "1.0" +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" ] } + +# The `console_error_panic_hook` crate provides better debugging of panics by +# logging them with `console.error`. This is great for development, but requires +# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for +# code size when deploying. +console_error_panic_hook = { version = "0.1.6", optional = true } + +# `wee_alloc` is a tiny allocator for wasm that is only ~1K in code size +# compared to the default allocator's ~10K. It is slower than the default +# allocator, however. +# +# Unfortunately, `wee_alloc` requires nightly Rust when targeting wasm for now. +wee_alloc = { version = "0.4.5", optional = true } + +[dev-dependencies] +wasm-bindgen-test = "0.3.13" + +[profile.release] +# Tell `rustc` to optimize for small code size. +opt-level = "s" diff --git a/light-client-js/README.md b/light-client-js/README.md new file mode 100644 index 000000000..09c595760 --- /dev/null +++ b/light-client-js/README.md @@ -0,0 +1,17 @@ +# Light-Client API for JavaScript + +At present this just exposes the [Tendermint Light Client]'s verification logic +via WASM. This allows simple access to verification from JavaScript: + +```javascript +import * as LightClient from 'tendermint-light-client-js'; + +// Verify an untrusted block against a trusted one, given the specified options +// and current date/time. +let verdict = LightClient.verify(untrusted, trusted, options, now); +``` + +For an example of how to use this, please see the [verifier-web example]. + +[Tendermint Light Client]: ../light-client/ +[verifier-web example]: ./examples/verifier-web/ diff --git a/light-client-js/examples/verifier-web/README.md b/light-client-js/examples/verifier-web/README.md new file mode 100644 index 000000000..a249c75f1 --- /dev/null +++ b/light-client-js/examples/verifier-web/README.md @@ -0,0 +1,69 @@ +# Web-Based Light Client Verification + +This folder contains a simple *example* web application demonstrating +Tendermint Light Client verification. + +## Requirements + +* Rust stable latest +* NodeJS (tested with v14.15.5) +* [wasm-pack] + +## Try it out + +You first need to build a few things before you can try this example out +locally. + +### Step 1: Building the WASM binary + +From the [`light-client-js` folder](../../): + +```bash +wasm-pack build +``` + +This will build our WASM binary and JavaScript/TypeScript wrappers and place +them into a `pkg` directory. + +### Step 2: Build/run the example + +From this directory (`light-client-js/examples/verifier-web`): + +```bash +# Install all dependencies +npm install + +# Build/start the example app +npm run start +``` + +This should build the example and start a server at http://localhost:8080 + +### Step 3: Input your data + +When you open up at http://localhost:8080, you should see something like the +following: + +![screenshot1](screenshot1.png) + +Copy/paste the JSON representation of a trusted block alongside that of the +untrusted block you wish to verify into the supplied editors. Configure your +desired Light Client parameters, as well as the timestamp at which you want to +check whether the untrusted block is trustworthy, and click the **Verify** +button. + +This will show you the raw JSON object received back from the verifier. + +![screenshot2](screenshot2.png) + +## Limitations + +1. This example only demonstrates *verification*, and is pretty manual right + now (you must manually copy/paste the JSON representations of untrusted and + trusted blocks, and supply options/timestamps manually). No network I/O + happens here, so bring your own I/O to fetch light blocks. +2. The WASM binary built in step 1 only works for bundled web applications + (e.g. using [webpack]). + +[wasm-pack]: https://rustwasm.github.io/docs/wasm-pack/introduction.html +[webpack]: https://webpack.js.org/ diff --git a/light-client-js/examples/verifier-web/bootstrap.js b/light-client-js/examples/verifier-web/bootstrap.js new file mode 100644 index 000000000..7934d627e --- /dev/null +++ b/light-client-js/examples/verifier-web/bootstrap.js @@ -0,0 +1,5 @@ +// A dependency graph that contains any wasm must all be imported +// asynchronously. This `bootstrap.js` file does the single async import, so +// that no one else needs to worry about it again. +import("./index.js") + .catch(e => console.error("Error importing `index.js`:", e)); diff --git a/light-client-js/examples/verifier-web/index.html b/light-client-js/examples/verifier-web/index.html new file mode 100644 index 000000000..88d5cdce5 --- /dev/null +++ b/light-client-js/examples/verifier-web/index.html @@ -0,0 +1,106 @@ + + + + + + Tendermint Light Client WASM Example + + + + +
+
+

Tendermint Light Client WASM Example

+

+ This example demonstrates how to make use of the + Tendermint Light Client + WASM build in a simple web application. +

+
+
+ +
+
+
+
+

Untrusted Block

+
+
+ +
+

Trusted Block

+
+
+
+
+
+ +
+
+

Options

+
+
+
+ +
+ +
+

Must be in the format: numerator/denominator

+
+
+ +
+
+ +
+ +
+

A.k.a. "unbonding period", in seconds

+
+
+ +
+
+ +
+ +
+

In seconds

+
+
+ +
+
+ +
+ +
+

Timestamp in RFC3339 format for which you want to verify the block

+
+
+
+
+
+ +
+
+ +
+
+ +
+
+

Verdict

+

+  
+
+ + + diff --git a/light-client-js/examples/verifier-web/index.js b/light-client-js/examples/verifier-web/index.js new file mode 100644 index 000000000..42890cf00 --- /dev/null +++ b/light-client-js/examples/verifier-web/index.js @@ -0,0 +1,186 @@ +import * as monaco from 'monaco-editor'; +import * as LightClient from 'tendermint-light-client-js'; + +let untrustedBlockEditor = monaco.editor.create(document.getElementById("untrusted-block-editor"), { + value: JSON.stringify({ + "signed_header": { + "header": { + "version": { + "block": "11", + "app": "0" + }, + "chain_id": "test-chain", + "height": "4", + "time": "1970-01-01T00:00:04Z", + "last_block_id": null, + "last_commit_hash": null, + "data_hash": null, + "validators_hash": "75E6DD63C2DC2B58FE0ED82792EAB369C4308C7EC16B69446382CC4B41D46068", + "next_validators_hash": "C8CFFADA9808F685C4111693E1ADFDDBBEE9B9493493BEF805419F143C5B0D0A", + "consensus_hash": "75E6DD63C2DC2B58FE0ED82792EAB369C4308C7EC16B69446382CC4B41D46068", + "app_hash": "", + "last_results_hash": null, + "evidence_hash": null, + "proposer_address": "6AE5C701F508EB5B63343858E068C5843F28105F" + }, + "commit": { + "height": "4", + "round": 1, + "block_id": { + "hash": "D0E7B0C678E290DA835BB26EE826472D66B6A306801E5FE0803C5320C554610A", + "part_set_header": { + "total": 1, + "hash": "D0E7B0C678E290DA835BB26EE826472D66B6A306801E5FE0803C5320C554610A" + } + }, + "signatures": [ + { + "block_id_flag": 2, + "validator_address": "6AE5C701F508EB5B63343858E068C5843F28105F", + "timestamp": "1970-01-01T00:00:04Z", + "signature": "lTGBsjVI6YwIRcxQ6Lct4Q+xrtJc9h3648c42uWe4MpSgy4rUI5g71AEpG90Tbn0PRizjKgCPhokPpQoQLiqAg==" + } + ] + } + }, + "validator_set": { + "total_voting_power": "0", + "validators": [ + { + "address": "6AE5C701F508EB5B63343858E068C5843F28105F", + "pub_key": { + "type": "tendermint/PubKeyEd25519", + "value": "GQEC/HB4sDBAVhHtUzyv4yct9ZGnudaP209QQBSTfSQ=" + }, + "voting_power": "50", + "proposer_priority": null + } + ] + }, + "next_validator_set": { + "total_voting_power": "0", + "validators": [ + { + "address": "C479DB6F37AB9757035CFBE10B687E27668EE7DF", + "pub_key": { + "type": "tendermint/PubKeyEd25519", + "value": "3wf60CidQcsIO7TksXzEZsJefMUFF73k6nP1YeEo9to=" + }, + "voting_power": "50", + "proposer_priority": null + } + ] + }, + "provider": "BADFADAD0BEFEEDC0C0ADEADBEEFC0FFEEFACADE" + }, null, 2), + language: 'json', + minimap: { enabled: false } +}); + +let trustedBlockEditor = monaco.editor.create(document.getElementById("trusted-block-editor"), { + value: JSON.stringify({ + "signed_header": { + "header": { + "version": { + "block": "11", + "app": "0" + }, + "chain_id": "test-chain", + "height": "3", + "time": "1970-01-01T00:00:03Z", + "last_block_id": null, + "last_commit_hash": null, + "data_hash": null, + "validators_hash": "75E6DD63C2DC2B58FE0ED82792EAB369C4308C7EC16B69446382CC4B41D46068", + "next_validators_hash": "75E6DD63C2DC2B58FE0ED82792EAB369C4308C7EC16B69446382CC4B41D46068", + "consensus_hash": "75E6DD63C2DC2B58FE0ED82792EAB369C4308C7EC16B69446382CC4B41D46068", + "app_hash": "", + "last_results_hash": null, + "evidence_hash": null, + "proposer_address": "6AE5C701F508EB5B63343858E068C5843F28105F" + }, + "commit": { + "height": "3", + "round": 1, + "block_id": { + "hash": "AAB1B09D5FADAAE7CDF3451961A63F810DB73BF3214A7B74DBA36C52EDF1A793", + "part_set_header": { + "total": 1, + "hash": "AAB1B09D5FADAAE7CDF3451961A63F810DB73BF3214A7B74DBA36C52EDF1A793" + } + }, + "signatures": [ + { + "block_id_flag": 2, + "validator_address": "6AE5C701F508EB5B63343858E068C5843F28105F", + "timestamp": "1970-01-01T00:00:03Z", + "signature": "xn0eSsHYIsqUbmfAiJq1R0hqZbfuIjs5Na1c88EC1iPTuQAesKg9I7nXG4pk8d6U5fU4GysNLk5I4f7aoefOBA==" + } + ] + } + }, + "validator_set": { + "total_voting_power": "0", + "validators": [ + { + "address": "6AE5C701F508EB5B63343858E068C5843F28105F", + "pub_key": { + "type": "tendermint/PubKeyEd25519", + "value": "GQEC/HB4sDBAVhHtUzyv4yct9ZGnudaP209QQBSTfSQ=" + }, + "voting_power": "50", + "proposer_priority": null + } + ] + }, + "next_validator_set": { + "total_voting_power": "0", + "validators": [ + { + "address": "6AE5C701F508EB5B63343858E068C5843F28105F", + "pub_key": { + "type": "tendermint/PubKeyEd25519", + "value": "GQEC/HB4sDBAVhHtUzyv4yct9ZGnudaP209QQBSTfSQ=" + }, + "voting_power": "50", + "proposer_priority": null + } + ] + }, + "provider": "BADFADAD0BEFEEDC0C0ADEADBEEFC0FFEEFACADE" + }, null, 2), + language: 'json', + minimap: { enabled: false } +}); + +document.getElementById('verify-btn').addEventListener('click', function() { + let untrusted = JSON.parse(untrustedBlockEditor.getValue()); + let trusted = JSON.parse(trustedBlockEditor.getValue()); + let trustThreshold = document.getElementById('trust-threshold-input').value; + let trustThresholdParts = trustThreshold.split("/"); + if (trustThresholdParts.length !== 2) { + window.alert("Expected trust threshold to be of the format \"numerator/denominator\""); + return; + } + let trustThresholdNum = parseInt(trustThresholdParts[0].trim(), 10); + let trustThresholdDen = parseInt(trustThresholdParts[1].trim(), 10); + let trustingPeriod = parseInt(document.getElementById('trusting-period-input').value, 10); + let clockDrift = parseInt(document.getElementById('clock-drift-input').value, 10); + let now = document.getElementById('now-input').value; + + let options = { + trust_threshold: [trustThresholdNum, trustThresholdDen], + trusting_period: trustingPeriod, + clock_drift: clockDrift + }; + + console.log("Untrusted block:", untrusted); + console.log("Trusted block:", trusted); + console.log("Options:", options); + console.log("Now:", now); + + let verdict = LightClient.verify(untrusted, trusted, options, now); + + document.getElementById('verdict').innerText = JSON.stringify(verdict, null, 2); + document.getElementById('verdict-section').style.visibility = 'visible'; +}); diff --git a/light-client-js/examples/verifier-web/package.json b/light-client-js/examples/verifier-web/package.json new file mode 100644 index 000000000..ee22aae9b --- /dev/null +++ b/light-client-js/examples/verifier-web/package.json @@ -0,0 +1,46 @@ +{ + "name": "tendermint-light-client-verifier-web", + "version": "0.1.0", + "description": "an example app to demonstrate Tendermint Light Client verification on the web using WASM", + "main": "index.js", + "scripts": { + "build": "webpack --config webpack.config.js", + "start": "webpack-dev-server" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/informalsystems/tendermint-rs.git" + }, + "keywords": [ + "tendermint", + "light-client", + "webassembly", + "wasm", + "rust", + "webpack" + ], + "author": "Informal Systems ", + "contributors": [ + "Romain Ruetschi ", + "Thane Thomson " + ], + "license": "(MIT OR Apache-2.0)", + "bugs": { + "url": "https://github.com/informalsystems/tendermint-rs/issues" + }, + "homepage": "https://github.com/informalsystems/tendermint-rs", + "devDependencies": { + "copy-webpack-plugin": "^5.1.2", + "css-loader": "^5.0.2", + "file-loader": "^6.2.0", + "monaco-editor-webpack-plugin": "^3.0.0", + "style-loader": "^2.0.0", + "webpack": "^4.46.0", + "webpack-cli": "^3.3.12", + "webpack-dev-server": "^3.11.2" + }, + "dependencies": { + "monaco-editor": "^0.22.3", + "tendermint-light-client-js": "file:../../pkg" + } +} diff --git a/light-client-js/examples/verifier-web/screenshot1.png b/light-client-js/examples/verifier-web/screenshot1.png new file mode 100644 index 000000000..7aebf15cd Binary files /dev/null and b/light-client-js/examples/verifier-web/screenshot1.png differ diff --git a/light-client-js/examples/verifier-web/screenshot2.png b/light-client-js/examples/verifier-web/screenshot2.png new file mode 100644 index 000000000..46000b865 Binary files /dev/null and b/light-client-js/examples/verifier-web/screenshot2.png differ diff --git a/light-client-js/examples/verifier-web/webpack.config.js b/light-client-js/examples/verifier-web/webpack.config.js new file mode 100644 index 000000000..09a1447a9 --- /dev/null +++ b/light-client-js/examples/verifier-web/webpack.config.js @@ -0,0 +1,27 @@ +const CopyWebpackPlugin = require("copy-webpack-plugin"); +const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin'); +const path = require('path'); + +module.exports = { + entry: "./bootstrap.js", + output: { + path: path.resolve(__dirname, "dist"), + filename: "bootstrap.js", + }, + module: { + rules: [{ + test: /\.css$/, + use: ['style-loader', 'css-loader'] + }, { + test: /\.ttf$/, + use: ['file-loader'] + }] + }, + mode: "development", + plugins: [ + new CopyWebpackPlugin(['index.html']), + new MonacoWebpackPlugin({ + languages: ["json"] + }) + ], +}; diff --git a/light-client-js/src/lib.rs b/light-client-js/src/lib.rs new file mode 100644 index 000000000..ea5245e67 --- /dev/null +++ b/light-client-js/src/lib.rs @@ -0,0 +1,88 @@ +mod utils; + +use serde::{Deserialize, Serialize}; +use std::time::Duration; +use tendermint::Time; +use tendermint_light_client::components::verifier::{ProdVerifier, Verifier}; +use tendermint_light_client::light_client::Options; +use tendermint_light_client::types::{LightBlock, TrustThreshold}; +use wasm_bindgen::prelude::*; +use wasm_bindgen::JsValue; + +// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global +// allocator. +#[cfg(feature = "wee_alloc")] +#[global_allocator] +static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; + +/// Check whether a given untrusted block can be trusted. +#[wasm_bindgen] +pub fn verify(untrusted: &JsValue, trusted: &JsValue, options: &JsValue, now: &JsValue) -> JsValue { + let result = deserialize_params(untrusted, trusted, options, now).map( + |(untrusted, trusted, options, now)| { + let verifier = ProdVerifier::default(); + verifier.verify(&untrusted, &trusted, &options, now) + }, + ); + JsValue::from_serde(&result).unwrap() +} + +fn deserialize_params( + untrusted: &JsValue, + trusted: &JsValue, + options: &JsValue, + now: &JsValue, +) -> Result<(LightBlock, LightBlock, Options, Time), Error> { + let untrusted = untrusted.into_serde().map_err(|e| Error::Serialization { + param: "untrusted".to_string(), + msg: e.to_string(), + })?; + + let trusted = trusted.into_serde().map_err(|e| Error::Serialization { + param: "trusted".to_string(), + msg: e.to_string(), + })?; + + let options = options + .into_serde::() + .map(Into::into) + .map_err(|e| Error::Serialization { + param: "options".to_string(), + msg: e.to_string(), + })?; + + let now = now.into_serde().map_err(|e| Error::Serialization { + param: "now".to_string(), + msg: e.to_string(), + })?; + + Ok((untrusted, trusted, options, now)) +} + +/// Errors produced by this crate. +#[derive(Serialize, Deserialize, Debug)] +#[serde(tag = "type")] +pub enum Error { + /// A serialization/deserialization error occurred. + #[serde(rename = "serialization")] + Serialization { param: String, msg: String }, +} + +// Simplified options supplied from JavaScript. +#[derive(Debug, Serialize, Deserialize)] +pub struct JsOptions { + pub trust_threshold: (u64, u64), + pub trusting_period: u64, + pub clock_drift: u64, +} + +impl From for Options { + fn from(o: JsOptions) -> Self { + let (num, den) = o.trust_threshold; + Self { + trust_threshold: TrustThreshold::new(num, den).unwrap(), + trusting_period: Duration::from_secs(o.trusting_period), + clock_drift: Duration::from_secs(o.clock_drift), + } + } +} diff --git a/light-client-js/src/utils.rs b/light-client-js/src/utils.rs new file mode 100644 index 000000000..c4be847ee --- /dev/null +++ b/light-client-js/src/utils.rs @@ -0,0 +1,11 @@ +#[allow(dead_code)] +pub fn set_panic_hook() { + // When the `console_error_panic_hook` feature is enabled, we can call the + // `set_panic_hook` function at least once during initialization, and then + // we will get better error messages if our code ever panics. + // + // For more details see + // https://github.com/rustwasm/console_error_panic_hook#readme + #[cfg(feature = "console_error_panic_hook")] + console_error_panic_hook::set_once(); +} diff --git a/light-client-js/tests/web.rs b/light-client-js/tests/web.rs new file mode 100644 index 000000000..03e36c753 --- /dev/null +++ b/light-client-js/tests/web.rs @@ -0,0 +1,209 @@ +//! Test suite for the Web and headless browsers. + +#![cfg(target_arch = "wasm32")] + +extern crate wasm_bindgen_test; +use tendermint::Time; +use tendermint_light_client::components::verifier::Verdict; +use tendermint_light_client::types::LightBlock; +use tendermint_light_client_js::{verify, Error, JsOptions}; +use wasm_bindgen::JsValue; +use wasm_bindgen_test::*; + +wasm_bindgen_test_configure!(run_in_browser); + +const UNTRUSTED_BLOCK: &str = r#"{ + "signed_header": { + "header": { + "version": { + "block": "11", + "app": "0" + }, + "chain_id": "test-chain", + "height": "4", + "time": "1970-01-01T00:00:04Z", + "last_block_id": null, + "last_commit_hash": null, + "data_hash": null, + "validators_hash": "75E6DD63C2DC2B58FE0ED82792EAB369C4308C7EC16B69446382CC4B41D46068", + "next_validators_hash": "C8CFFADA9808F685C4111693E1ADFDDBBEE9B9493493BEF805419F143C5B0D0A", + "consensus_hash": "75E6DD63C2DC2B58FE0ED82792EAB369C4308C7EC16B69446382CC4B41D46068", + "app_hash": "", + "last_results_hash": null, + "evidence_hash": null, + "proposer_address": "6AE5C701F508EB5B63343858E068C5843F28105F" + }, + "commit": { + "height": "4", + "round": 1, + "block_id": { + "hash": "D0E7B0C678E290DA835BB26EE826472D66B6A306801E5FE0803C5320C554610A", + "part_set_header": { + "total": 1, + "hash": "D0E7B0C678E290DA835BB26EE826472D66B6A306801E5FE0803C5320C554610A" + } + }, + "signatures": [ + { + "block_id_flag": 2, + "validator_address": "6AE5C701F508EB5B63343858E068C5843F28105F", + "timestamp": "1970-01-01T00:00:04Z", + "signature": "lTGBsjVI6YwIRcxQ6Lct4Q+xrtJc9h3648c42uWe4MpSgy4rUI5g71AEpG90Tbn0PRizjKgCPhokPpQoQLiqAg==" + } + ] + } + }, + "validator_set": { + "total_voting_power": "0", + "validators": [ + { + "address": "6AE5C701F508EB5B63343858E068C5843F28105F", + "pub_key": { + "type": "tendermint/PubKeyEd25519", + "value": "GQEC/HB4sDBAVhHtUzyv4yct9ZGnudaP209QQBSTfSQ=" + }, + "voting_power": "50", + "proposer_priority": null + } + ] + }, + "next_validator_set": { + "total_voting_power": "0", + "validators": [ + { + "address": "C479DB6F37AB9757035CFBE10B687E27668EE7DF", + "pub_key": { + "type": "tendermint/PubKeyEd25519", + "value": "3wf60CidQcsIO7TksXzEZsJefMUFF73k6nP1YeEo9to=" + }, + "voting_power": "50", + "proposer_priority": null + } + ] + }, + "provider": "BADFADAD0BEFEEDC0C0ADEADBEEFC0FFEEFACADE" +}"#; + +const TRUSTED_BLOCK: &str = r#"{ + "signed_header": { + "header": { + "version": { + "block": "11", + "app": "0" + }, + "chain_id": "test-chain", + "height": "3", + "time": "1970-01-01T00:00:03Z", + "last_block_id": null, + "last_commit_hash": null, + "data_hash": null, + "validators_hash": "75E6DD63C2DC2B58FE0ED82792EAB369C4308C7EC16B69446382CC4B41D46068", + "next_validators_hash": "75E6DD63C2DC2B58FE0ED82792EAB369C4308C7EC16B69446382CC4B41D46068", + "consensus_hash": "75E6DD63C2DC2B58FE0ED82792EAB369C4308C7EC16B69446382CC4B41D46068", + "app_hash": "", + "last_results_hash": null, + "evidence_hash": null, + "proposer_address": "6AE5C701F508EB5B63343858E068C5843F28105F" + }, + "commit": { + "height": "3", + "round": 1, + "block_id": { + "hash": "AAB1B09D5FADAAE7CDF3451961A63F810DB73BF3214A7B74DBA36C52EDF1A793", + "part_set_header": { + "total": 1, + "hash": "AAB1B09D5FADAAE7CDF3451961A63F810DB73BF3214A7B74DBA36C52EDF1A793" + } + }, + "signatures": [ + { + "block_id_flag": 2, + "validator_address": "6AE5C701F508EB5B63343858E068C5843F28105F", + "timestamp": "1970-01-01T00:00:03Z", + "signature": "xn0eSsHYIsqUbmfAiJq1R0hqZbfuIjs5Na1c88EC1iPTuQAesKg9I7nXG4pk8d6U5fU4GysNLk5I4f7aoefOBA==" + } + ] + } + }, + "validator_set": { + "total_voting_power": "0", + "validators": [ + { + "address": "6AE5C701F508EB5B63343858E068C5843F28105F", + "pub_key": { + "type": "tendermint/PubKeyEd25519", + "value": "GQEC/HB4sDBAVhHtUzyv4yct9ZGnudaP209QQBSTfSQ=" + }, + "voting_power": "50", + "proposer_priority": null + } + ] + }, + "next_validator_set": { + "total_voting_power": "0", + "validators": [ + { + "address": "6AE5C701F508EB5B63343858E068C5843F28105F", + "pub_key": { + "type": "tendermint/PubKeyEd25519", + "value": "GQEC/HB4sDBAVhHtUzyv4yct9ZGnudaP209QQBSTfSQ=" + }, + "voting_power": "50", + "proposer_priority": null + } + ] + }, + "provider": "BADFADAD0BEFEEDC0C0ADEADBEEFC0FFEEFACADE" +}"#; + +#[wasm_bindgen_test] +fn successful_verification() { + let (untrusted_block, trusted_block) = test_blocks(); + let options = test_options(); + // Choose a "now" value within the trusting period + let now = + JsValue::from_serde(&Time::parse_from_rfc3339("1970-01-07T00:00:00Z").unwrap()).unwrap(); + let js_result = verify(&untrusted_block, &trusted_block, &options, &now); + console_log!("js_result = {:?}", js_result); + let verdict = JsValue::into_serde::>(&js_result) + .unwrap() + .unwrap(); + assert_eq!(verdict, Verdict::Success); +} + +#[wasm_bindgen_test] +fn failed_verification_outside_trusting_period() { + let (untrusted_block, trusted_block) = test_blocks(); + let options = test_options(); + // Choose a "now" value outside the trusting period + let now = + JsValue::from_serde(&Time::parse_from_rfc3339("1970-01-16T00:00:00Z").unwrap()).unwrap(); + let js_result = verify(&untrusted_block, &trusted_block, &options, &now); + console_log!("js_result = {:?}", js_result); + // The result is Ok because we successfully obtained a verdict, even if the + // verdict isn't Verdict::Success. + let verdict = JsValue::into_serde::>(&js_result) + .unwrap() + .unwrap(); + match verdict { + Verdict::Success | Verdict::NotEnoughTrust(_) => panic!("unexpected verdict"), + _ => {} + } +} + +fn test_blocks() -> (JsValue, JsValue) { + let untrusted_block = + JsValue::from_serde(&serde_json::from_str::(UNTRUSTED_BLOCK).unwrap()).unwrap(); + let trusted_block = + JsValue::from_serde(&serde_json::from_str::(TRUSTED_BLOCK).unwrap()).unwrap(); + (untrusted_block, trusted_block) +} + +fn test_options() -> JsValue { + JsValue::from_serde(&JsOptions { + trust_threshold: (1, 3), + trusting_period: 1209600, // 2 weeks + clock_drift: 5, // 5 seconds + }) + .unwrap() +} diff --git a/light-client/src/components/verifier.rs b/light-client/src/components/verifier.rs index b25177dac..fb37b1c81 100644 --- a/light-client/src/components/verifier.rs +++ b/light-client/src/components/verifier.rs @@ -11,10 +11,11 @@ use crate::{ types::{LightBlock, Time}, }; use preds::{errors::VerificationError, ProdPredicates, VerificationPredicates}; +use serde::{Deserialize, Serialize}; /// Represents the result of the verification performed by the /// verifier component. -#[derive(Debug)] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] pub enum Verdict { /// Verification succeeded, the block is valid. Success, diff --git a/light-client/src/operations/voting_power.rs b/light-client/src/operations/voting_power.rs index 2bcd40955..1e46d1b5f 100644 --- a/light-client/src/operations/voting_power.rs +++ b/light-client/src/operations/voting_power.rs @@ -16,7 +16,7 @@ use tendermint::trust_threshold::TrustThreshold as _; use tendermint::vote::{SignedVote, ValidatorIndex, Vote}; /// Tally for the voting power computed by the `VotingPowerCalculator` -#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize, Eq)] pub struct VotingPowerTally { /// Total voting power pub total: u64, diff --git a/light-client/src/predicates/errors.rs b/light-client/src/predicates/errors.rs index 569f3d157..99c02fb34 100644 --- a/light-client/src/predicates/errors.rs +++ b/light-client/src/predicates/errors.rs @@ -10,7 +10,7 @@ use crate::types::{Hash, Height, Time, Validator, ValidatorAddress}; /// The various errors which can be raised by the verifier component, /// when validating or verifying a light block. -#[derive(Debug, Clone, Error, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, Error, PartialEq, Serialize, Deserialize, Eq)] pub enum VerificationError { /// The header is from the future #[error("header from the future: header_time={header_time} now={now}")] diff --git a/tendermint/src/validator.rs b/tendermint/src/validator.rs index 9201ccdf8..62bc04a6a 100644 --- a/tendermint/src/validator.rs +++ b/tendermint/src/validator.rs @@ -143,7 +143,7 @@ impl Set { /// Validator information // Todo: Remove address and make it into a function that generates it on the fly from pub_key. -#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize, Eq)] pub struct Info { /// Validator account address pub address: account::Id,