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();
}