diff --git a/Cargo.toml b/Cargo.toml index 08269b4ce..95befcacc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,5 +2,5 @@ members = [ "tendermint", - "tendermint-lite", + "light-node", ] diff --git a/docs/architecture/adr-003-light-client-core-verification.md b/docs/architecture/adr-003-light-client-core-verification.md index 4da85d560..1e085a3ff 100644 --- a/docs/architecture/adr-003-light-client-core-verification.md +++ b/docs/architecture/adr-003-light-client-core-verification.md @@ -241,6 +241,8 @@ where } ``` +In practice, this can be implemented as a Tendermint RPC client making requests +to the `/commit` and `/validators` endpoints of full nodes. For testing, the Requester can be implemented by JSON files. ### Verification diff --git a/docs/architecture/adr-004-light-client-cli.md b/docs/architecture/adr-004-light-client-cli.md index 3229ef7d8..50c4d0ea1 100644 --- a/docs/architecture/adr-004-light-client-cli.md +++ b/docs/architecture/adr-004-light-client-cli.md @@ -2,40 +2,67 @@ ## Changelog -2020-01-22: Some content copied from old ADR-002 +- 2020-02-09: Update about Abscissa +- 2020-01-22: Some content copied from old ADR-002 ## Status -WIP. Just copied over from old ADR. Needs rework +WIP. ## Context +The high level context for the light client is described in +[ADR-002](adr-002-light-client-adr-index.md). -### State +For reference, a schematic of the light node is below: + +![Light Node Diagram](assets/light-node.png). + +Here we focus on how the Light Node process itself is composed. +The light node process must consider the following features: + +- command line UX and flags +- config file +- logging +- error handling +- state management +- exposing RPC servers + +Ideally, it can support all of this with a minimum of dependencies. + +We'd like to be able to start a light node process and have it sync to the +latest height and stay synced while it runs. + +## Decision + +### Abscissa + +[Abscissa](https://github.com/iqlusioninc/abscissa) is a framework for building CLI +tools in Rust by Tony Arcieri of Iqlusion. +It's focus is on security and minimizing dependencies. +The full list of dependencies can be found [here](https://github.com/iqlusioninc/abscissa#depencencies). + +For instance, while it includes functionality for command-line option parsing like that +provided by `structopt` + `clap`, it does so with far less dependencies. + +[Users](https://github.com/iqlusioninc/abscissa#projects-using-abscissa) +of note include the [Tendermint KMS](https://github.com/tendermint/kms) +for validators and the new +[Zebra ZCash full node](https://github.com/ZcashFoundation/zebra). -The light node state contains the following: +See the [introductory blog +post](https://iqlusion.blog/introducing-abscissa-rust-application-framework) +for more details. -- current height (H) - height for the next header we want to verify -- last header (H-1) - the last header we verified -- current validators (H) - validators for the height we want to verify (including all validator pubkeys and voting powers) +### Config -It also includes some configuration, which contains: +Config includes: - trusting period - initial list of full nodes - method (sequential or skipping) - trust level (if method==skipping) -The node is initialized with a trusted header for some height H-1 -(call this header[H-1]), and a validator set for height H (call this vals[H]). - -The node may be initialized by the user with only a height and header hash, and -proceed to request the full header and validator set from a full node. This -reduces the initialization burden on the user, and simplifies passing this -information into the process, but for the state to be properly initialized it -will need to get the correct header and validator set before starting the light -client syncing protocol. - The configuration contains an initial list of full nodes (peers). For the sake of simplicity, one of the peers is selected as the "primary", while the rest are considered "backups". Most of the data is downloaded from the primary, @@ -46,75 +73,19 @@ the time from the trusted header is greater than a configurable "trusting period". If at any point the state is expired, the node should log an error and exit - it's needs to be manually reset. -### Syncer - -The Syncing co-ordinates the syncing and is the highest level component. -We consider two approaches to syncing the light node: sequential and skipping. - -#### Sequential Sync -Inital state: +### Initialization - - time T - - height H - - header[H-1] - - vals[H] +The node is initialized with a trusted header for some height and a validator set for the next height. -Here we describe the happy path: - -1) Request header[H], commit[H], and vals[H+1] from the primary, and check that they are well formed and from the correct height -2) Pass header[H], commit[H], vals[H], and vals[H+1] to the verification library, which will: - - - check that vals[H] and vals[H+1] are correctly reflected in header[H] - - check that commit[H] is for header[H] - - check that +2/3 of the validators correctly signed the hash of header[H] - -3) Request header[H] from each of the backups and check that they match header[H] received from the primary -4) Update the state with header[H] and vals[H+1], and increment H -5) return to (1) - -If (1) or (2) fails, mark the primary as bad and select a new peer to be the -primary. - -If (3) returns a conflicting header, verify the header by requesting the -corresponding commit and running the verification of (2). If the verification -passes, there is a fork, and evidence should be published so the validators get -slashed. We leave the mechanics of evidence to a future document. For now, the -light client will just log an error and exit. If the verification fails, it -means the backup that provided the conflict is bad and should be removed. - -#### Skipping Sync - -Skipping sync is essentially the same as sequential, except for a few points: - -- instead of verifying sequential headers, we attempt to "skip" ahead to the - full node's most recent height -- skipping is only permitted if the validator set has not changed too much - ie. - if +1/3 of the last trusted validator set has signed the commit for the height we're attempting to skip to -- if the validator set changes too much, we "bisect" the height space, - attempting to skip to a lower height, recursively. -- in the worst case, the bisection takes us to a sequential height - -### Requester - -The requester is simply a Tendermint RPC client. It makes requests to full -nodes. It uses the `/commit` and `/validators` endpoints to get signed headers -and validator sets for relevant heights. It may also use the `/status` endpoint -to get the latest height of the full node (for skipping verification). It -uses the following trait (see below for definitions of the referenced types): - -```rust -pub trait Requester { - type SignedHeader: SignedHeader; - type ValidatorSet: ValidatorSet; - - fn signed_header(&self, h: H) -> Result - where H: Into; - - fn validator_set(&self, h: H) -> Result - where H: Into; -} -``` +The node may be initialized by the user with only a height and header hash, and +proceed to request the full header and validator set from a full node. This +reduces the initialization burden on the user, and simplifies passing this +information into the process, but for the state to be properly initialized it +will need to get the correct header and validator set before starting the light +client syncing protocol. -Note that trait uses `Into` which is a common idiom for the codebase. +### State +The light node will need to maintain state including the current height, the +last verified and trusted header, and the current set of trusted validators. diff --git a/docs/architecture/adr-005-light-client-fork-detection.md b/docs/architecture/adr-005-light-client-fork-detection.md index a2dc140fd..3030278d6 100644 --- a/docs/architecture/adr-005-light-client-fork-detection.md +++ b/docs/architecture/adr-005-light-client-fork-detection.md @@ -45,3 +45,47 @@ one correct full node in order to detect conflicts in a timely fashion. We keep this mechanism simple for now, but in the future a more advanced peer discovery mechanism may be utilized. + +#### Sequential Sync + +Inital state: + + - time T + - height H + - header[H-1] + - vals[H] + +Here we describe the happy path: + +1) Request header[H], commit[H], and vals[H+1] from the primary, and check that they are well formed and from the correct height +2) Pass header[H], commit[H], vals[H], and vals[H+1] to the verification library, which will: + + - check that vals[H] and vals[H+1] are correctly reflected in header[H] + - check that commit[H] is for header[H] + - check that +2/3 of the validators correctly signed the hash of header[H] + +3) Request header[H] from each of the backups and check that they match header[H] received from the primary +4) Update the state with header[H] and vals[H+1], and increment H +5) return to (1) + +If (1) or (2) fails, mark the primary as bad and select a new peer to be the +primary. + +If (3) returns a conflicting header, verify the header by requesting the +corresponding commit and running the verification of (2). If the verification +passes, there is a fork, and evidence should be published so the validators get +slashed. We leave the mechanics of evidence to a future document. For now, the +light client will just log an error and exit. If the verification fails, it +means the backup that provided the conflict is bad and should be removed. + +#### Skipping Sync + +Skipping sync is essentially the same as sequential, except for a few points: + +- instead of verifying sequential headers, we attempt to "skip" ahead to the + full node's most recent height +- skipping is only permitted if the validator set has not changed too much - ie. + if +1/3 of the last trusted validator set has signed the commit for the height we're attempting to skip to +- if the validator set changes too much, we "bisect" the height space, + attempting to skip to a lower height, recursively. +- in the worst case, the bisection takes us to a sequential height diff --git a/light-node/.gitignore b/light-node/.gitignore new file mode 100644 index 000000000..53eaa2196 --- /dev/null +++ b/light-node/.gitignore @@ -0,0 +1,2 @@ +/target +**/*.rs.bk diff --git a/light-node/Cargo.toml b/light-node/Cargo.toml new file mode 100644 index 000000000..85d8fab51 --- /dev/null +++ b/light-node/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "light_node" +authors = ["Ethan Buchman ", "Ismail Khoffi "] +version = "0.1.0" +edition = "2018" + +[dependencies] +gumdrop = "0.7" +serde = { version = "1", features = ["serde_derive"] } +tendermint = { version = "0.12.0-rc0", path = "../tendermint" } +async-trait = "0.1" +tokio = { version = "0.2", features = ["full"] } +abscissa_tokio = "0.5" + +[dependencies.abscissa_core] +version = "0.5.0" +# optional: use `gimli` to capture backtraces +# see https://github.com/rust-lang/backtrace-rs/issues/189 +# features = ["gimli-backtrace"] + +[dev-dependencies] +abscissa_core = { version = "0.5.0", features = ["testing"] } +once_cell = "1.2" + diff --git a/light-node/README.md b/light-node/README.md new file mode 100644 index 000000000..544af9540 --- /dev/null +++ b/light-node/README.md @@ -0,0 +1,14 @@ +# LightNode + +Tendermint light client node. + +## Getting Started + +This application is authored using [Abscissa], a Rust application framework. + +For more information, see: + +[Documentation] + +[Abscissa]: https://github.com/iqlusioninc/abscissa +[Documentation]: https://docs.rs/abscissa_core/ diff --git a/light-node/src/application.rs b/light-node/src/application.rs new file mode 100644 index 000000000..1ab05ac9e --- /dev/null +++ b/light-node/src/application.rs @@ -0,0 +1,111 @@ +//! LightNode Abscissa Application + +use crate::{commands::LightNodeCmd, config::LightNodeConfig}; +use abscissa_core::{ + application::{self, AppCell}, + config, trace, Application, EntryPoint, FrameworkError, StandardPaths, +}; +use abscissa_tokio::TokioComponent; + +/// Application state +pub static APPLICATION: AppCell = AppCell::new(); + +/// Obtain a read-only (multi-reader) lock on the application state. +/// +/// Panics if the application state has not been initialized. +pub fn app_reader() -> application::lock::Reader { + APPLICATION.read() +} + +/// Obtain an exclusive mutable lock on the application state. +pub fn app_writer() -> application::lock::Writer { + APPLICATION.write() +} + +/// Obtain a read-only (multi-reader) lock on the application configuration. +/// +/// Panics if the application configuration has not been loaded. +pub fn app_config() -> config::Reader { + config::Reader::new(&APPLICATION) +} + +/// LightNode Application +#[derive(Debug)] +pub struct LightNodeApp { + /// Application configuration. + config: Option, + + /// Application state. + state: application::State, +} + +/// Initialize a new application instance. +/// +/// By default no configuration is loaded, and the framework state is +/// initialized to a default, empty state (no components, threads, etc). +impl Default for LightNodeApp { + fn default() -> Self { + Self { + config: None, + state: application::State::default(), + } + } +} + +impl Application for LightNodeApp { + /// Entrypoint command for this application. + type Cmd = EntryPoint; + + /// Application configuration. + type Cfg = LightNodeConfig; + + /// Paths to resources within the application. + type Paths = StandardPaths; + + /// Accessor for application configuration. + fn config(&self) -> &LightNodeConfig { + self.config.as_ref().expect("config not loaded") + } + + /// Borrow the application state immutably. + fn state(&self) -> &application::State { + &self.state + } + + /// Borrow the application state mutably. + fn state_mut(&mut self) -> &mut application::State { + &mut self.state + } + + /// Register all components used by this application. + /// + /// If you would like to add additional components to your application + /// beyond the default ones provided by the framework, this is the place + /// to do so. + fn register_components(&mut self, command: &Self::Cmd) -> Result<(), FrameworkError> { + let mut components = self.framework_components(command)?; + components.push(Box::new(TokioComponent::new()?)); + self.state.components.register(components) + } + + /// Post-configuration lifecycle callback. + /// + /// Called regardless of whether config is loaded to indicate this is the + /// time in app lifecycle when configuration would be loaded if + /// possible. + fn after_config(&mut self, config: Self::Cfg) -> Result<(), FrameworkError> { + // Configure components + self.state.components.after_config(&config)?; + self.config = Some(config); + Ok(()) + } + + /// Get tracing configuration from command-line options + fn tracing_config(&self, command: &EntryPoint) -> trace::Config { + if command.verbose { + trace::Config::verbose() + } else { + trace::Config::default() + } + } +} diff --git a/light-node/src/bin/light_node/main.rs b/light-node/src/bin/light_node/main.rs new file mode 100644 index 000000000..253ff7cc5 --- /dev/null +++ b/light-node/src/bin/light_node/main.rs @@ -0,0 +1,11 @@ +//! Main entry point for LightNode + +#![deny(warnings, missing_docs, trivial_casts, unused_qualifications)] +#![forbid(unsafe_code)] + +use light_node::application::APPLICATION; + +/// Boot LightNode +fn main() { + abscissa_core::boot(&APPLICATION); +} diff --git a/light-node/src/commands.rs b/light-node/src/commands.rs new file mode 100644 index 000000000..2ee520dd8 --- /dev/null +++ b/light-node/src/commands.rs @@ -0,0 +1,67 @@ +//! LightNode Subcommands +//! +//! The light client supports the following subcommands: +//! +//! - `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 start; +mod version; + +use self::{start::StartCmd, version::VersionCmd}; +use crate::config::LightNodeConfig; +use abscissa_core::{ + config::Override, Command, Configurable, FrameworkError, Help, Options, Runnable, +}; +use std::path::PathBuf; + +/// LightNode Configuration Filename +pub const CONFIG_FILE: &str = "light_node.toml"; + +/// LightNode Subcommands +#[derive(Command, Debug, Options, Runnable)] +pub enum LightNodeCmd { + /// The `help` subcommand + #[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")] + Start(StartCmd), + + /// `version` of the light client + #[options(help = "display version information")] + Version(VersionCmd), +} + +/// This trait allows you to define how application configuration is loaded. +impl Configurable for LightNodeCmd { + /// Location of the configuration file + fn config_path(&self) -> Option { + // Check if the config file exists, and if it does not, ignore it. + // If you'd like for a missing configuration file to be a hard error + // instead, always return `Some(CONFIG_FILE)` here. + let filename = PathBuf::from(CONFIG_FILE); + + if filename.exists() { + Some(filename) + } else { + None + } + } + + /// Apply changes to the config after it's been loaded, e.g. overriding + /// values in a config file using command-line options. + /// + /// This can be safely deleted if you don't want to override config + /// settings from command-line options. + fn process_config(&self, config: LightNodeConfig) -> Result { + match self { + LightNodeCmd::Start(cmd) => cmd.override_config(config), + _ => Ok(config), + } + } +} diff --git a/light-node/src/commands/start.rs b/light-node/src/commands/start.rs new file mode 100644 index 000000000..a0961825d --- /dev/null +++ b/light-node/src/commands/start.rs @@ -0,0 +1,159 @@ +//! `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 crate::prelude::*; + +use tendermint::hash; +use tendermint::lite; +use tendermint::lite::ValidatorSet as _; +use tendermint::lite::{Header, Height, Requester, TrustThresholdFraction}; +use tendermint::rpc; +use tendermint::Hash; + +use crate::application::APPLICATION; +use crate::config::LightNodeConfig; +use crate::requester::RPCRequester; +use crate::store::{MemStore, State}; +use abscissa_core::{config, Command, FrameworkError, Options, Runnable}; +use std::process; +use std::time::{Duration, SystemTime}; +use tendermint::lite::error::Error; + +/// `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, +} + +impl Runnable for StartCmd { + /// Start the application. + fn run(&self) { + if let Err(err) = abscissa_tokio::run(&APPLICATION, async { + let config = app_config(); + + let client = rpc::Client::new(config.rpc_address.parse().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(config.subjective_init.height, vals_hash, &mut store, &req) + .await + .unwrap(); + + 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; + } + + 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 ? + } + }) { + eprintln!("Error while running application: {}", err); + process::exit(1); + } + } +} + +impl config::Override for StartCmd { + // Process the given command line options, overriding settings from + // a configuration file using explicit flags taken from command-line + // arguments. + fn override_config( + &self, + mut config: LightNodeConfig, + ) -> Result { + 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 ? +*/ +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(()); + } + + // check that the val hash matches + let vals = req.validator_set(height).await?; + + if vals.hash() != vals_hash { + // TODO + panic!("vals hash dont match") + } + + let signed_header = req.signed_header(height).await?; + + // TODO: validate signed_header.commit() with the vals ... + + let next_vals = req.validator_set(height + 1).await?; + + // TODO: check next_vals ... + + let trusted_state = &State::new(signed_header, next_vals); + + store.add(trusted_state.to_owned())?; + + Ok(()) +} diff --git a/light-node/src/commands/version.rs b/light-node/src/commands/version.rs new file mode 100644 index 000000000..1c141eeb7 --- /dev/null +++ b/light-node/src/commands/version.rs @@ -0,0 +1,17 @@ +//! `version` subcommand + +#![allow(clippy::never_loop)] + +use super::LightNodeCmd; +use abscissa_core::{Command, Options, Runnable}; + +/// `version` subcommand +#[derive(Command, Debug, Default, Options)] +pub struct VersionCmd {} + +impl Runnable for VersionCmd { + /// Print version message + fn run(&self) { + println!("{} {}", LightNodeCmd::name(), LightNodeCmd::version()); + } +} diff --git a/light-node/src/config.rs b/light-node/src/config.rs new file mode 100644 index 000000000..63ac9bbb0 --- /dev/null +++ b/light-node/src/config.rs @@ -0,0 +1,58 @@ +//! LightNode Config +//! +//! See instructions in `commands.rs` to specify the path to your +//! application's configuration file and/or command-line options +//! for specifying it. + +use serde::{Deserialize, Serialize}; +use std::time::Duration; + +/// 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 duration until we consider a trusted state as expired. + pub trusting_period: Duration, + /// Subjective initialization. + pub subjective_init: SubjectiveInit, +} + +/// Default configuration settings. +/// +/// Note: if your needs are as simple as below, you can +/// use `#[derive(Default)]` on LightNodeConfig instead. +impl Default for LightNodeConfig { + fn default() -> Self { + Self { + rpc_address: "localhost:26657".to_owned(), + trusting_period: Duration::new(6000, 0), + subjective_init: SubjectiveInit::default(), + } + } +} + +/// 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, +} + +impl Default for SubjectiveInit { + fn default() -> 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(), + } + } +} diff --git a/light-node/src/error.rs b/light-node/src/error.rs new file mode 100644 index 000000000..d6713c5bf --- /dev/null +++ b/light-node/src/error.rs @@ -0,0 +1,80 @@ +//! Error types + +use abscissa_core::error::{BoxError, Context}; +use std::{ + fmt::{self, Display}, + io, + ops::Deref, +}; + +/// Kinds of errors +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum ErrorKind { + /// Error in configuration file + Config, + + /// Input/output error + Io, +} + +impl ErrorKind { + /// Create an error context from this error + pub fn context(self, source: impl Into) -> Context { + Context::new(self, Some(source.into())) + } +} + +impl Display for ErrorKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let description = match self { + ErrorKind::Config => "config error", + ErrorKind::Io => "I/O error", + }; + + f.write_str(description) + } +} + +impl std::error::Error for ErrorKind {} + +/// Error type +#[derive(Debug)] +pub struct Error(Box>); + +impl Deref for Error { + type Target = Context; + + fn deref(&self) -> &Context { + &self.0 + } +} + +impl Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + self.0.source() + } +} + +impl From for Error { + fn from(kind: ErrorKind) -> Self { + Context::new(kind, None).into() + } +} + +impl From> for Error { + fn from(context: Context) -> Self { + Error(Box::new(context)) + } +} + +impl From for Error { + fn from(err: io::Error) -> Self { + ErrorKind::Io.context(err).into() + } +} diff --git a/light-node/src/lib.rs b/light-node/src/lib.rs new file mode 100644 index 000000000..22abf3d77 --- /dev/null +++ b/light-node/src/lib.rs @@ -0,0 +1,25 @@ +//! LightNode +//! +//! Application based on the [Abscissa] framework. +//! +//! [Abscissa]: https://github.com/iqlusioninc/abscissa + +// Tip: Deny warnings with `RUSTFLAGS="-D warnings"` environment variable in CI + +#![forbid(unsafe_code)] +#![warn( + rust_2018_idioms, + trivial_casts, + unused_lifetimes, + unused_qualifications +)] +// TODO(ismail): add proper docs and remove this! +#![allow(missing_docs)] + +pub mod application; +pub mod commands; +pub mod config; +pub mod error; +pub mod prelude; +pub mod requester; +pub mod store; diff --git a/light-node/src/prelude.rs b/light-node/src/prelude.rs new file mode 100644 index 000000000..b22d0225d --- /dev/null +++ b/light-node/src/prelude.rs @@ -0,0 +1,9 @@ +//! Application-local prelude: conveniently import types/functions/macros +//! which are generally useful and should be available in every module with +//! `use crate::prelude::*; + +/// Abscissa core prelude +pub use abscissa_core::prelude::*; + +/// Application state accessors +pub use crate::application::{app_config, app_reader, app_writer}; diff --git a/tendermint-lite/src/requester.rs b/light-node/src/requester.rs similarity index 100% rename from tendermint-lite/src/requester.rs rename to light-node/src/requester.rs diff --git a/tendermint-lite/src/store.rs b/light-node/src/store.rs similarity index 100% rename from tendermint-lite/src/store.rs rename to light-node/src/store.rs diff --git a/light-node/tests/acceptance.rs b/light-node/tests/acceptance.rs new file mode 100644 index 000000000..c112e7525 --- /dev/null +++ b/light-node/tests/acceptance.rs @@ -0,0 +1,95 @@ +//! Acceptance test: runs the application as a subprocess and asserts its +//! output for given argument combinations matches what is expected. +//! +//! Modify and/or delete these as you see fit to test the specific needs of +//! your application. +//! +//! For more information, see: +//! + +// Tip: Deny warnings with `RUSTFLAGS="-D warnings"` environment variable in CI + +#![forbid(unsafe_code)] +#![warn( + missing_docs, + rust_2018_idioms, + trivial_casts, + unused_lifetimes, + unused_qualifications +)] + +use abscissa_core::testing::prelude::*; +use light_node::config::LightNodeConfig; +use once_cell::sync::Lazy; + +/// Executes your application binary via `cargo run`. +/// +/// Storing this value as a [`Lazy`] static ensures that all instances of +/// the runner acquire a mutex when executing commands and inspecting +/// exit statuses, serializing what would otherwise be multithreaded +/// invocations as `cargo test` executes tests in parallel by default. +pub static RUNNER: Lazy = Lazy::new(CmdRunner::default); + +/// Use `LightNodeConfig::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(""); + 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 + .args(&["start", "acceptance", "test"]) + .capture_stdout() + .run(); + + cmd.stdout().expect_line("Hello, acceptance test!"); + cmd.wait().unwrap().expect_success(); +} + +/// 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(); +} + +/// 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(); +} + +/// Example of a test which matches a regular expression +#[test] +fn version_no_args() { + let mut runner = RUNNER.clone(); + let mut cmd = runner.arg("version").capture_stdout().run(); + cmd.stdout().expect_regex(r"\A\w+ [\d\.\-]+\z"); +} diff --git a/tendermint-lite/Cargo.toml b/tendermint-lite/Cargo.toml index c3da3f89b..e69de29bb 100644 --- a/tendermint-lite/Cargo.toml +++ b/tendermint-lite/Cargo.toml @@ -1,13 +0,0 @@ -[package] -name = "tendermint-lite" -version = "0.1.0" -authors = ["Ethan Buchman "] -edition = "2018" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -tendermint = { path = "../tendermint" } - -async-trait = "0.1" -tokio = { version = "0.2", features = ["full"] } diff --git a/tendermint-lite/src/lib.rs b/tendermint-lite/src/lib.rs deleted file mode 100644 index 226aebff2..000000000 --- a/tendermint-lite/src/lib.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod requester; -pub mod store; diff --git a/tendermint-lite/src/main.rs b/tendermint-lite/src/main.rs index 13d53e271..e69de29bb 100644 --- a/tendermint-lite/src/main.rs +++ b/tendermint-lite/src/main.rs @@ -1,131 +0,0 @@ -use tendermint::hash; -use tendermint::lite; -use tendermint::lite::TrustThresholdFraction; -use tendermint::lite::{Header as _, Requester as _, ValidatorSet as _}; -use tendermint::rpc; -use tendermint::{block::Height, Hash}; - -use tendermint_lite::{requester::RPCRequester, store::MemStore}; - -use std::time::{Duration, SystemTime}; -use tendermint::lite::error::Error; -use tendermint_lite::store::State; - -// TODO: these should be config/args -static SUBJECTIVE_HEIGHT: u64 = 1; -static SUBJECTIVE_VALS_HASH_HEX: &str = - "A5A7DEA707ADE6156F8A981777CA093F178FC790475F6EC659B6617E704871DD"; -static RPC_ADDR: &str = "localhost:26657"; - -#[tokio::main] -async fn main() { - // TODO: this should be config - let trusting_period = Duration::new(6000, 0); - - // setup requester for primary peer - let client = rpc::Client::new(RPC_ADDR.parse().unwrap()); - - if let Err(err) = client.health().await { - eprintln!("error: health check failed: {}", err); - std::process::exit(1); - } - - let req = RPCRequester::new(client); - let mut store = MemStore::new(); - - let vals_hash = - Hash::from_hex_upper(hash::Algorithm::Sha256, SUBJECTIVE_VALS_HASH_HEX).unwrap(); - - subjective_init(Height::from(SUBJECTIVE_HEIGHT), vals_hash, &mut store, &req) - .await - .unwrap(); - - loop { - let latest = (&req).signed_header(0).await.unwrap(); - let latest_peer_height = latest.header().height(); - - let latest = store.get(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(0).unwrap().last_header().header().height(), - latest_peer_height, - ); - - let now = SystemTime::now(); - let trusted_state = store.get(0).expect("can not read trusted state"); - - let new_states = lite::verify_bisection( - trusted_state.clone(), - latest_peer_height, - TrustThresholdFraction::default(), - trusting_period, - now, - &req, - ) - .await - .unwrap(); - - for new_state in new_states { - store - .add(new_state) - .expect("couldn't store new trusted state"); - } - - println!("Succeeded bisecting!"); - - // notifications ? - - // sleep for a few secs ? - } -} - -/* - * 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.value()).is_ok() { - // we already have this ! - return Ok(()); - } - - // check that the val hash matches - let vals = req.validator_set(height.value()).await?; - - if vals.hash() != vals_hash { - // TODO - panic!("vals hash dont match") - } - - let signed_header = req.signed_header(SUBJECTIVE_HEIGHT).await?; - - // TODO: validate signed_header.commit() with the vals ... - - let next_vals = req.validator_set(height.increment().value()).await?; - - // TODO: check next_vals ... - - let trusted_state = State::new(signed_header, next_vals); - - store.add(trusted_state)?; - - Ok(()) -} diff --git a/tendermint/src/abci/transaction.rs b/tendermint/src/abci/transaction.rs index 690a02eb9..d5f078b31 100644 --- a/tendermint/src/abci/transaction.rs +++ b/tendermint/src/abci/transaction.rs @@ -90,6 +90,6 @@ impl Data { impl AsRef<[Transaction]> for Data { fn as_ref(&self) -> &[Transaction] { - self.txs.as_ref().map(Vec::as_slice).unwrap_or_else(|| &[]) + self.txs.as_deref().unwrap_or_else(|| &[]) } } diff --git a/tendermint/src/evidence.rs b/tendermint/src/evidence.rs index ff0d81d73..69486152f 100644 --- a/tendermint/src/evidence.rs +++ b/tendermint/src/evidence.rs @@ -80,10 +80,7 @@ impl Data { impl AsRef<[Evidence]> for Data { fn as_ref(&self) -> &[Evidence] { - self.evidence - .as_ref() - .map(Vec::as_slice) - .unwrap_or_else(|| &[]) + self.evidence.as_deref().unwrap_or_else(|| &[]) } }