diff --git a/light-node/Cargo.toml b/light-node/Cargo.toml
index d8ec745e7..99e03a3e5 100644
--- a/light-node/Cargo.toml
+++ b/light-node/Cargo.toml
@@ -16,6 +16,7 @@ jsonrpc-http-server = "14.2"
jsonrpc-derive = "14.2"
serde = { version = "1", features = ["serde_derive"] }
serde_json = "1.0"
+sled = "0.31.0"
tendermint = { version = "0.14.0", path = "../tendermint" }
tendermint-light-client = { version = "0.14.0", path = "../light-client" }
tendermint-rpc = { version = "0.14.0", path = "../rpc", features = [ "client" ] }
diff --git a/light-node/README.md b/light-node/README.md
index 544af9540..1097df48c 100644
--- a/light-node/README.md
+++ b/light-node/README.md
@@ -1,14 +1,209 @@
-# LightNode
+# Light-Node
-Tendermint light client node.
+The [Tendermint] light-node wraps the [light-client] crate into a command-line interface tool.
+It can be used as a standalone light client daemon and exposes a JSONRPC endpoint
+from which you can query the current state of the light node.
## Getting Started
-This application is authored using [Abscissa], a Rust application framework.
+### Prerequisites
-For more information, see:
+This short tutorial assumes that you are familiar with how to run a Tendermint fullnode on your machine. To learn how to do this, you can consult the [quick start] section of the tendermint documentation.
-[Documentation]
+This tutorial further assumes you have `git` and the latest stable rust tool-chain installed (see https://rustup.rs/).
+Additionally, the `jq` tool will make your life easier when dealing with JSON output.
-[Abscissa]: https://github.com/iqlusioninc/abscissa
-[Documentation]: https://docs.rs/abscissa_core/
+#### Cloning the repository
+
+To run the light node from source you have to clone this repository first:
+```
+$ git clone https://github.com/informalsystems/tendermint-rs.git
+```
+
+Then navigate to the light node crate:
+```
+$ cd tendermint-rs/light-node
+```
+
+### Configuration
+
+You can configure all aspects of light node via a configuration file.
+An example cofigartion can be found under [light_node.toml.example](light_node.toml.example).
+
+If you are running a Tendermint fullnode on your machine, you can simply copy and use it to get started:
+```
+$ cp light_node.toml.example light_node.toml
+```
+Please, take a look into the config file and edit it according to your needs.
+The provided example configuration file comes with a lot of explanatory comments
+which hopefully provide enough guidance to configure your light node.
+
+### Subjective initialization
+Assuming that you are running a Tendermint fullnode that exposes an RPC endpoint on your loopback interface, you can intialize the light-node subjectively following th following steps:
+
+First, you have to obtain a header hash and height you want to trust (subjectively). For our purposes you can obtain one via querying the Tendermint fullnode you are running.
+Here we are obtaining the header hash of height 2:
+```
+$ curl -X GET "http://localhost:26657/block?height=2" -H "accept: application/json" | jq .result.block_id.hash 1515:15:26
+ % Total % Received % Xferd Average Speed Time Time Time Current
+ Dload Upload Total Spent Left Speed
+100 2155 0 2155 0 0 161k 0 --:--:-- --:--:-- --:--:-- 161k
+"76F85BEF1133114482FC8F78C5E78D2B1C1875DD8422A0394B175DD694A7FBA1"
+```
+
+You can now use this header hash to subjectively initialize your light node via:
+```
+$ cargo run -- initialize 2 76F85BEF1133114482FC8F78C5E78D2B1C1875DD8422A0394B175DD694A7FBA1
+```
+
+Note that calling `cargo run` for the first time might take a while as this command will also compile the light node and all its dependencies.
+
+### Running the light node daemon
+
+Now you can start your light node by simply running:
+```
+$ cargo run -- start
+```
+
+If everything worked the output will look sth like:
+```
+ cargo run -- start 17:56:31
+ Finished dev [unoptimized + debuginfo] target(s) in 0.42s
+ Running `/redacted/tendermint-rs/target/debug/light_node start`
+[info] synced to block 20041
+[info] synced to block 20042
+[info] synced to block 20044
+[info] synced to block 20046
+[info] synced to block 20048
+[info] synced to block 20049
+[info] synced to block 20051
+[info] synced to block 20053
+[info] synced to block 20054
+[...]
+```
+
+You can stop the light node by pressing Ctrl+c.
+
+### Help
+
+You will notice that some config parameters can be overwritten via command line arguments.
+
+To get a full overview and commandline parameters and available sub-commands, run:
+
+```
+$ cargo run -- help
+```
+Or on a specific sub-command, e.g.:
+ ```shell script
+$ cargo run -- help start
+ ```
+
+### JSONRPC Endpoint(s)
+
+When you have a light-node running you can query its current state via:
+```
+$ curl localhost:8888 -X POST -H 'Content-Type: application/json' \
+ -d '{"jsonrpc": "2.0", "method": "state", "id": 1}' | jq
+```
+
+
+ Click here to see an example for expected output:
+
+Command:
+ ```
+$ curl localhost:8888 -X POST -H 'Content-Type: application/json' \
+ -d '{"jsonrpc": "2.0", "method": "state", "id": 1}' | jq
+ % Total % Received % Xferd Average Speed Time Time Time Current
+ Dload Upload Total Spent Left Speed
+100 1902 100 1856 100 46 164k 4181 --:--:-- --:--:-- --:--:-- 168k
+```
+Example output:
+```json
+{
+ "jsonrpc": "2.0",
+ "result": {
+ "next_validator_set": {
+ "validators": [
+ {
+ "address": "AD358F20C8CE80889E0F0248FDDC454595D632AE",
+ "proposer_priority": "0",
+ "pub_key": {
+ "type": "tendermint/PubKeyEd25519",
+ "value": "uo9rbgR5J0kuED0C529bTa6mcHZ4uXDjJRdg1k8proY="
+ },
+ "voting_power": "10"
+ }
+ ]
+ },
+ "provider": "BADFADAD0BEFEEDC0C0ADEADBEEFC0FFEEFACADE",
+ "signed_header": {
+ "commit": {
+ "block_id": {
+ "hash": "76F85BEF1133114482FC8F78C5E78D2B1C1875DD8422A0394B175DD694A7FBA1",
+ "parts": {
+ "hash": "568F279E3F59FBE3CABEACE7A3C028C15CA6A902F9D77DDEBA3BFCB9514E2881",
+ "total": "1"
+ }
+ },
+ "height": "2",
+ "round": "0",
+ "signatures": [
+ {
+ "block_id_flag": 2,
+ "signature": "sN3e6bzKLeIFNRptQ4SytBDLZJA53e92D6FWTll5Lq8Wdg4fVzxya6qx3SHFU82ukuj8jKmBMkwTTJsb8xThCQ==",
+ "timestamp": "2020-07-10T12:39:06.977628900Z",
+ "validator_address": "AD358F20C8CE80889E0F0248FDDC454595D632AE"
+ }
+ ]
+ },
+ "header": {
+ "app_hash": "0000000000000000",
+ "chain_id": "dockerchain",
+ "consensus_hash": "048091BC7DDC283F77BFBF91D73C44DA58C3DF8A9CBC867405D8B7F3DAADA22F",
+ "data_hash": null,
+ "evidence_hash": null,
+ "height": "2",
+ "last_block_id": {
+ "hash": "F008EACA817CF6A3918CF7A6FD44F1F2464BB24D25A7EDB45A03E8783E9AB438",
+ "parts": {
+ "hash": "BF5130E879A02AC4BB83E392732ED4A37BE2F01304A615467EE7960858774E57",
+ "total": "1"
+ }
+ },
+ "last_commit_hash": "474496740A2EAA967EED02B239DA302BAF696AE36AEA78F7FEFCE4A77CCA5B33",
+ "last_results_hash": null,
+ "next_validators_hash": "74F2AC2B6622504D08DD2509E28CE731985CFE4D133C9DB0CB85763EDCA95AA3",
+ "proposer_address": "AD358F20C8CE80889E0F0248FDDC454595D632AE",
+ "time": "2020-07-10T12:39:05.977628900Z",
+ "validators_hash": "74F2AC2B6622504D08DD2509E28CE731985CFE4D133C9DB0CB85763EDCA95AA3",
+ "version": {
+ "app": "1",
+ "block": "10"
+ }
+ }
+ },
+ "validator_set": {
+ "validators": [
+ {
+ "address": "AD358F20C8CE80889E0F0248FDDC454595D632AE",
+ "proposer_priority": "0",
+ "pub_key": {
+ "type": "tendermint/PubKeyEd25519",
+ "value": "uo9rbgR5J0kuED0C529bTa6mcHZ4uXDjJRdg1k8proY="
+ },
+ "voting_power": "10"
+ }
+ ]
+ }
+ },
+ "id": 1
+}
+
+```
+
+
+
+
+[quick start]: https://github.com/tendermint/tendermint/blob/master/docs/introduction/quick-start.md
+[Tendermint]: https://github.com/tendermint/tendermint
+[light-client]: https://github.com/informalsystems/tendermint-rs/tree/master/light-client
\ No newline at end of file
diff --git a/light-node/examples/rpc.rs b/light-node/examples/rpc.rs
deleted file mode 100644
index aa778a8b3..000000000
--- a/light-node/examples/rpc.rs
+++ /dev/null
@@ -1,146 +0,0 @@
-//! Basic example of running the RPC server. This is a temporary show-case and should be removed
-//! once integrated in the light node proper. To test the `/state` endpoint run:
-//!
-//! curl localhost:8888 -X POST -H 'Content-Type: application/json' -d '{"jsonrpc": "2.0", "method": "state", "id": 1}'
-
-use tendermint_light_client::errors::Error;
-use tendermint_light_client::supervisor::Handle;
-use tendermint_light_client::types::LightBlock;
-
-use tendermint_light_node::rpc;
-
-fn main() -> Result<(), Box> {
- let handle = MockHandle {};
- let server = rpc::Server::new(handle);
-
- Ok(rpc::run(server, "127.0.0.1:8888")?)
-}
-
-struct MockHandle;
-
-impl Handle for MockHandle {
- fn latest_trusted(&self) -> Result, Error> {
- let block: LightBlock = serde_json::from_str(LIGHTBLOCK_JSON).unwrap();
-
- Ok(Some(block))
- }
-}
-
-const LIGHTBLOCK_JSON: &str = r#"
-{
- "signed_header": {
- "header": {
- "version": {
- "block": "0",
- "app": "0"
- },
- "chain_id": "test-chain-01",
- "height": "1",
- "time": "2019-11-02T15:04:00Z",
- "last_block_id": {
- "hash": "",
- "parts": {
- "total": "0",
- "hash": ""
- }
- },
- "last_commit_hash": "",
- "data_hash": "",
- "validators_hash": "ADAE23D9D908638F3866C11A39E31CE4399AE6DE8EC8EBBCB1916B90C46EDDE3",
- "next_validators_hash": "ADAE23D9D908638F3866C11A39E31CE4399AE6DE8EC8EBBCB1916B90C46EDDE3",
- "consensus_hash": "048091BC7DDC283F77BFBF91D73C44DA58C3DF8A9CBC867405D8B7F3DAADA22F",
- "app_hash": "6170705F68617368",
- "last_results_hash": "",
- "evidence_hash": "",
- "proposer_address": "01F527D77D3FFCC4FCFF2DDC2952EEA5414F2A33"
- },
- "commit": {
- "height": "1",
- "round": "1",
- "block_id": {
- "hash": "76B0FB738138A2C934300D7B23C280B65965D7427DA4D5414B41C75EBC4AD4C3",
- "parts": {
- "total": "1",
- "hash": "073CE26981DF93820595E602CE63B810BC8F1003D6BB28DEDFF5B2F4F09811A1"
- }
- },
- "signatures": [
- {
- "block_id_flag": 2,
- "validator_address": "01F527D77D3FFCC4FCFF2DDC2952EEA5414F2A33",
- "timestamp": "2019-11-02T15:04:10Z",
- "signature": "NaNXQhv7SgBtcq+iHwItxlYUMGHP5MeFpTbyNsnLtzwM6P/EAAAexUH94+osvRDoiahUOoQrRlTiZrYGfahWBw=="
- },
- {
- "block_id_flag": 2,
- "validator_address": "026CC7B6F3E62F789DBECEC59766888B5464737D",
- "timestamp": "2019-11-02T15:04:10Z",
- "signature": "tw0csJ1L1vkBG/71BMjrFEcA6VWjOx29WMwkg1cmDn82XBjRFz+HJu7amGoIj6WLL2p26pO25yQR49crsYQ+AA=="
- }
- ]
- }
- },
- "validator_set": {
- "validators": [
- {
- "address": "01F527D77D3FFCC4FCFF2DDC2952EEA5414F2A33",
- "pub_key": {
- "type": "tendermint/PubKeyEd25519",
- "value": "OAaNq3DX/15fGJP2MI6bujt1GRpvjwrqIevChirJsbc="
- },
- "voting_power": "50",
- "proposer_priority": "-50"
- },
- {
- "address": "026CC7B6F3E62F789DBECEC59766888B5464737D",
- "pub_key": {
- "type": "tendermint/PubKeyEd25519",
- "value": "+vlsKpn6ojn+UoTZl+w+fxeqm6xvUfBokTcKfcG3au4="
- },
- "voting_power": "50",
- "proposer_priority": "50"
- }
- ],
- "proposer": {
- "address": "01F527D77D3FFCC4FCFF2DDC2952EEA5414F2A33",
- "pub_key": {
- "type": "tendermint/PubKeyEd25519",
- "value": "OAaNq3DX/15fGJP2MI6bujt1GRpvjwrqIevChirJsbc="
- },
- "voting_power": "50",
- "proposer_priority": "-50"
- }
- },
- "next_validator_set": {
- "validators": [
- {
- "address": "01F527D77D3FFCC4FCFF2DDC2952EEA5414F2A33",
- "pub_key": {
- "type": "tendermint/PubKeyEd25519",
- "value": "OAaNq3DX/15fGJP2MI6bujt1GRpvjwrqIevChirJsbc="
- },
- "voting_power": "50",
- "proposer_priority": "0"
- },
- {
- "address": "026CC7B6F3E62F789DBECEC59766888B5464737D",
- "pub_key": {
- "type": "tendermint/PubKeyEd25519",
- "value": "+vlsKpn6ojn+UoTZl+w+fxeqm6xvUfBokTcKfcG3au4="
- },
- "voting_power": "50",
- "proposer_priority": "0"
- }
- ],
- "proposer": {
- "address": "026CC7B6F3E62F789DBECEC59766888B5464737D",
- "pub_key": {
- "type": "tendermint/PubKeyEd25519",
- "value": "+vlsKpn6ojn+UoTZl+w+fxeqm6xvUfBokTcKfcG3au4="
- },
- "voting_power": "50",
- "proposer_priority": "0" }
- },
- "provider": "9D61B19DEFFD5A60BA844AF492EC2CC44449C569"
-}
-"#;
diff --git a/light-node/light_node.toml.example b/light-node/light_node.toml.example
new file mode 100644
index 000000000..0add345fc
--- /dev/null
+++ b/light-node/light_node.toml.example
@@ -0,0 +1,51 @@
+# Example light-node configuration file
+#
+# This is just an example for reference which can be used
+# against a locally running tendermint fullnode.
+
+# The fraction of the total voting power of a known
+# and trusted validator set is sufficient for a commit to be
+# accepted going forward.
+[trust_threshold]
+numerator = "1"
+denominator = "3"
+
+# The duration until we consider a trusted state as expired.
+[trusting_period]
+secs = 864000
+nanos = 0
+
+# Correction parameter dealing with only approximately synchronized clocks.
+# The local clock should always be ahead of timestamps from the blockchain; this
+# is the maximum amount that the local clock may drift behind a timestamp from the
+# blockchain.
+[clock_drift]
+secs = 5
+nanos = 0
+
+# rpc_config contains all configration options for the RPC server
+# of the light node as well as RPC client related options.
+#
+# - listen_addr: the address the RPC server will serve
+# - rpc_config.request_timeout: The duration after which any RPC request to tendermint node will time out.
+[rpc_config]
+listen_addr = "127.0.0.1:8888"
+
+[rpc_config.request_timeout]
+secs = 60
+nanos = 0
+
+# Actual light client configuration.
+# - address: Address of the Tendermint fullnode
+# to connect to and fetch LightBlock data from.
+# - peer_id: PeerID of the same fullnode.
+# - The data base folder for this instance's store.
+[[light_clients]]
+address = "tcp://127.0.0.1:26657"
+peer_id = "BADFADAD0BEFEEDC0C0ADEADBEEFC0FFEEFACADE"
+db_path = "./lightstore/BADFADAD0BEFEEDC0C0ADEADBEEFC0FFEEFACADE"
+
+[[light_clients]]
+address = "tcp://127.0.0.1:26657"
+peer_id = "CEFEEDBADFADAD0C0CEEFACADE0ADEADBEEFC0FF"
+db_path = "./lightstore/CEFEEDBADFADAD0C0CEEFACADE0ADEADBEEFC0FF"
diff --git a/light-node/src/bin/light_node/main.rs b/light-node/src/bin/light-node/main.rs
similarity index 100%
rename from light-node/src/bin/light_node/main.rs
rename to light-node/src/bin/light-node/main.rs
diff --git a/light-node/src/commands.rs b/light-node/src/commands.rs
index 2ee520dd8..206f90796 100644
--- a/light-node/src/commands.rs
+++ b/light-node/src/commands.rs
@@ -1,17 +1,19 @@
//! LightNode Subcommands
//!
//! The light client supports the following subcommands:
-//!
+//! - `initialize`: subjectively initializes the light node with a given height and hash
//! - `start`: launches the light client
//! - `version`: print application version
//!
//! See the `impl Configurable` below for how to specify the path to the
//! application's configuration file.
+mod initialize;
mod start;
mod version;
use self::{start::StartCmd, version::VersionCmd};
+use crate::commands::initialize::InitCmd;
use crate::config::LightNodeConfig;
use abscissa_core::{
config::Override, Command, Configurable, FrameworkError, Help, Options, Runnable,
@@ -28,11 +30,17 @@ pub enum LightNodeCmd {
#[options(help = "get usage information")]
Help(Help),
- /// `start` the light client
- #[options(help = "start the light client daemon with the given config or command line params")]
+ /// `intialize` the light node
+ #[options(
+ help = "subjectively initialize the light client with given subjective height and validator set hash"
+ )]
+ Initialize(InitCmd),
+
+ /// `start` the light node
+ #[options(help = "start the light node daemon with the given config or command line params")]
Start(StartCmd),
- /// `version` of the light client
+ /// `version` of the light node
#[options(help = "display version information")]
Version(VersionCmd),
}
diff --git a/light-node/src/commands/initialize.rs b/light-node/src/commands/initialize.rs
new file mode 100644
index 000000000..23f57d8ef
--- /dev/null
+++ b/light-node/src/commands/initialize.rs
@@ -0,0 +1,110 @@
+//! `intialize` subcommand
+
+use crate::application::app_config;
+use crate::config::LightClientConfig;
+
+use std::collections::HashMap;
+
+use abscissa_core::status_err;
+use abscissa_core::status_warn;
+use abscissa_core::Command;
+use abscissa_core::Options;
+use abscissa_core::Runnable;
+
+use tendermint::hash;
+use tendermint::lite::Header;
+use tendermint::Hash;
+
+use tendermint_light_client::components::io::{AtHeight, Io, ProdIo};
+use tendermint_light_client::operations::ProdHasher;
+use tendermint_light_client::predicates::{ProdPredicates, VerificationPredicates};
+use tendermint_light_client::store::sled::SledStore;
+use tendermint_light_client::store::LightStore;
+use tendermint_light_client::types::Status;
+
+/// `initialize` subcommand
+#[derive(Command, Debug, Default, Options)]
+pub struct InitCmd {
+ #[options(
+ free,
+ help = "subjective height of the initial trusted state to initialize the node with"
+ )]
+ pub height: u64,
+
+ #[options(
+ free,
+ help = "hash of the initial subjectively trusted header to initialize the node with"
+ )]
+ pub header_hash: String,
+}
+
+impl Runnable for InitCmd {
+ fn run(&self) {
+ let subjective_header_hash =
+ Hash::from_hex_upper(hash::Algorithm::Sha256, &self.header_hash).unwrap();
+ let app_cfg = app_config();
+
+ let lc = app_cfg.light_clients.first().unwrap();
+
+ let mut peer_map = HashMap::new();
+ peer_map.insert(lc.peer_id, lc.address.clone());
+
+ let io = ProdIo::new(peer_map, Some(app_cfg.rpc_config.request_timeout));
+
+ initialize_subjectively(self.height, subjective_header_hash, &lc, &io);
+ }
+}
+
+// TODO(ismail): sth along these lines should live in the light-client crate / library
+// instead of here.
+// TODO(ismail): additionally here and everywhere else, we should return errors
+// instead of std::process::exit because no destructors will be run.
+fn initialize_subjectively(
+ height: u64,
+ subjective_header_hash: Hash,
+ l_conf: &LightClientConfig,
+ io: &ProdIo,
+) {
+ let db = sled::open(l_conf.db_path.clone()).unwrap_or_else(|e| {
+ status_err!("could not open database: {}", e);
+ std::process::exit(1);
+ });
+
+ let mut light_store = SledStore::new(db);
+
+ if light_store.latest_trusted_or_verified().is_some() {
+ let lb = light_store.latest_trusted_or_verified().unwrap();
+ status_warn!(
+ "already existing trusted or verified state of height {} in database: {:?}",
+ lb.signed_header.header.height,
+ l_conf.db_path
+ );
+ }
+
+ let trusted_state = io
+ .fetch_light_block(l_conf.peer_id, AtHeight::At(height))
+ .unwrap_or_else(|e| {
+ status_err!("could not retrieve trusted header: {}", e);
+ std::process::exit(1);
+ });
+
+ let predicates = ProdPredicates;
+ let hasher = ProdHasher;
+ if let Err(err) = predicates.validator_sets_match(&trusted_state, &hasher) {
+ status_err!("invalid light block: {}", err);
+ std::process::exit(1);
+ }
+ // TODO(ismail): actually verify more predicates of light block before storing!?
+ let got_header_hash = trusted_state.signed_header.header.hash();
+ if got_header_hash != subjective_header_hash {
+ status_err!(
+ "received LightBlock's header hash: {} does not match the subjective hash: {}",
+ got_header_hash,
+ subjective_header_hash
+ );
+ std::process::exit(1);
+ }
+ // TODO(liamsi): it is unclear if this should be Trusted or only Verified
+ // - update the spec first and then use library method instead of this:
+ light_store.insert(trusted_state, Status::Verified);
+}
diff --git a/light-node/src/commands/start.rs b/light-node/src/commands/start.rs
index d4f89edd2..bc85d5578 100644
--- a/light-node/src/commands/start.rs
+++ b/light-node/src/commands/start.rs
@@ -1,100 +1,86 @@
//! `start` subcommand - start the light node.
-/// App-local prelude includes `app_reader()`/`app_writer()`/`app_config()`
-/// accessors along with logging macros. Customize as you see fit.
-use abscissa_core::{config, Command, FrameworkError, Options, Runnable};
use std::process;
-use std::time::{Duration, SystemTime};
-use tendermint::hash;
-use tendermint::lite;
-use tendermint::lite::error::Error;
-use tendermint::lite::ValidatorSet as _;
-use tendermint::lite::{Header, Height, Requester, TrustThresholdFraction};
-use tendermint::Hash;
-
-use tendermint_rpc as rpc;
-
-use crate::application::APPLICATION;
-use crate::config::LightNodeConfig;
-use crate::prelude::*;
-use crate::requester::RPCRequester;
-use crate::store::{MemStore, State};
+use crate::application::{app_config, APPLICATION};
+use crate::config::{LightClientConfig, LightNodeConfig};
+use crate::rpc;
+use crate::rpc::Server;
+
+use abscissa_core::config;
+use abscissa_core::path::PathBuf;
+use abscissa_core::status_err;
+use abscissa_core::status_info;
+use abscissa_core::Command;
+use abscissa_core::FrameworkError;
+use abscissa_core::Options;
+use abscissa_core::Runnable;
+
+use std::collections::HashMap;
+use std::net::SocketAddr;
+use std::ops::Deref;
+use std::time::Duration;
+
+use tendermint_light_client::components::clock::SystemClock;
+use tendermint_light_client::components::io::ProdIo;
+use tendermint_light_client::components::scheduler;
+use tendermint_light_client::components::verifier::ProdVerifier;
+use tendermint_light_client::evidence::ProdEvidenceReporter;
+use tendermint_light_client::fork_detector::ProdForkDetector;
+use tendermint_light_client::light_client;
+use tendermint_light_client::light_client::LightClient;
+use tendermint_light_client::peer_list::{PeerList, PeerListBuilder};
+use tendermint_light_client::state::State;
+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
///
-/// The `Options` proc macro generates an option parser based on the struct
-/// definition, and is defined in the `gumdrop` crate. See their documentation
-/// for a more comprehensive example:
-///
-///
#[derive(Command, Debug, Options)]
pub struct StartCmd {
- /// RPC address to request headers and validators from.
- #[options(free)]
- rpc_addr: String,
+ /// Path to configuration file
+ #[options(
+ short = "b",
+ long = "jsonrpc-server-addr",
+ help = "address the rpc server will bind to"
+ )]
+ pub listen_addr: Option,
+
+ /// Path to configuration file
+ #[options(short = "c", long = "config", help = "path to light_node.toml")]
+ pub config: Option,
}
impl Runnable for StartCmd {
/// Start the application.
fn run(&self) {
if let Err(err) = abscissa_tokio::run(&APPLICATION, async {
- let config = app_config();
+ StartCmd::assert_init_was_run();
+ let mut supervisor = self.construct_supervisor();
- let client = rpc::Client::new(config.rpc_address.parse().unwrap());
- let req = RPCRequester::new(client);
- let mut store = MemStore::new();
+ let rpc_handler = supervisor.handle();
+ StartCmd::start_rpc_server(rpc_handler);
- let vals_hash = Hash::from_hex_upper(
- hash::Algorithm::Sha256,
- &config.subjective_init.validators_hash,
- )
- .unwrap();
-
- println!("Requesting from {}.", config.rpc_address);
-
- subjective_init(config.subjective_init.height, vals_hash, &mut store, &req)
- .await
- .unwrap();
+ let handle = supervisor.handle();
+ std::thread::spawn(|| supervisor.run());
loop {
- let latest_sh = (&req).signed_header(0).await.unwrap();
- let latest_peer_height = latest_sh.header().height();
-
- let latest_trusted = store.get(0).unwrap();
- let latest_trusted_height = latest_trusted.last_header().header().height();
-
- // only bisect to higher heights
- if latest_peer_height <= latest_trusted_height {
- std::thread::sleep(Duration::new(1, 0));
- continue;
+ match handle.verify_to_highest() {
+ Ok(light_block) => {
+ status_info!("synced to block {}", light_block.height().to_string());
+ }
+ Err(err) => {
+ status_err!("sync failed: {}", err);
+ }
}
-
- println!(
- "attempting bisection from height {:?} to height {:?}",
- latest_trusted_height, latest_peer_height,
- );
-
- let now = SystemTime::now();
- lite::verify_bisection(
- latest_trusted.to_owned(),
- latest_peer_height,
- TrustThresholdFraction::default(), // TODO
- config.trusting_period,
- now,
- &req,
- )
- .await
- .unwrap();
-
- println!("Succeeded bisecting!");
-
- // notifications ?
-
- // sleep for a few secs ?
+ // TODO(liamsi): use ticks and make this configurable:
+ std::thread::sleep(Duration::from_millis(800));
}
}) {
- eprintln!("Error while running application: {}", err);
+ status_err!("Unexpected error while running application: {}", err);
process::exit(1);
}
}
@@ -108,53 +94,102 @@ impl config::Override for StartCmd {
&self,
mut config: LightNodeConfig,
) -> Result {
- if !self.rpc_addr.is_empty() {
- config.rpc_address = self.rpc_addr.to_owned();
+ // TODO(liamsi): figure out if other options would be reasonable to overwrite via CLI arguments.
+ if let Some(addr) = self.listen_addr {
+ config.rpc_config.listen_addr = addr;
}
-
Ok(config)
}
}
-
-/*
- * The following is initialization logic that should have a
- * function in the lite crate like:
- * `subjective_init(height, vals_hash, store, requester) -> Result<(), Error`
- * it would fetch the initial header/vals from the requester and populate a
- * trusted state and store it in the store ...
- * TODO: this should take traits ... but how to deal with the State ?
- * TODO: better name ?
- */
-async fn subjective_init(
- height: Height,
- vals_hash: Hash,
- store: &mut MemStore,
- req: &RPCRequester,
-) -> Result<(), Error> {
- if store.get(height).is_ok() {
- // we already have this !
- return Ok(());
+impl StartCmd {
+ fn assert_init_was_run() {
+ // TODO(liamsi): handle errors properly:
+ let primary_db_path = app_config().light_clients.first().unwrap().db_path.clone();
+ let db = sled::open(primary_db_path).unwrap_or_else(|e| {
+ status_err!("could not open database: {}", e);
+ std::process::exit(1);
+ });
+
+ let primary_store = SledStore::new(db);
+
+ if primary_store.latest_trusted_or_verified().is_none() {
+ status_err!("no trusted or verified state in store for primary, please initialize with the `initialize` subcommand first");
+ std::process::exit(1);
+ }
}
+ // TODO: this should do proper error handling, be gerneralized
+ // then moved to to the light-client crate.
+ fn make_instance(
+ &self,
+ light_config: &LightClientConfig,
+ io: ProdIo,
+ options: light_client::Options,
+ ) -> Instance {
+ let peer_id = light_config.peer_id;
+ let db_path = light_config.db_path.clone();
- // check that the val hash matches
- let vals = req.validator_set(height).await?;
+ let db = sled::open(db_path).unwrap_or_else(|e| {
+ status_err!("could not open database: {}", e);
+ std::process::exit(1);
+ });
- if vals.hash() != vals_hash {
- // TODO
- panic!("vals hash dont match")
- }
+ let light_store = SledStore::new(db);
- let signed_header = req.signed_header(height).await?;
+ let state = State {
+ light_store: Box::new(light_store),
+ verification_trace: HashMap::new(),
+ };
- // TODO: validate signed_header.commit() with the vals ...
+ let verifier = ProdVerifier::default();
+ let clock = SystemClock;
+ let scheduler = scheduler::basic_bisecting_schedule;
- let next_vals = req.validator_set(height + 1).await?;
+ let light_client = LightClient::new(peer_id, options, clock, scheduler, verifier, io);
- // TODO: check next_vals ...
+ Instance::new(light_client, state)
+ }
- let trusted_state = &State::new(signed_header, next_vals);
+ fn start_rpc_server(h: H)
+ where
+ H: Handle + Send + Sync + 'static,
+ {
+ let server = Server::new(h);
+ 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()));
+ }
+}
- store.add(trusted_state.to_owned())?;
+impl StartCmd {
+ fn construct_supervisor(&self) -> Supervisor {
+ // TODO(ismail): we need to verify the addr <-> peerId mappings somewhere!
+ let mut peer_map = HashMap::new();
+ for light_conf in &app_config().light_clients {
+ peer_map.insert(light_conf.peer_id, light_conf.address.clone());
+ }
+ let io = ProdIo::new(
+ peer_map.clone(),
+ Some(app_config().rpc_config.request_timeout),
+ );
+ let conf = app_config().deref().clone();
+ let options: light_client::Options = conf.into();
+
+ let mut peer_list: PeerListBuilder = PeerList::builder();
+ for (i, light_conf) in app_config().light_clients.iter().enumerate() {
+ let instance = self.make_instance(light_conf, io.clone(), options);
+ if i == 0 {
+ // primary instance
+ peer_list = peer_list.primary(instance.light_client.peer, instance);
+ } else {
+ peer_list = peer_list.witness(instance.light_client.peer, instance);
+ }
+ }
+ let peer_list = peer_list.build();
- Ok(())
+ Supervisor::new(
+ peer_list,
+ ProdForkDetector::default(),
+ ProdEvidenceReporter::new(peer_map.clone()),
+ )
+ }
}
diff --git a/light-node/src/config.rs b/light-node/src/config.rs
index 63ac9bbb0..bf2ac1944 100644
--- a/light-node/src/config.rs
+++ b/light-node/src/config.rs
@@ -4,55 +4,101 @@
//! application's configuration file and/or command-line options
//! for specifying it.
+use abscissa_core::path::PathBuf;
use serde::{Deserialize, Serialize};
+use std::net::SocketAddr;
use std::time::Duration;
+use tendermint_light_client::light_client;
+use tendermint_light_client::types::{PeerId, TrustThreshold};
+
/// LightNode Configuration
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct LightNodeConfig {
- /// RPC address to request headers and validators from.
- pub rpc_address: String,
+ /// The fraction of the total voting power of a known
+ /// and trusted validator set is sufficient for a commit to be
+ /// accepted going forward.
+ pub trust_threshold: TrustThreshold,
/// The duration until we consider a trusted state as expired.
pub trusting_period: Duration,
- /// Subjective initialization.
- pub subjective_init: SubjectiveInit,
+ /// Correction parameter dealing with only approximately synchronized clocks.
+ pub clock_drift: Duration,
+
+ /// RPC related config parameters.
+ pub rpc_config: RpcConfig,
+
+ // TODO "now" should probably always be passed in as `Time::now()`
+ /// The actual light client instances' configuration.
+ /// Note: the first config will be used in the subjectively initialize
+ /// the light node in the `initialize` subcommand.
+ pub light_clients: Vec,
}
-/// Default configuration settings.
-///
-/// Note: if your needs are as simple as below, you can
-/// use `#[derive(Default)]` on LightNodeConfig instead.
-impl Default for LightNodeConfig {
+/// LightClientConfig contains all options of a light client instance.
+#[derive(Clone, Debug, Deserialize, Serialize)]
+#[serde(deny_unknown_fields)]
+pub struct LightClientConfig {
+ /// Address of the Tendermint fullnode to connect to and
+ /// fetch LightBlock data from.
+ pub address: tendermint::net::Address,
+ /// PeerID of the same Tendermint fullnode.
+ pub peer_id: PeerId,
+ /// The data base folder for this instance's store.
+ pub db_path: PathBuf,
+}
+
+/// RpcConfig contains for the RPC server of the light node as
+/// well as RPC client related options.
+#[derive(Clone, Debug, Deserialize, Serialize)]
+#[serde(deny_unknown_fields)]
+pub struct RpcConfig {
+ /// The address the RPC server will serve.
+ pub listen_addr: SocketAddr,
+ /// The duration after which any RPC request to tendermint node will time out.
+ pub request_timeout: Duration,
+}
+
+/// Default light client config settings.
+impl Default for LightClientConfig {
fn default() -> Self {
Self {
- rpc_address: "localhost:26657".to_owned(),
- trusting_period: Duration::new(6000, 0),
- subjective_init: SubjectiveInit::default(),
+ address: "tcp://127.0.0.1:26657".parse().unwrap(),
+ peer_id: "BADFADAD0BEFEEDC0C0ADEADBEEFC0FFEEFACADE".parse().unwrap(),
+ db_path: "./lightstore/BADFADAD0BEFEEDC0C0ADEADBEEFC0FFEEFACADE"
+ .parse()
+ .unwrap(),
}
}
}
-/// Configuration for subjective initialization.
-///
-/// Contains the subjective height and validators hash (as a string formatted as hex).
-#[derive(Clone, Debug, Deserialize, Serialize)]
-#[serde(deny_unknown_fields)]
-pub struct SubjectiveInit {
- /// Subjective height.
- pub height: u64,
- /// Subjective validators hash.
- pub validators_hash: String,
+/// Default configuration settings.
+impl Default for LightNodeConfig {
+ fn default() -> Self {
+ Self {
+ trusting_period: Duration::from_secs(864_000), // 60*60*24*10
+ trust_threshold: TrustThreshold {
+ numerator: 1,
+ denominator: 3,
+ },
+ clock_drift: Duration::from_secs(1),
+ rpc_config: RpcConfig {
+ listen_addr: "127.0.0.1:8888".parse().unwrap(),
+ request_timeout: Duration::from_secs(60),
+ },
+ // TODO(ismail): need at least 2 peers for a proper init
+ // otherwise the light node will complain on `start` with `no witness left`
+ light_clients: vec![LightClientConfig::default()],
+ }
+ }
}
-impl Default for SubjectiveInit {
- fn default() -> Self {
+impl From for light_client::Options {
+ fn from(lnc: LightNodeConfig) -> Self {
Self {
- height: 1,
- // TODO(liamsi): a default hash here does not make sense unless it is a valid hash
- // from a public network
- validators_hash: "A5A7DEA707ADE6156F8A981777CA093F178FC790475F6EC659B6617E704871DD"
- .to_owned(),
+ trust_threshold: lnc.trust_threshold,
+ trusting_period: lnc.trusting_period,
+ clock_drift: lnc.clock_drift,
}
}
}
diff --git a/light-node/tests/acceptance.rs b/light-node/tests/acceptance.rs
index 315e8ab27..071311d58 100644
--- a/light-node/tests/acceptance.rs
+++ b/light-node/tests/acceptance.rs
@@ -21,8 +21,6 @@
use abscissa_core::testing::prelude::*;
use once_cell::sync::Lazy;
-use tendermint_light_node::config::LightNodeConfig;
-
/// Executes your application binary via `cargo run`.
///
/// Storing this value as a [`Lazy`] static ensures that all instances of
@@ -45,46 +43,21 @@ fn start_no_args() {
#[test]
#[ignore]
fn start_with_args() {
- let mut runner = RUNNER.clone();
- let mut cmd = runner
- .args(&["start", "acceptance", "test"])
- .capture_stdout()
- .run();
-
- cmd.stdout().expect_line("Hello, acceptance test!");
- cmd.wait().unwrap().expect_success();
+ todo!()
}
/// Use configured value
#[test]
#[ignore]
fn start_with_config_no_args() {
- let mut config = LightNodeConfig::default();
- config.rpc_address = "localhost:26657".to_owned();
- let expected_line = format!("Requesting from {}.", &config.rpc_address);
-
- let mut runner = RUNNER.clone();
- let mut cmd = runner.config(&config).arg("start").capture_stdout().run();
- cmd.stdout().expect_line(&expected_line);
- cmd.wait().unwrap().expect_success();
+ todo!()
}
/// Override configured value with command-line argument
#[test]
#[ignore]
fn start_with_config_and_args() {
- let mut config = LightNodeConfig::default();
- config.rpc_address = "localhost:26657".to_owned();
-
- let mut runner = RUNNER.clone();
- let mut cmd = runner
- .config(&config)
- .args(&["start", "other:26657"])
- .capture_stdout()
- .run();
-
- cmd.stdout().expect_line("Requesting from other:26657.");
- cmd.wait().unwrap().expect_success();
+ todo!()
}
/// Example of a test which matches a regular expression