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