diff --git a/lite-node/src/commands/start.rs b/lite-node/src/commands/start.rs index 6a97033c6..05fc16482 100644 --- a/lite-node/src/commands/start.rs +++ b/lite-node/src/commands/start.rs @@ -4,8 +4,24 @@ /// accessors along with logging macros. Customize as you see fit. use crate::prelude::*; +use core::future::Future; +use tendermint::hash; +use tendermint::lite; +use tendermint::lite::{Error, Header, Requester, SignedHeader, Store, TrustedState}; +use tendermint::rpc; +use tendermint::{block::Height, Hash}; +use tokio::runtime::Builder; + +use tendermint::lite::ValidatorSet as _; + use crate::config::LiteNodeConfig; +use crate::requester::RPCRequester; +use crate::state::State; +use crate::store::MemStore; +use crate::threshold::TrustThresholdOneThird; + use abscissa_core::{config, Command, FrameworkError, Options, Runnable}; +use std::time::{Duration, SystemTime}; /// `start` subcommand /// @@ -16,17 +32,82 @@ use abscissa_core::{config, Command, FrameworkError, Options, Runnable}; /// #[derive(Command, Debug, Options)] pub struct StartCmd { - /// To whom are we saying hello? + /// RPC address to request headers and validators from. #[options(free)] - recipient: Vec, + rpc_addr: String, } +// TODO: this should also somehow be configurable ... +// we can't simply add this as a field in the config because this either would +// be a trait (`TrustThreshold`) or immediately and impl thereof (`TrustThresholdOneThird`). +static THRESHOLD: &TrustThresholdOneThird = &TrustThresholdOneThird {}; + impl Runnable for StartCmd { /// Start the application. fn run(&self) { let config = app_config(); - // TODO(liamsi): light client loop goes here... - println!("Hello, {}!", &config.hello.recipient); + + let client = block_on(rpc::Client::new(&config.rpc_address.parse().unwrap())).unwrap(); + let req = RPCRequester::new(client); + let mut store = MemStore::new(); + + let vals_hash = Hash::from_hex_upper( + hash::Algorithm::Sha256, + &config.subjective_init.validators_hash, + ) + .unwrap(); + + println!("Requesting from {}.", config.rpc_address); + + subjective_init( + Height::from(config.subjective_init.height), + vals_hash, + &mut store, + &req, + ) + .unwrap(); + + loop { + let latest = (&req).signed_header(0).unwrap(); + let latest_peer_height = latest.header().height(); + + let latest = store.get(Height::from(0)).unwrap(); + let latest_height = latest.last_header().header().height(); + + // only bisect to higher heights + if latest_peer_height <= latest_height { + std::thread::sleep(Duration::new(1, 0)); + continue; + } + + println!( + "attempting bisection from height {:?} to height {:?}", + store + .get(Height::from(0)) + .unwrap() + .last_header() + .header() + .height(), + latest_peer_height, + ); + + let now = &SystemTime::now(); + lite::verify_and_update_bisection( + latest_peer_height, + THRESHOLD, // TODO + &config.trusting_period, + now, + &req, + &mut store, + ) + .unwrap(); + + println!("Succeeded bisecting!"); + + // notifications ? + + // sleep for a few secs ? + } } } @@ -38,10 +119,62 @@ impl config::Override for StartCmd { &self, mut config: LiteNodeConfig, ) -> Result { - if !self.recipient.is_empty() { - config.hello.recipient = self.recipient.join(" "); + if !self.rpc_addr.is_empty() { + config.rpc_address = self.rpc_addr.to_owned(); } 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 ? +*/ +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(()); + } + + // check that the val hash matches + let vals = req.validator_set(height)?; + + if vals.hash() != vals_hash { + // TODO + panic!("vals hash dont match") + } + + let signed_header = req.signed_header(height)?; + + // TODO: validate signed_header.commit() with the vals ... + + let next_vals = req.validator_set(height.increment())?; + + // TODO: check next_vals ... + + let trusted_state = &State::new(&signed_header, &next_vals); + + store.add(trusted_state)?; + + Ok(()) +} + +fn block_on(future: F) -> F::Output { + Builder::new() + .basic_scheduler() + .enable_all() + .build() + .unwrap() + .block_on(future) +} diff --git a/lite-node/src/config.rs b/lite-node/src/config.rs index fef4f73c0..9ae811c56 100644 --- a/lite-node/src/config.rs +++ b/lite-node/src/config.rs @@ -5,13 +5,20 @@ //! for specifying it. use serde::{Deserialize, Serialize}; +use std::time::Duration; + +use tendermint::lite::TrustThreshold; /// LiteNode Configuration #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(deny_unknown_fields)] pub struct LiteNodeConfig { - /// An example configuration section - pub hello: ExampleSection, + /// RPC address to request headers and validators from. + pub rpc_address: String, + /// The duration until we consider a trusted state as expired. + pub trusting_period: Duration, + /// Subjective initialization. + pub subjective_init: SubjectiveInit, } /// Default configuration settings. @@ -21,25 +28,31 @@ pub struct LiteNodeConfig { impl Default for LiteNodeConfig { fn default() -> Self { Self { - hello: ExampleSection::default(), + rpc_address: "localhost:26657".to_owned(), + trusting_period: Duration::new(6000, 0), + subjective_init: SubjectiveInit::default(), } } } -/// Example configuration section. +/// Configuration for subjective initialization. /// -/// Delete this and replace it with your actual configuration structs. +/// Contains the subjective height and validators hash (as a string formatted as hex). #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(deny_unknown_fields)] -pub struct ExampleSection { - /// Example configuration value - pub recipient: String, +pub struct SubjectiveInit { + /// Subjective height. + pub height: u64, + /// Subjective validators hash. + pub validators_hash: String, } -impl Default for ExampleSection { +impl Default for SubjectiveInit { fn default() -> Self { Self { - recipient: "world".to_owned(), + height: 1, + validators_hash: "A5A7DEA707ADE6156F8A981777CA093F178FC790475F6EC659B6617E704871DD" + .to_owned(), } } } diff --git a/lite-node/src/lib.rs b/lite-node/src/lib.rs index b53680cb3..ba32ce846 100644 --- a/lite-node/src/lib.rs +++ b/lite-node/src/lib.rs @@ -20,3 +20,8 @@ pub mod commands; pub mod config; pub mod error; pub mod prelude; + +pub mod requester; +pub mod state; +pub mod store; +pub mod threshold; diff --git a/lite-node/src/requester.rs b/lite-node/src/requester.rs new file mode 100644 index 000000000..b7ffdb042 --- /dev/null +++ b/lite-node/src/requester.rs @@ -0,0 +1,83 @@ +use tendermint::block; +use tendermint::lite; +use tendermint::rpc; +use tendermint::validator; + +use core::future::Future; +use tokio::runtime::Builder; + +/// RPCRequester wraps the Tendermint rpc::Client. +pub struct RPCRequester { + client: rpc::Client, +} + +impl RPCRequester { + pub fn new(client: rpc::Client) -> Self { + RPCRequester { client } + } +} + +impl lite::types::Requester for RPCRequester { + type SignedHeader = block::signed_header::SignedHeader; + type ValidatorSet = validator::Set; + + /// Request the signed header at height h. + /// If h==0, request the latest signed header. + /// TODO: use an enum instead of h==0. + fn signed_header(&self, h: H) -> Result + where + H: Into, + { + let height: block::Height = h.into(); + let r = match height.value() { + 0 => block_on(self.client.latest_commit()), + _ => block_on(self.client.commit(height)), + }; + match r { + Ok(response) => Ok(response.signed_header), + Err(_error) => Err(lite::Error::RequestFailed), + } + } + + /// Request the validator set at height h. + fn validator_set(&self, h: H) -> Result + where + H: Into, + { + let r = block_on(self.client.validators(h)); + match r { + Ok(response) => Ok(validator::Set::new(response.validators)), + Err(_error) => Err(lite::Error::RequestFailed), + } + } +} + +pub fn block_on(future: F) -> F::Output { + Builder::new() + .basic_scheduler() + .enable_all() + .build() + .unwrap() + .block_on(future) +} + +#[cfg(test)] +mod tests { + use super::*; + use tendermint::lite::types::Header as LiteHeader; + use tendermint::lite::types::Requester as LiteRequester; + use tendermint::lite::types::SignedHeader as LiteSignedHeader; + use tendermint::lite::types::ValidatorSet as LiteValSet; + use tendermint::rpc; + + // TODO: integration test + #[test] + #[ignore] + fn test_val_set() { + let client = block_on(rpc::Client::new(&"localhost:26657".parse().unwrap())).unwrap(); + let req = RPCRequester::new(client); + let r1 = req.validator_set(5).unwrap(); + let r2 = req.signed_header(5).unwrap(); + assert_eq!(r1.hash(), r2.header().validators_hash()); + } +} diff --git a/lite-node/src/state.rs b/lite-node/src/state.rs new file mode 100644 index 000000000..e1601080e --- /dev/null +++ b/lite-node/src/state.rs @@ -0,0 +1,30 @@ +use tendermint::lite::TrustedState; +use tendermint::{block::signed_header::SignedHeader, validator::Set}; + +#[derive(Clone)] +pub struct State { + last_header: SignedHeader, + vals: Set, +} + +impl TrustedState for State { + type LastHeader = SignedHeader; + type ValidatorSet = Set; + + fn new(last_header: &Self::LastHeader, vals: &Self::ValidatorSet) -> Self { + State { + last_header: last_header.clone(), + vals: vals.clone(), + } + } + + // height H-1 + fn last_header(&self) -> &Self::LastHeader { + &self.last_header + } + + // height H + fn validators(&self) -> &Self::ValidatorSet { + &self.vals + } +} diff --git a/lite-node/src/store.rs b/lite-node/src/store.rs new file mode 100644 index 000000000..cbebe6697 --- /dev/null +++ b/lite-node/src/store.rs @@ -0,0 +1,42 @@ +use crate::state::State; +use tendermint::block::Height; +use tendermint::lite::{Error, Header, SignedHeader, Store, TrustedState}; + +use std::collections::HashMap; + +#[derive(Default)] +pub struct MemStore { + height: Height, + store: HashMap, +} + +impl MemStore { + pub fn new() -> MemStore { + MemStore { + height: Height::from(0), + store: HashMap::new(), + } + } +} + +impl Store for MemStore { + type TrustedState = State; + + fn add(&mut self, trusted: &Self::TrustedState) -> Result<(), Error> { + let height = trusted.last_header().header().height(); + self.height = height; + self.store.insert(height, trusted.clone()); + Ok(()) + } + + fn get(&self, h: Height) -> Result<&Self::TrustedState, Error> { + let mut height = h; + if h.value() == 0 { + height = self.height + } + match self.store.get(&height) { + Some(state) => Ok(state), + None => Err(Error::RequestFailed), + } + } +} diff --git a/lite-node/src/threshold.rs b/lite-node/src/threshold.rs new file mode 100644 index 000000000..b1978fa73 --- /dev/null +++ b/lite-node/src/threshold.rs @@ -0,0 +1,11 @@ +use tendermint::lite::TrustThreshold; + +pub struct TrustThresholdOneThird {} +impl TrustThreshold for TrustThresholdOneThird {} + +pub struct TrustThresholdTwoThirds {} +impl TrustThreshold for TrustThresholdTwoThirds { + fn is_enough_power(&self, signed_voting_power: u64, total_voting_power: u64) -> bool { + signed_voting_power * 3 > total_voting_power * 2 + } +} diff --git a/lite-node/tests/acceptance.rs b/lite-node/tests/acceptance.rs index 024378ff6..12257128f 100644 --- a/lite-node/tests/acceptance.rs +++ b/lite-node/tests/acceptance.rs @@ -32,15 +32,17 @@ pub static RUNNER: Lazy = Lazy::new(CmdRunner::default); /// Use `LiteNodeConfig::default()` value if no config or args #[test] +#[ignore] fn start_no_args() { let mut runner = RUNNER.clone(); let mut cmd = runner.arg("start").capture_stdout().run(); - cmd.stdout().expect_line("Hello, world!"); + cmd.stdout().expect_line(""); cmd.wait().unwrap().expect_success(); } /// Use command-line argument value #[test] +#[ignore] fn start_with_args() { let mut runner = RUNNER.clone(); let mut cmd = runner @@ -54,10 +56,11 @@ fn start_with_args() { /// Use configured value #[test] +#[ignore] fn start_with_config_no_args() { let mut config = LiteNodeConfig::default(); - config.hello.recipient = "configured recipient".to_owned(); - let expected_line = format!("Hello, {}!", &config.hello.recipient); + 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(); @@ -67,18 +70,19 @@ fn start_with_config_no_args() { /// Override configured value with command-line argument #[test] +#[ignore] fn start_with_config_and_args() { let mut config = LiteNodeConfig::default(); - config.hello.recipient = "configured recipient".to_owned(); + config.rpc_address = "localhost:26657".to_owned(); let mut runner = RUNNER.clone(); let mut cmd = runner .config(&config) - .args(&["start", "acceptance", "test"]) + .args(&["start", "other:26657"]) .capture_stdout() .run(); - cmd.stdout().expect_line("Hello, acceptance test!"); + cmd.stdout().expect_line("Requesting from other:26657."); cmd.wait().unwrap().expect_success(); }