From 34d7d02a4cf8e182fb723ce73880a7d946d21b67 Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Wed, 30 Sep 2020 18:59:36 +0200 Subject: [PATCH] Implement builder DSL for light client initialization, and other things (#583) * Add Send + Sync bound on light client Handle * Remove default implementation on Handle trait * Whitespace * Fix light-node code after Handle update * Remove unused mut qualifier after Supervisor update * Derive Clone on SupervisorHandle * Add DSL for building supervisor and light clients * Refactor Io component * Refactor Supervisor builder * Re-add a TODO that got lost in the refactor * Document builders * Simplify return types of supervisor builder methods * Update light client example to use builders * Perform more validation when doing subjective initialization * Add option to use trusted state alrady in store to light client builder * Use builders in light node start command * Improve API of PeerListBuilder * Finish adapting start command to use supervisor builder * Fix WASM build * Expose a couple functions on the supervisor * Block on async tasks in a new thread to avoid nesting Tokio runtimes * Cleanup * Nest std_ext module under utils * Feature-guard `block_on` for WASM * Fix typo in comment Co-authored-by: Thane Thomson * Ensure that at least one witness is provided in the SupervisorBuilder * Rename SupervisorBuilder::unwrap to inner to express is infallability * Remove LightClientBuilder::trust_primary_latest since we do not need it and it is quite dangerous * Make `LightClientBuilder::trust_light_block` method private * Perform more thorough validation of the trusted light block in the light client builder * Update light client integration test to use builders Co-authored-by: Thane Thomson --- light-client/examples/light_client.rs | 118 +++++-------- light-client/src/builder.rs | 9 + light-client/src/builder/error.rs | 58 ++++++ light-client/src/builder/light_client.rs | 216 +++++++++++++++++++++++ light-client/src/builder/supervisor.rs | 120 +++++++++++++ light-client/src/components/io.rs | 114 ++++-------- light-client/src/evidence.rs | 15 +- light-client/src/lib.rs | 5 +- light-client/src/light_client.rs | 23 ++- light-client/src/peer_list.rs | 50 +++--- light-client/src/store.rs | 2 +- light-client/src/supervisor.rs | 35 ++-- light-client/src/tests.rs | 3 +- light-client/src/utils.rs | 8 + light-client/src/utils/block_on.rs | 29 +++ light-client/src/{ => utils}/std_ext.rs | 0 light-client/tests/integration.rs | 96 +++++----- light-client/tests/light_client.rs | 4 +- light-client/tests/supervisor.rs | 10 +- light-node/src/commands/initialize.rs | 113 ++++++------ light-node/src/commands/start.rs | 160 ++++++++--------- light-node/src/rpc.rs | 16 ++ 22 files changed, 794 insertions(+), 410 deletions(-) create mode 100644 light-client/src/builder.rs create mode 100644 light-client/src/builder/error.rs create mode 100644 light-client/src/builder/light_client.rs create mode 100644 light-client/src/builder/supervisor.rs create mode 100644 light-client/src/utils.rs create mode 100644 light-client/src/utils/block_on.rs rename light-client/src/{ => utils}/std_ext.rs (100%) diff --git a/light-client/examples/light_client.rs b/light-client/examples/light_client.rs index 93f2b2023..df037a4c0 100644 --- a/light-client/examples/light_client.rs +++ b/light-client/examples/light_client.rs @@ -1,26 +1,20 @@ -use std::collections::HashMap; use std::{ path::{Path, PathBuf}, time::Duration, }; +use anomaly::BoxError; use gumdrop::Options; -use tendermint_light_client::supervisor::{Handle as _, Instance, Supervisor}; +use tendermint::Hash; +use tendermint_rpc as rpc; + +use tendermint_light_client::supervisor::{Handle as _, Instance}; use tendermint_light_client::{ - components::{ - clock::SystemClock, - io::{AtHeight, Io, ProdIo}, - scheduler, - verifier::ProdVerifier, - }, - evidence::ProdEvidenceReporter, - fork_detector::ProdForkDetector, - light_client::{self, LightClient}, - peer_list::PeerList, - state::State, - store::{sled::SledStore, LightStore}, - types::{Height, PeerId, Status, TrustThreshold}, + builder::{LightClientBuilder, SupervisorBuilder}, + light_client, + store::sled::SledStore, + types::{Height, PeerId, TrustThreshold}, }; #[derive(Debug, Options)] @@ -55,6 +49,11 @@ struct SyncOpts { meta = "HEIGHT" )] trusted_height: Option, + #[options( + help = "hash of the initial trusted state (optional if store already initialized)", + meta = "HASH" + )] + trusted_hash: Option, #[options( help = "path to the database folder", meta = "PATH", @@ -65,6 +64,7 @@ struct SyncOpts { fn main() { let opts = CliOptions::parse_args_default_or_exit(); + match opts.command { None => { eprintln!("Please specify a command:"); @@ -72,7 +72,10 @@ fn main() { eprintln!("{}\n", CliOptions::usage()); std::process::exit(1); } - Some(Command::Sync(sync_opts)) => sync_cmd(sync_opts), + Some(Command::Sync(sync_opts)) => sync_cmd(sync_opts).unwrap_or_else(|e| { + eprintln!("Command failed: {}", e); + std::process::exit(1); + }), } } @@ -81,83 +84,46 @@ fn make_instance( addr: tendermint::net::Address, db_path: impl AsRef, opts: &SyncOpts, -) -> Instance { - let mut peer_map = HashMap::new(); - peer_map.insert(peer_id, addr); - - let timeout = Duration::from_secs(10); - let io = ProdIo::new(peer_map, Some(timeout)); - - let db = sled::open(db_path).unwrap_or_else(|e| { - println!("[ error ] could not open database: {}", e); - std::process::exit(1); - }); - - let mut light_store = SledStore::new(db); - - if let Some(height) = opts.trusted_height { - let trusted_state = io - .fetch_light_block(peer_id, AtHeight::At(height)) - .unwrap_or_else(|e| { - println!("[ error ] could not retrieve trusted header: {}", e); - std::process::exit(1); - }); - - light_store.insert(trusted_state, Status::Verified); - } else if light_store.latest(Status::Verified).is_none() { - println!("[ error ] no trusted state in database, please specify a trusted header"); - std::process::exit(1); - } - - let state = State { - light_store: Box::new(light_store), - verification_trace: HashMap::new(), - }; +) -> Result { + let db = sled::open(db_path)?; + let light_store = SledStore::new(db); + let rpc_client = rpc::HttpClient::new(addr).unwrap(); let options = light_client::Options { - trust_threshold: TrustThreshold { - numerator: 1, - denominator: 3, - }, + trust_threshold: TrustThreshold::default(), trusting_period: Duration::from_secs(36000), clock_drift: Duration::from_secs(1), }; - let verifier = ProdVerifier::default(); - let clock = SystemClock; - let scheduler = scheduler::basic_bisecting_schedule; + let builder = + LightClientBuilder::prod(peer_id, rpc_client, Box::new(light_store), options, None); - let light_client = LightClient::new(peer_id, options, clock, scheduler, verifier, io); + let builder = if let (Some(height), Some(hash)) = (opts.trusted_height, opts.trusted_hash) { + builder.trust_primary_at(height, hash) + } else { + builder.trust_from_store() + }?; - Instance::new(light_client, state) + Ok(builder.build()) } -fn sync_cmd(opts: SyncOpts) { - let addr = opts.address.clone(); - +fn sync_cmd(opts: SyncOpts) -> Result<(), BoxError> { let primary: PeerId = "BADFADAD0BEFEEDC0C0ADEADBEEFC0FFEEFACADE".parse().unwrap(); let witness: PeerId = "CEFEEDBADFADAD0C0CEEFACADE0ADEADBEEFC0FF".parse().unwrap(); + let primary_addr = opts.address.clone(); + let witness_addr = opts.address.clone(); + let primary_path = opts.db_path.join(primary.to_string()); let witness_path = opts.db_path.join(witness.to_string()); - let primary_instance = make_instance(primary, addr.clone(), primary_path, &opts); - let witness_instance = make_instance(witness, addr.clone(), witness_path, &opts); - - let mut peer_addr = HashMap::new(); - peer_addr.insert(primary, addr.clone()); - peer_addr.insert(witness, addr); - - let peer_list = PeerList::builder() - .primary(primary, primary_instance) - .witness(witness, witness_instance) - .build(); + let primary_instance = make_instance(primary, primary_addr.clone(), primary_path, &opts)?; + let witness_instance = make_instance(witness, witness_addr.clone(), witness_path, &opts)?; - let mut supervisor = Supervisor::new( - peer_list, - ProdForkDetector::default(), - ProdEvidenceReporter::new(peer_addr), - ); + let supervisor = SupervisorBuilder::new() + .primary(primary, primary_addr, primary_instance) + .witness(witness, witness_addr, witness_instance) + .build_prod(); let handle = supervisor.handle(); diff --git a/light-client/src/builder.rs b/light-client/src/builder.rs new file mode 100644 index 000000000..2a3636046 --- /dev/null +++ b/light-client/src/builder.rs @@ -0,0 +1,9 @@ +//! DSL for building light clients and supervisor + +mod light_client; +pub use light_client::LightClientBuilder; + +mod supervisor; +pub use supervisor::SupervisorBuilder; + +pub mod error; diff --git a/light-client/src/builder/error.rs b/light-client/src/builder/error.rs new file mode 100644 index 000000000..a77a80885 --- /dev/null +++ b/light-client/src/builder/error.rs @@ -0,0 +1,58 @@ +//! Errors raised by the builder DSL + +use anomaly::BoxError; +use anomaly::Context; +use tendermint::block::Height; +use tendermint::Hash; +use thiserror::Error; + +use crate::components::io::IoError; + +/// An error raised by the builder +pub type Error = anomaly::Error; + +/// The various error kinds raised by the builder +#[derive(Debug, Clone, Error, PartialEq)] +pub enum Kind { + /// I/O error + #[error("I/O error: {0}")] + Io(#[from] IoError), + + /// Height mismatch + #[error("height mismatch: given = {given}, found = {found}")] + HeightMismatch { + /// Height of trusted header + given: Height, + /// Height of fetched header + found: Height, + }, + + /// Hash mismatch + #[error("hash mismatch: given = {given}, found = {found}")] + HashMismatch { + /// Hash of trusted header + given: Hash, + /// hash of fetched header + found: Hash, + }, + + /// Invalid light block + #[error("invalid light block")] + InvalidLightBlock, + + /// No trusted state as found in the store + #[error("no trusted state in store")] + NoTrustedStateInStore, + + /// An empty witness list was given + #[error("empty witness list")] + EmptyWitnessList, +} + +impl Kind { + /// Add additional context (i.e. include a source error and capture a backtrace). + /// You can convert the resulting `Context` into an `Error` by calling `.into()`. + pub fn context(self, source: impl Into) -> Context { + Context::new(self, Some(source.into())) + } +} diff --git a/light-client/src/builder/light_client.rs b/light-client/src/builder/light_client.rs new file mode 100644 index 000000000..fdb1091b8 --- /dev/null +++ b/light-client/src/builder/light_client.rs @@ -0,0 +1,216 @@ +//! DSL for building a light client [`Instance`] + +use tendermint::{block::Height, Hash}; + +use crate::bail; +use crate::builder::error::{self, Error}; +use crate::components::clock::Clock; +use crate::components::io::{AtHeight, Io}; +use crate::components::scheduler::Scheduler; +use crate::components::verifier::Verifier; +use crate::light_client::{LightClient, Options}; +use crate::operations::Hasher; +use crate::predicates::VerificationPredicates; +use crate::state::{State, VerificationTrace}; +use crate::store::LightStore; +use crate::supervisor::Instance; +use crate::types::{LightBlock, PeerId, Status}; + +#[cfg(feature = "rpc-client")] +use { + crate::components::clock::SystemClock, crate::components::io::ProdIo, + crate::components::scheduler, crate::components::verifier::ProdVerifier, + crate::operations::ProdHasher, crate::predicates::ProdPredicates, std::time::Duration, + tendermint_rpc as rpc, +}; + +/// No trusted state has been set yet +pub struct NoTrustedState; + +/// A trusted state has been set and validated +pub struct HasTrustedState; + +/// Builder for a light client [`Instance`] +#[must_use] +pub struct LightClientBuilder { + peer_id: PeerId, + options: Options, + io: Box, + clock: Box, + hasher: Box, + verifier: Box, + scheduler: Box, + predicates: Box, + light_store: Box, + + #[allow(dead_code)] + state: State, +} + +impl LightClientBuilder { + /// Private method to move from one state to another + fn with_state(self, state: Next) -> LightClientBuilder { + LightClientBuilder { + peer_id: self.peer_id, + options: self.options, + io: self.io, + clock: self.clock, + hasher: self.hasher, + verifier: self.verifier, + scheduler: self.scheduler, + predicates: self.predicates, + light_store: self.light_store, + state, + } + } +} + +impl LightClientBuilder { + /// Initialize a builder for a production (non-mock) light client. + #[cfg(feature = "rpc-client")] + pub fn prod( + peer_id: PeerId, + rpc_client: rpc::HttpClient, + light_store: Box, + options: Options, + timeout: Option, + ) -> Self { + Self::custom( + peer_id, + options, + light_store, + Box::new(ProdIo::new(peer_id, rpc_client, timeout)), + Box::new(ProdHasher), + Box::new(SystemClock), + Box::new(ProdVerifier::default()), + Box::new(scheduler::basic_bisecting_schedule), + Box::new(ProdPredicates), + ) + } + + /// Initialize a builder for a custom light client, by providing all dependencies upfront. + #[allow(clippy::too_many_arguments)] + pub fn custom( + peer_id: PeerId, + options: Options, + light_store: Box, + io: Box, + hasher: Box, + clock: Box, + verifier: Box, + scheduler: Box, + predicates: Box, + ) -> Self { + Self { + peer_id, + hasher, + io, + verifier, + light_store, + clock, + scheduler, + options, + predicates, + state: NoTrustedState, + } + } + + /// Set the given light block as the initial trusted state. + fn trust_light_block( + mut self, + trusted_state: LightBlock, + ) -> Result, Error> { + self.validate(&trusted_state)?; + + // TODO(liamsi, romac): it is unclear if this should be Trusted or only Verified + self.light_store.insert(trusted_state, Status::Trusted); + + Ok(self.with_state(HasTrustedState)) + } + + /// Keep using the latest verified or trusted block in the light store. + /// Such a block must exists otherwise this will fail. + pub fn trust_from_store(self) -> Result, Error> { + let trusted_state = self + .light_store + .latest_trusted_or_verified() + .ok_or_else(|| error::Kind::NoTrustedStateInStore)?; + + self.trust_light_block(trusted_state) + } + + /// Set the block from the primary peer at the given height as the trusted state. + pub fn trust_primary_at( + self, + trusted_height: Height, + trusted_hash: Hash, + ) -> Result, Error> { + let trusted_state = self + .io + .fetch_light_block(AtHeight::At(trusted_height)) + .map_err(error::Kind::Io)?; + + if trusted_state.height() != trusted_height { + bail!(error::Kind::HeightMismatch { + given: trusted_height, + found: trusted_state.height(), + }); + } + + let header_hash = self.hasher.hash_header(&trusted_state.signed_header.header); + + if header_hash != trusted_hash { + bail!(error::Kind::HashMismatch { + given: trusted_hash, + found: header_hash, + }); + } + + self.trust_light_block(trusted_state) + } + + fn validate(&self, light_block: &LightBlock) -> Result<(), Error> { + let header = &light_block.signed_header.header; + let now = self.clock.now(); + + self.predicates + .is_within_trust_period(header, self.options.trusting_period, now) + .map_err(|e| error::Kind::InvalidLightBlock.context(e))?; + + self.predicates + .is_header_from_past(header, self.options.clock_drift, now) + .map_err(|e| error::Kind::InvalidLightBlock.context(e))?; + + self.predicates + .validator_sets_match(light_block, &*self.hasher) + .map_err(|e| error::Kind::InvalidLightBlock.context(e))?; + + self.predicates + .next_validators_match(light_block, &*self.hasher) + .map_err(|e| error::Kind::InvalidLightBlock.context(e))?; + + Ok(()) + } +} + +impl LightClientBuilder { + /// Build the light client [`Instance`]. + #[must_use] + pub fn build(self) -> Instance { + let state = State { + light_store: self.light_store, + verification_trace: VerificationTrace::new(), + }; + + let light_client = LightClient::from_boxed( + self.peer_id, + self.options, + self.clock, + self.scheduler, + self.verifier, + self.io, + ); + + Instance::new(light_client, state) + } +} diff --git a/light-client/src/builder/supervisor.rs b/light-client/src/builder/supervisor.rs new file mode 100644 index 000000000..f6f34c5f8 --- /dev/null +++ b/light-client/src/builder/supervisor.rs @@ -0,0 +1,120 @@ +use tendermint::net; + +use crate::builder::error::{self, Error}; +use crate::peer_list::{PeerList, PeerListBuilder}; +use crate::supervisor::Instance; +use crate::types::PeerId; + +#[cfg(feature = "rpc-client")] +use { + crate::evidence::ProdEvidenceReporter, crate::fork_detector::ProdForkDetector, + crate::supervisor::Supervisor, +}; + +pub struct Init; +pub struct HasPrimary; +pub struct Done; + +/// Builder for the [`Supervisor`] +#[must_use] +pub struct SupervisorBuilder { + instances: PeerListBuilder, + addresses: PeerListBuilder, + #[allow(dead_code)] + state: State, +} + +impl SupervisorBuilder { + /// Private method to move from one state to another + fn with_state(self, state: Next) -> SupervisorBuilder { + SupervisorBuilder { + instances: self.instances, + addresses: self.addresses, + state, + } + } +} + +impl Default for SupervisorBuilder { + fn default() -> Self { + Self::new() + } +} + +impl SupervisorBuilder { + /// Create an empty builder + pub fn new() -> Self { + Self { + instances: PeerListBuilder::default(), + addresses: PeerListBuilder::default(), + state: Init, + } + } + + /// Set the primary [`Instance`]. + pub fn primary( + mut self, + peer_id: PeerId, + address: net::Address, + instance: Instance, + ) -> SupervisorBuilder { + self.instances.primary(peer_id, instance); + self.addresses.primary(peer_id, address); + + self.with_state(HasPrimary) + } +} + +impl SupervisorBuilder { + /// Add a witness [`Instance`]. + pub fn witness( + mut self, + peer_id: PeerId, + address: net::Address, + instance: Instance, + ) -> SupervisorBuilder { + self.instances.witness(peer_id, instance); + self.addresses.witness(peer_id, address); + + self.with_state(Done) + } + + /// Add multiple witnesses at once. + pub fn witnesses( + mut self, + witnesses: impl IntoIterator, + ) -> Result, Error> { + let mut iter = witnesses.into_iter().peekable(); + if iter.peek().is_none() { + return Err(error::Kind::EmptyWitnessList.into()); + } + + for (peer_id, address, instance) in iter { + self.instances.witness(peer_id, instance); + self.addresses.witness(peer_id, address); + } + + Ok(self.with_state(Done)) + } +} + +impl SupervisorBuilder { + /// Build a production (non-mock) [`Supervisor`]. + #[must_use] + #[cfg(feature = "rpc-client")] + pub fn build_prod(self) -> Supervisor { + let (instances, addresses) = self.inner(); + + Supervisor::new( + instances, + ProdForkDetector::default(), + ProdEvidenceReporter::new(addresses.into_values()), + ) + } + + /// Get the underlying list of instances and addresses. + #[must_use] + pub fn inner(self) -> (PeerList, PeerList) { + (self.instances.build(), self.addresses.build()) + } +} diff --git a/light-client/src/components/io.rs b/light-client/src/components/io.rs index ead060706..4c0d957f7 100644 --- a/light-client/src/components/io.rs +++ b/light-client/src/components/io.rs @@ -1,6 +1,5 @@ //! Provides an interface and a default implementation of the `Io` component -use contracts::{contract_trait, post}; use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -53,24 +52,18 @@ impl IoError { } /// Interface for fetching light blocks from a full node, typically via the RPC client. -#[contract_trait] #[allow(missing_docs)] // This is required because of the `contracts` crate (TODO: open/link issue) pub trait Io: Send { - /// Fetch a light block at the given height from the peer with the given peer ID. - /// - /// ## Postcondition - /// - The provider of the returned light block matches the given peer [LCV-IO-POST-PROVIDER] - #[post(ret.as_ref().map(|lb| lb.provider == peer).unwrap_or(true))] - fn fetch_light_block(&self, peer: PeerId, height: AtHeight) -> Result; + /// Fetch a light block at the given height from a peer + fn fetch_light_block(&self, height: AtHeight) -> Result; } -#[contract_trait] impl Io for F where - F: Fn(PeerId, AtHeight) -> Result, + F: Fn(AtHeight) -> Result, { - fn fetch_light_block(&self, peer: PeerId, height: AtHeight) -> Result { - self(peer, height) + fn fetch_light_block(&self, height: AtHeight) -> Result { + self(height) } } @@ -81,34 +74,35 @@ pub use self::prod::ProdIo; mod prod { use super::*; - use std::collections::HashMap; use std::time::Duration; - use crate::bail; - use contracts::{contract_trait, pre}; - use tendermint::{ - block::signed_header::SignedHeader as TMSignedHeader, validator::Set as TMValidatorSet, - }; + use crate::{bail, utils::block_on}; + use tendermint::block::signed_header::SignedHeader as TMSignedHeader; + use tendermint::validator::Set as TMValidatorSet; /// Production implementation of the Io component, which fetches /// light blocks from full nodes via RPC. #[derive(Clone, Debug)] pub struct ProdIo { - peer_map: HashMap, + peer_id: PeerId, + rpc_client: rpc::HttpClient, timeout: Option, } - #[contract_trait] impl Io for ProdIo { - fn fetch_light_block(&self, peer: PeerId, height: AtHeight) -> Result { - let signed_header = self.fetch_signed_header(peer, height)?; + fn fetch_light_block(&self, height: AtHeight) -> Result { + let signed_header = self.fetch_signed_header(height)?; let height = signed_header.header.height; - let validator_set = self.fetch_validator_set(peer, height.into())?; - let next_validator_set = self.fetch_validator_set(peer, height.increment().into())?; + let validator_set = self.fetch_validator_set(height.into())?; + let next_validator_set = self.fetch_validator_set(height.increment().into())?; - let light_block = - LightBlock::new(signed_header, validator_set, next_validator_set, peer); + let light_block = LightBlock::new( + signed_header, + validator_set, + next_validator_set, + self.peer_id, + ); Ok(light_block) } @@ -119,28 +113,27 @@ mod prod { /// /// A peer map which maps peer IDS to their network address must be supplied. pub fn new( - peer_map: HashMap, + peer_id: PeerId, + rpc_client: rpc::HttpClient, // TODO(thane): Generalize over client transport (instead of using HttpClient directly) timeout: Option, ) -> Self { - Self { peer_map, timeout } + Self { + peer_id, + rpc_client, + timeout, + } } - #[pre(self.peer_map.contains_key(&peer))] - fn fetch_signed_header( - &self, - peer: PeerId, - height: AtHeight, - ) -> Result { - let rpc_client = self.rpc_client_for(peer)?; - + fn fetch_signed_header(&self, height: AtHeight) -> Result { + let client = self.rpc_client.clone(); let res = block_on( - async { + async move { match height { - AtHeight::Highest => rpc_client.latest_commit().await, - AtHeight::At(height) => rpc_client.commit(height).await, + AtHeight::Highest => client.latest_commit().await, + AtHeight::At(height) => client.commit(height).await, } }, - peer, + self.peer_id, self.timeout, )?; @@ -150,12 +143,7 @@ mod prod { } } - #[pre(self.peer_map.contains_key(&peer))] - fn fetch_validator_set( - &self, - peer: PeerId, - height: AtHeight, - ) -> Result { + fn fetch_validator_set(&self, height: AtHeight) -> Result { let height = match height { AtHeight::Highest => bail!(IoError::InvalidHeight( "given height must be greater than 0".to_string() @@ -163,42 +151,14 @@ mod prod { AtHeight::At(height) => height, }; - let res = block_on( - self.rpc_client_for(peer)?.validators(height), - peer, - self.timeout, - )?; + let client = self.rpc_client.clone(); + let task = async move { client.validators(height).await }; + let res = block_on(task, self.peer_id, self.timeout)?; match res { Ok(response) => Ok(TMValidatorSet::new(response.validators)), Err(err) => Err(IoError::RpcError(err)), } } - - // TODO(thane): Generalize over client transport (instead of using HttpClient directly). - #[pre(self.peer_map.contains_key(&peer))] - fn rpc_client_for(&self, peer: PeerId) -> Result { - let peer_addr = self.peer_map.get(&peer).unwrap().to_owned(); - Ok(rpc::HttpClient::new(peer_addr).map_err(IoError::from)?) - } - } - - fn block_on( - f: F, - peer: PeerId, - timeout: Option, - ) -> Result { - let mut rt = tokio::runtime::Builder::new() - .basic_scheduler() - .enable_all() - .build() - .unwrap(); - - if let Some(timeout) = timeout { - rt.block_on(async { tokio::time::timeout(timeout, f).await }) - .map_err(|_| IoError::Timeout(peer)) - } else { - Ok(rt.block_on(f)) - } } } diff --git a/light-client/src/evidence.rs b/light-client/src/evidence.rs index e1a746556..a8804d635 100644 --- a/light-client/src/evidence.rs +++ b/light-client/src/evidence.rs @@ -22,9 +22,11 @@ pub use self::prod::ProdEvidenceReporter; #[cfg(feature = "rpc-client")] mod prod { use super::*; + use crate::utils::block_on; use contracts::pre; use std::collections::HashMap; + use tendermint_rpc as rpc; use tendermint_rpc::Client; @@ -39,7 +41,9 @@ mod prod { impl EvidenceReporter for ProdEvidenceReporter { #[pre(self.peer_map.contains_key(&peer))] fn report(&self, e: Evidence, peer: PeerId) -> Result { - let res = block_on(self.rpc_client_for(peer)?.broadcast_evidence(e)); + let client = self.rpc_client_for(peer)?; + let task = async move { client.broadcast_evidence(e).await }; + let res = block_on(task, peer, None)?; match res { Ok(response) => Ok(response.hash), @@ -62,13 +66,4 @@ mod prod { Ok(rpc::HttpClient::new(peer_addr).map_err(IoError::from)?) } } - - fn block_on(f: F) -> F::Output { - tokio::runtime::Builder::new() - .basic_scheduler() - .enable_all() - .build() - .unwrap() - .block_on(f) - } } diff --git a/light-client/src/lib.rs b/light-client/src/lib.rs index c5fd99ea2..2008d79e8 100644 --- a/light-client/src/lib.rs +++ b/light-client/src/lib.rs @@ -16,6 +16,7 @@ //! See the `light_client` module for the main documentation. +pub mod builder; pub mod components; pub mod contracts; pub mod errors; @@ -26,11 +27,13 @@ pub mod operations; pub mod peer_list; pub mod predicates; pub mod state; -mod std_ext; pub mod store; pub mod supervisor; pub mod types; +pub(crate) mod utils; + +#[doc(hidden)] mod macros; #[doc(hidden)] diff --git a/light-client/src/light_client.rs b/light-client/src/light_client.rs index 4942da1ec..5e5f0530a 100644 --- a/light-client/src/light_client.rs +++ b/light-client/src/light_client.rs @@ -88,11 +88,30 @@ impl LightClient { } } + /// Constructs a new light client from boxed components + pub fn from_boxed( + peer: PeerId, + options: Options, + clock: Box, + scheduler: Box, + verifier: Box, + io: Box, + ) -> Self { + Self { + peer, + options, + clock, + scheduler, + verifier, + io, + } + } + /// Attempt to update the light client to the highest block of the primary node. /// /// Note: This function delegates the actual work to `verify_to_target`. pub fn verify_to_highest(&mut self, state: &mut State) -> Result { - let target_block = match self.io.fetch_light_block(self.peer, AtHeight::Highest) { + let target_block = match self.io.fetch_light_block(AtHeight::Highest) { Ok(last_block) => last_block, Err(io_error) => bail!(ErrorKind::Io(io_error)), }; @@ -242,7 +261,7 @@ impl LightClient { let block = self .io - .fetch_light_block(self.peer, AtHeight::At(height)) + .fetch_light_block(AtHeight::At(height)) .map_err(ErrorKind::Io)?; state.light_store.insert(block.clone(), Status::Unverified); diff --git a/light-client/src/peer_list.rs b/light-client/src/peer_list.rs index 0f8776836..20e740ab7 100644 --- a/light-client/src/peer_list.rs +++ b/light-client/src/peer_list.rs @@ -14,7 +14,7 @@ use std::collections::{BTreeSet, HashMap}; /// and faulty nodes. Provides lifecycle methods to swap the primary, /// mark witnesses as faulty, and maintains an `invariant` for /// correctness. -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct PeerList { values: HashMap, primary: PeerId, @@ -56,7 +56,7 @@ impl PeerList { /// - [LCD-INV-NODES] pub fn transition_invariant(_prev: &PeerList, _next: &PeerList) -> bool { true - // TODO + // TODO: Implement transition invariant // &next.full_nodes | &next.witnesses | &next.faulty_nodes // == &prev.full_nodes | &prev.witnesses | &prev.faulty_nodes } @@ -147,18 +147,26 @@ impl PeerList { if let Some(new_primary) = self.witnesses.iter().next().copied() { self.primary = new_primary; self.witnesses.remove(&new_primary); - return Ok(new_primary); - } - - if let Some(err) = primary_error { + Ok(new_primary) + } else if let Some(err) = primary_error { bail!(ErrorKind::NoWitnessLeft.context(err)) } else { bail!(ErrorKind::NoWitnessLeft) } } + + /// Get a reference to the underlying `HashMap` + pub fn values(&self) -> &HashMap { + &self.values + } + /// Consume into the underlying `HashMap` + pub fn into_values(self) -> HashMap { + self.values + } } /// A builder of `PeerList` with a fluent API. +#[must_use] pub struct PeerListBuilder { values: HashMap, primary: Option, @@ -185,33 +193,30 @@ impl Default for PeerListBuilder { impl PeerListBuilder { /// Register the given peer id and instance as the primary. /// Overrides the previous primary if it was already set. - pub fn primary(mut self, peer_id: PeerId, value: T) -> Self { + pub fn primary(&mut self, peer_id: PeerId, value: T) { self.primary = Some(peer_id); self.values.insert(peer_id, value); - self } /// Register the given peer id and value as a witness. #[pre(self.primary != Some(peer_id))] - pub fn witness(mut self, peer_id: PeerId, value: T) -> Self { + pub fn witness(&mut self, peer_id: PeerId, value: T) { self.values.insert(peer_id, value); self.witnesses.insert(peer_id); - self } /// Register the given peer id and value as a full node. #[pre(self.primary != Some(peer_id))] - pub fn full_node(mut self, peer_id: PeerId, value: T) -> Self { + pub fn full_node(&mut self, peer_id: PeerId, value: T) { self.values.insert(peer_id, value); self.full_nodes.insert(peer_id); - self } + /// Register the given peer id and value as a faulty node. #[pre(self.primary != Some(peer_id))] - pub fn faulty_node(mut self, peer_id: PeerId, value: T) -> Self { + pub fn faulty_node(&mut self, peer_id: PeerId, value: T) { self.values.insert(peer_id, value); self.faulty_nodes.insert(peer_id); - self } /// Builds the `PeerList`. @@ -258,12 +263,11 @@ mod tests { "da918eef62d986812b4e6271de78db4ec52594eb".parse().unwrap() } fn dummy_peer_list() -> PeerList { - let builder = PeerList::builder(); - builder - .primary(a(), 1_u32) - .witness(b(), 2_u32) - .full_node(c(), 3_u32) - .build() + let mut builder = PeerList::builder(); + builder.primary(a(), 1_u32); + builder.witness(b(), 2_u32); + builder.full_node(c(), 3_u32); + builder.build() } #[test] @@ -281,8 +285,10 @@ mod tests { #[test] #[should_panic(expected = "Pre-condition of build violated")] fn builder_fails_if_no_primary() { - let builder = PeerList::builder(); - let _ = builder.witness(b(), 2_u32).full_node(c(), 3_u32).build(); + let mut builder = PeerList::builder(); + builder.witness(b(), 2_u32); + builder.full_node(c(), 3_u32); + let _ = builder.build(); unreachable!(); } diff --git a/light-client/src/store.rs b/light-client/src/store.rs index 79a450dc2..38c70dd71 100644 --- a/light-client/src/store.rs +++ b/light-client/src/store.rs @@ -4,8 +4,8 @@ //! - a transient, in-memory implementation for testing purposes //! - a persistent, on-disk, sled-backed implementation for production -use crate::std_ext; use crate::types::{Height, LightBlock, Status}; +use crate::utils::std_ext; pub mod memory; pub mod sled; diff --git a/light-client/src/supervisor.rs b/light-client/src/supervisor.rs index 635cfe1a1..b79b6964e 100644 --- a/light-client/src/supervisor.rs +++ b/light-client/src/supervisor.rs @@ -14,31 +14,21 @@ use crate::state::State; use crate::types::{Height, LatestStatus, LightBlock, PeerId, Status}; /// Provides an interface to the supervisor for use in downstream code. -pub trait Handle { +pub trait Handle: Send + Sync { /// Get latest trusted block. - fn latest_trusted(&self) -> Result, Error> { - todo!() - } + fn latest_trusted(&self) -> Result, Error>; /// Get the latest status. - fn latest_status(&self) -> Result { - todo!() - } + fn latest_status(&self) -> Result; /// Verify to the highest block. - fn verify_to_highest(&self) -> Result { - todo!() - } + fn verify_to_highest(&self) -> Result; /// Verify to the block at the given height. - fn verify_to_target(&self, _height: Height) -> Result { - todo!() - } + fn verify_to_target(&self, _height: Height) -> Result; /// Terminate the underlying [`Supervisor`]. - fn terminate(&self) -> Result<(), Error> { - todo!() - } + fn terminate(&self) -> Result<(), Error>; } /// Input events sent by the [`Handle`]s to the [`Supervisor`]. They carry a [`Callback`] which is @@ -47,12 +37,16 @@ pub trait Handle { enum HandleInput { /// Terminate the supervisor process Terminate(channel::Sender<()>), + /// Verify to the highest height, call the provided callback with result VerifyToHighest(channel::Sender>), + /// Verify to the given height, call the provided callback with result VerifyToTarget(Height, channel::Sender>), + /// Get the latest trusted block. LatestTrusted(channel::Sender>), + /// Get the current status of the LightClient GetStatus(channel::Sender), } @@ -166,11 +160,12 @@ impl Supervisor { } /// Create a new handle to this supervisor. - pub fn handle(&mut self) -> impl Handle { + pub fn handle(&self) -> SupervisorHandle { SupervisorHandle::new(self.sender.clone()) } - fn latest_trusted(&self) -> Option { + /// Get the latest trusted state of the primary peer, if any + pub fn latest_trusted(&self) -> Option { self.peers.primary().latest_trusted() } @@ -364,7 +359,8 @@ impl Supervisor { /// A [`Handle`] to the [`Supervisor`] which allows to communicate with /// the supervisor across thread boundaries via message passing. -struct SupervisorHandle { +#[derive(Clone)] +pub struct SupervisorHandle { sender: channel::Sender, } @@ -387,6 +383,7 @@ impl SupervisorHandle { receiver.recv().map_err(ErrorKind::from)? } } + impl Handle for SupervisorHandle { fn latest_trusted(&self) -> Result, Error> { let (sender, receiver) = channel::bounded::>(1); diff --git a/light-client/src/tests.rs b/light-client/src/tests.rs index 6ad1a400e..a4768853d 100644 --- a/light-client/src/tests.rs +++ b/light-client/src/tests.rs @@ -122,9 +122,8 @@ impl MockIo { } } -#[contract_trait] impl Io for MockIo { - fn fetch_light_block(&self, _peer: PeerId, height: AtHeight) -> Result { + fn fetch_light_block(&self, height: AtHeight) -> Result { let height = match height { AtHeight::Highest => self.latest_height, AtHeight::At(height) => height, diff --git a/light-client/src/utils.rs b/light-client/src/utils.rs new file mode 100644 index 000000000..2a7930b1c --- /dev/null +++ b/light-client/src/utils.rs @@ -0,0 +1,8 @@ +//! Various general-purpose utilities + +#[cfg(feature = "rpc-client")] +mod block_on; +#[cfg(feature = "rpc-client")] +pub use block_on::block_on; + +pub mod std_ext; diff --git a/light-client/src/utils/block_on.rs b/light-client/src/utils/block_on.rs new file mode 100644 index 000000000..e058a8a9c --- /dev/null +++ b/light-client/src/utils/block_on.rs @@ -0,0 +1,29 @@ +use std::{future::Future, time::Duration}; + +use crate::{components::io::IoError, types::PeerId}; + +/// Run a future to completion on a new thread, with the given timeout. +/// +/// This function will block the caller until the given future has completed. +pub fn block_on(f: F, peer: PeerId, timeout: Option) -> Result +where + F: Future + Send + 'static, + F::Output: Send, +{ + std::thread::spawn(move || { + let mut rt = tokio::runtime::Builder::new() + .basic_scheduler() + .enable_all() + .build() + .unwrap(); + + if let Some(timeout) = timeout { + let task = async { tokio::time::timeout(timeout, f).await }; + rt.block_on(task).map_err(|_| IoError::Timeout(peer)) + } else { + Ok(rt.block_on(f)) + } + }) + .join() + .unwrap() +} diff --git a/light-client/src/std_ext.rs b/light-client/src/utils/std_ext.rs similarity index 100% rename from light-client/src/std_ext.rs rename to light-client/src/utils/std_ext.rs diff --git a/light-client/tests/integration.rs b/light-client/tests/integration.rs index 3d11b6901..8843040d0 100644 --- a/light-client/tests/integration.rs +++ b/light-client/tests/integration.rs @@ -8,54 +8,55 @@ //! ``` use tendermint_light_client::{ - components::{ - clock::SystemClock, - io::{AtHeight, Io, IoError, ProdIo}, - scheduler, - verifier::ProdVerifier, - }, + builder::LightClientBuilder, + builder::SupervisorBuilder, + components::io::AtHeight, + components::io::Io, + components::io::IoError, + components::io::ProdIo, evidence::{Evidence, EvidenceReporter}, - fork_detector::ProdForkDetector, - light_client::{self, LightClient}, - peer_list::PeerList, - state::State, - store::{memory::MemoryStore, LightStore}, - supervisor::{Handle, Instance, Supervisor}, + light_client, + store::memory::MemoryStore, + store::LightStore, + supervisor::{Handle, Instance}, types::{PeerId, Status, TrustThreshold}, }; -use tendermint::abci::transaction::Hash as TransactionHash; +use tendermint::abci::transaction::Hash as TxHash; +use tendermint::net; +use tendermint_rpc as rpc; -use std::collections::HashMap; use std::time::Duration; -fn make_instance(peer_id: PeerId, options: light_client::Options, io: ProdIo) -> Instance { - let trusted_state = io - .fetch_light_block(peer_id, AtHeight::Highest) - .expect("could not request latest light block"); - - let mut light_store = MemoryStore::new(); - light_store.insert(trusted_state, Status::Trusted); - - let state = State { - light_store: Box::new(light_store), - verification_trace: HashMap::new(), - }; - - let verifier = ProdVerifier::default(); - let clock = SystemClock; - let scheduler = scheduler::basic_bisecting_schedule; - - let light_client = LightClient::new(peer_id, options, clock, scheduler, verifier, io); - - Instance::new(light_client, state) +fn make_instance( + peer_id: PeerId, + options: light_client::Options, + address: net::Address, +) -> Instance { + let rpc_client = rpc::HttpClient::new(address).unwrap(); + let io = ProdIo::new(peer_id, rpc_client.clone(), Some(Duration::from_secs(2))); + let latest_block = io.fetch_light_block(AtHeight::Highest).unwrap(); + + let mut light_store = Box::new(MemoryStore::new()); + light_store.insert(latest_block, Status::Trusted); + + LightClientBuilder::prod( + peer_id, + rpc_client, + light_store, + options, + Some(Duration::from_secs(2)), + ) + .trust_from_store() + .unwrap() + .build() } struct TestEvidenceReporter; #[contracts::contract_trait] impl EvidenceReporter for TestEvidenceReporter { - fn report(&self, evidence: Evidence, peer: PeerId) -> Result { + fn report(&self, evidence: Evidence, peer: PeerId) -> Result { panic!( "unexpected fork detected for peer {} with evidence: {:?}", peer, evidence @@ -69,18 +70,12 @@ fn sync() { let primary: PeerId = "BADFADAD0BEFEEDC0C0ADEADBEEFC0FFEEFACADE".parse().unwrap(); let witness: PeerId = "CEFEEDBADFADAD0C0CEEFACADE0ADEADBEEFC0FF".parse().unwrap(); - let node_address: tendermint::net::Address = "tcp://127.0.0.1:26657".parse().unwrap(); - // Because our CI infrastructure can only spawn a single Tendermint node at the moment, // we run this test against this very node as both the primary and witness. // In a production environment, one should make sure that the primary and witness are // different nodes, and check that the configured peer IDs match the ones returned // by the nodes. - let mut peer_map = HashMap::new(); - peer_map.insert(primary, node_address.clone()); - peer_map.insert(witness, node_address); - - let io = ProdIo::new(peer_map, Some(Duration::from_secs(2))); + let node_address: tendermint::net::Address = "tcp://127.0.0.1:26657".parse().unwrap(); let options = light_client::Options { trust_threshold: TrustThreshold { @@ -91,21 +86,18 @@ fn sync() { clock_drift: Duration::from_secs(5 * 60), // 5 minutes }; - let primary_instance = make_instance(primary, options, io.clone()); - let witness_instance = make_instance(witness, options, io); - - let peer_list = PeerList::builder() - .primary(primary, primary_instance) - .witness(witness, witness_instance) - .build(); + let primary_instance = make_instance(primary, options, node_address.clone()); + let witness_instance = make_instance(witness, options, node_address.clone()); - let mut supervisor = - Supervisor::new(peer_list, ProdForkDetector::default(), TestEvidenceReporter); + let supervisor = SupervisorBuilder::new() + .primary(primary, node_address.clone(), primary_instance) + .witness(witness, node_address, witness_instance) + .build_prod(); let handle = supervisor.handle(); std::thread::spawn(|| supervisor.run()); - let max_iterations: usize = 1; // Todo: Fix no witness left error in subsequent iterations + let max_iterations: usize = 1; // FIXME: Fix no witness left error in subsequent iterations for i in 1..=max_iterations { println!("[info ] - iteration {}/{}", i, max_iterations); diff --git a/light-client/tests/light_client.rs b/light-client/tests/light_client.rs index d618d2350..b6db26a43 100644 --- a/light-client/tests/light_client.rs +++ b/light-client/tests/light_client.rs @@ -50,7 +50,7 @@ fn run_bisection_test(tc: TestBisection) -> BisectionTestResult { let trusted_height = tc.trust_options.height; let trusted_state = io - .fetch_light_block(primary, AtHeight::At(trusted_height)) + .fetch_light_block(AtHeight::At(trusted_height)) .expect("could not 'request' light block"); let mut light_store = MemoryStore::new(); @@ -75,7 +75,7 @@ fn run_bisection_test(tc: TestBisection) -> BisectionTestResult { let result = verify_bisection(untrusted_height, &mut light_client, &mut state); let untrusted_light_block = io - .fetch_light_block(primary, AtHeight::At(untrusted_height)) + .fetch_light_block(AtHeight::At(untrusted_height)) .expect("header at untrusted height not found"); BisectionTestResult { diff --git a/light-client/tests/supervisor.rs b/light-client/tests/supervisor.rs index a5867ff3c..0b3f3a0e9 100644 --- a/light-client/tests/supervisor.rs +++ b/light-client/tests/supervisor.rs @@ -28,7 +28,7 @@ const TEST_FILES_PATH: &str = "./tests/support/"; fn make_instance(peer_id: PeerId, trust_options: TrustOptions, io: MockIo, now: Time) -> Instance { let trusted_height = trust_options.height; let trusted_state = io - .fetch_light_block(peer_id, AtHeight::At(trusted_height)) + .fetch_light_block(AtHeight::At(trusted_height)) .expect("could not 'request' light block"); let mut light_store = MemoryStore::new(); @@ -72,17 +72,17 @@ fn run_multipeer_test(tc: TestBisection) { let primary_instance = make_instance(primary, tc.trust_options.clone(), io.clone(), tc.now); let mut peer_list = PeerList::builder(); - peer_list = peer_list.primary(primary, primary_instance); + peer_list.primary(primary, primary_instance); for provider in tc.witnesses.into_iter() { let peer_id = provider.value.lite_blocks[0].provider; println!("Witness: {}", peer_id); let io = MockIo::new(provider.value.chain_id, provider.value.lite_blocks); let instance = make_instance(peer_id, tc.trust_options.clone(), io.clone(), tc.now); - peer_list = peer_list.witness(peer_id, instance); + peer_list.witness(peer_id, instance); } - let mut supervisor = Supervisor::new( + let supervisor = Supervisor::new( peer_list.build(), ProdForkDetector::default(), MockEvidenceReporter::new(), @@ -99,7 +99,7 @@ fn run_multipeer_test(tc: TestBisection) { Ok(new_state) => { // Check that the expected state and new_state match let untrusted_light_block = io - .fetch_light_block(primary, AtHeight::At(target_height)) + .fetch_light_block(AtHeight::At(target_height)) .expect("header at untrusted height not found"); let expected_state = untrusted_light_block; diff --git a/light-node/src/commands/initialize.rs b/light-node/src/commands/initialize.rs index 2989bb85f..182d14170 100644 --- a/light-node/src/commands/initialize.rs +++ b/light-node/src/commands/initialize.rs @@ -1,9 +1,11 @@ //! `intialize` subcommand +use std::ops::Deref; +use std::time::Duration; + use crate::application::app_config; use crate::config::LightClientConfig; - -use std::collections::HashMap; +use crate::config::LightNodeConfig; use abscissa_core::status_err; use abscissa_core::status_warn; @@ -13,12 +15,13 @@ use abscissa_core::Runnable; use tendermint::{hash, 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::builder::LightClientBuilder; use tendermint_light_client::store::sled::SledStore; use tendermint_light_client::store::LightStore; -use tendermint_light_client::types::{Height, Status}; +use tendermint_light_client::supervisor::Instance; +use tendermint_light_client::types::Height; + +use tendermint_rpc as rpc; /// `initialize` subcommand #[derive(Command, Debug, Default, Options)] @@ -40,69 +43,61 @@ 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.into(), subjective_header_hash, &lc, &io); + let node_config = app_config().deref().clone(); + let light_client_config = node_config.light_clients.first().unwrap(); + + if let Err(e) = initialize_subjectively( + self.height.into(), + subjective_header_hash, + &node_config, + &light_client_config, + Some(node_config.rpc_config.request_timeout), + ) { + status_err!("failed to initialize light client: {}", e); + // TODO: Set exit code to 1 + } } } -// 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: Height, 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(); + node_config: &LightNodeConfig, + config: &LightClientConfig, + timeout: Option, +) -> Result { + let db = sled::open(config.db_path.clone()) + .map_err(|e| format!("could not open database: {}", e))?; + + let light_store = SledStore::new(db); + + if let Some(trusted_state) = light_store.latest_trusted_or_verified() { status_warn!( "already existing trusted or verified state of height {} in database: {:?}", - lb.signed_header.header.height, - l_conf.db_path + trusted_state.signed_header.header.height, + config.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); + let rpc_client = rpc::HttpClient::new(config.address.clone()).map_err(|e| e.to_string())?; + + let builder = LightClientBuilder::prod( + config.peer_id, + rpc_client, + Box::new(light_store), + node_config.clone().into(), + timeout, + ); + + let builder = builder + .trust_primary_at(height, subjective_header_hash) + .map_err(|e| { + format!( + "could not trust header at height {} and hash {}. Reason: {}", + height, subjective_header_hash, e + ) + })?; + + Ok(builder.build()) } diff --git a/light-node/src/commands/start.rs b/light-node/src/commands/start.rs index bfc653863..5e7da06a4 100644 --- a/light-node/src/commands/start.rs +++ b/light-node/src/commands/start.rs @@ -16,25 +16,14 @@ 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::builder::{LightClientBuilder, SupervisorBuilder}; 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::store::{sled::SledStore, LightStore}; +use tendermint_light_client::supervisor::{Handle, Instance, Supervisor}; /// `start` subcommand #[derive(Command, Debug, Options)] @@ -56,8 +45,18 @@ impl Runnable for StartCmd { /// Start the application. fn run(&self) { if let Err(err) = abscissa_tokio::run(&APPLICATION, async { - StartCmd::assert_init_was_run(); - let mut supervisor = self.construct_supervisor(); + if let Err(e) = StartCmd::assert_init_was_run() { + status_err!(&e); + panic!(e); + } + + let supervisor = match self.construct_supervisor() { + Ok(supervisor) => supervisor, + Err(e) => { + status_err!(&e); + panic!(e); + } + }; let rpc_handler = supervisor.handle(); StartCmd::start_rpc_server(rpc_handler); @@ -74,6 +73,7 @@ impl Runnable for StartCmd { status_err!("sync failed: {}", err); } } + // TODO(liamsi): use ticks and make this configurable: std::thread::sleep(Duration::from_millis(800)); } @@ -100,52 +100,18 @@ impl config::Override for StartCmd { Ok(config) } } + 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); - }); + fn assert_init_was_run() -> Result<(), String> { + let db_path = app_config().light_clients.first().unwrap().db_path.clone(); + let db = sled::open(db_path).map_err(|e| format!("could not open database: {}", e))?; 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); + return Err("no trusted or verified state in store for primary, please initialize with the `initialize` subcommand first".to_string()); } - } - // 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(); - - let db = sled::open(db_path).unwrap_or_else(|e| { - status_err!("could not open database: {}", e); - std::process::exit(1); - }); - - let light_store = SledStore::new(db); - - let state = State { - light_store: Box::new(light_store), - verification_trace: HashMap::new(), - }; - let verifier = ProdVerifier::default(); - let clock = SystemClock; - let scheduler = scheduler::basic_bisecting_schedule; - - let light_client = LightClient::new(peer_id, options, clock, scheduler, verifier, io); - - Instance::new(light_client, state) + Ok(()) } fn start_rpc_server(h: H) @@ -158,38 +124,68 @@ impl StartCmd { std::thread::spawn(move || rpc::run(server, &laddr.to_string())); status_info!("started RPC server:", laddr.to_string()); } -} -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), + fn make_instance( + &self, + light_config: &LightClientConfig, + options: light_client::Options, + timeout: Option, + ) -> Result { + let rpc_client = tendermint_rpc::HttpClient::new(light_config.address.clone()) + .map_err(|e| format!("failed to create HTTP client: {}", e))?; + + let db_path = light_config.db_path.clone(); + let db = sled::open(db_path).map_err(|e| format!("could not open database: {}", e))?; + + let light_store = SledStore::new(db); + + let builder = LightClientBuilder::prod( + light_config.peer_id, + rpc_client, + Box::new(light_store), + options, + timeout, ); + + let builder = builder + .trust_from_store() + .map_err(|e| format!("could not set initial trusted state: {}", e))?; + + Ok(builder.build()) + } + + fn construct_supervisor(&self) -> Result { let conf = app_config().deref().clone(); + let timeout = app_config().rpc_config.request_timeout; 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 light_confs = &app_config().light_clients; + if light_confs.len() < 2 { + return Err(format!("configuration incomplete: not enough light clients configued, minimum: 2, found: {}", light_confs.len())); + } + + let primary_conf = &light_confs[0]; // Safe, see check above + let witness_confs = &light_confs[1..]; // Safe, see check above + + let builder = SupervisorBuilder::new(); + + let primary_instance = self.make_instance(primary_conf, options, Some(timeout))?; + let builder = builder.primary( + primary_conf.peer_id, + primary_conf.address.clone(), + primary_instance, + ); + + let mut witnesses = Vec::with_capacity(witness_confs.len()); + for witness_conf in witness_confs { + let instance = self.make_instance(witness_conf, options, Some(timeout))?; + witnesses.push((witness_conf.peer_id, witness_conf.address.clone(), instance)); } - let peer_list = peer_list.build(); - Supervisor::new( - peer_list, - ProdForkDetector::default(), - ProdEvidenceReporter::new(peer_map), - ) + let builder = builder + .witnesses(witnesses) + .map_err(|e| format!("failed to set witnesses: {}", e))?; + + Ok(builder.build_prod()) } } diff --git a/light-node/src/rpc.rs b/light-node/src/rpc.rs index 30812455d..ea282aefb 100644 --- a/light-node/src/rpc.rs +++ b/light-node/src/rpc.rs @@ -152,11 +152,27 @@ mod test { Ok(Some(block)) } + fn latest_status(&self) -> Result { let status: LatestStatus = serde_json::from_str(STATUS_JSON).unwrap(); Ok(status) } + + fn verify_to_highest(&self) -> Result { + todo!() + } + + fn verify_to_target( + &self, + _height: tendermint::block::Height, + ) -> Result { + todo!() + } + + fn terminate(&self) -> Result<(), Error> { + todo!() + } } const LIGHTBLOCK_JSON: &str = r#"