Skip to content

Commit

Permalink
Light Client refactoring (#237)
Browse files Browse the repository at this point in the history
* Rework predicates

* WIP: Add tracing

* Fix verification procedure

* Rename requester component to rpc

* Rename Trace::run to Trace::collect

* Return meaningful data in errors

* Proper error handling with thiserror+anomaly

* Make events PartialEq+Eq

* Implement verifier

* Implement scheduler and bisection

* Remove write access to trusted store for scheduler

* Add a couple of FIXMEs

* Formatting

* Fix clippy warnings

* Fix misplaced attribute

* Enable VerificationPredicates to be made into a trait object

* Allow cloning TSReader

* Shorter method name

* Decouple components using Router trait

* Silence a couple Clippy warnings

* Cleanup trace module

* Revamp errors

* Revamp error, part 2

* Bundle verification options together

* Cleanup

* Use output enum for all components

* Split queries out

* Rewrite using coroutines

* cleanup

* Add fork detector prototype

* Add stub example

* Add traits to abstract of each concrete component

* Add actual commit to Commit struct

* Refactor and simplify

* Implement Store::latest

* Better verification loop

* Add pre/post conditions to demuxer::verify

* Add contract for schedule

* Convert between tendermint and spike types

* Working example

* Better working version

* Implement production header hasher

* Re-add proper Error type for whole client

* Cleanup

* Add peers to demuxer state

* Cleanup

* Trace blocks needed for verification of a target block

* Split validation and trust check into their own top-level predicates

* Add provider to LightBlock struct

* Split overlap verification into its own verifier input

* Cleanup

* Don't mix verifier and scheduler concerns

* Validate commits and compute actual voting power

* Cleanup

* Remove scheduler events

* Remove verifier events

* Remove fork detector events

* Remove IO events

* Cleanup

* Simplify code flow by using an iterator of highest trusted states

* Fix example

* Update example to prod implementations

* Stop verification when reaching already trusted state

* Use height of fetched header to fetch validators (avoids issues when height is 0 for latest)

* Allow tracing same block, just ignore it

* Better error reporting

* Fix bug in is_monotonic_height

* Shorten ProductionPredicates name

* Cleanup

* Port over single-step tests

* Port bisection tests

* Move test types into their own module

* Refactor

* Fix bug in validators overlap check

* Refactor LightStore, and introduce proper contracts

* Cleanup

* Simply LightStore trait

* Use tendermint::node::Id as PeerId, as per the spec

* Add LightStore::update method

* Fix clippy warnings

* Extract get_or_fetch_block method from demuxer loop

* Rename Demuxer to LightClient

* Rename LightClientOptions to Options

* Implement on-disk store backed by Sled

* Formatting

* Cleanup

* Properly implement SledStore::update

* Cleanup

* Add LightClient CLI to continuously pull headers from a node

* Fix tests

* Adapt test to new JSON files organization

* Rename light-spike crate to light-client

* Turn production predicates into default trait impl

* Comment out provider field of LightBlock until conformance tests are adapted

* Refactor is_within_trust_period to better match the spec

* Add core verification loop invariant

* WIP: Documentation

* Make cargo fmt happy

* Make clippy happy

* Re-enable `provider` field in LightBlock struct
  • Loading branch information
romac committed May 27, 2020
1 parent 4e179c9 commit 1ba8a2c
Show file tree
Hide file tree
Showing 35 changed files with 2,531 additions and 7 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
members = [
"tendermint",
"light-node",
"light-client",
]
24 changes: 24 additions & 0 deletions light-client/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
[package]
name = "light-client"
version = "0.1.0"
authors = ["Romain Ruetschi <romain@informal.systems>"]
edition = "2018"

[dependencies]
tendermint = { path = "../tendermint" }

anomaly = { version = "0.2.0", features = ["serializer"] }
derive_more = "0.99.5"
serde = "1.0.106"
serde_derive = "1.0.106"
thiserror = "1.0.15"
futures = "0.3.4"
tokio = "0.2.20"
prost-amino = "0.5.0"
contracts = "0.4.0"
sled = "0.31.0"
serde_cbor = "0.11.1"

[dev-dependencies]
serde_json = "1.0.51"
gumdrop = "0.8.0"
145 changes: 145 additions & 0 deletions light-client/examples/light_client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
use gumdrop::Options;
use light_client::prelude::Height;

use std::collections::HashMap;
use std::path::PathBuf;

#[derive(Debug, Options)]
struct CliOptions {
#[options(help = "print this help message")]
help: bool,
#[options(help = "enable verbose output")]
verbose: bool,

#[options(command)]
command: Option<Command>,
}

#[derive(Debug, Options)]
enum Command {
#[options(help = "run the light client and continuously sync up to the latest block")]
Sync(SyncOpts),
}

#[derive(Debug, Options)]
struct SyncOpts {
#[options(help = "show help for this command")]
help: bool,
#[options(
help = "address of the Tendermint node to connect to",
meta = "ADDR",
default = "tcp://127.0.0.1:26657"
)]
address: tendermint::net::Address,
#[options(
help = "height of the initial trusted state (optional if store already initialized)",
meta = "HEIGHT"
)]
trusted_height: Option<Height>,
#[options(
help = "path to the database folder",
meta = "PATH",
default = "./lightstore"
)]
db_path: PathBuf,
}

fn main() {
let opts = CliOptions::parse_args_default_or_exit();
match opts.command {
None => {
eprintln!("Please specify a command:");
eprintln!("{}\n", CliOptions::command_list().unwrap());
eprintln!("{}\n", CliOptions::usage());
std::process::exit(1);
}
Some(Command::Sync(sync_opts)) => sync_cmd(sync_opts),
}
}

fn sync_cmd(opts: SyncOpts) {
use light_client::components::scheduler;
use light_client::prelude::*;

let primary_addr = opts.address;
let primary: PeerId = "BADFADAD0BEFEEDC0C0ADEADBEEFC0FFEEFACADE".parse().unwrap();

let mut peer_map = HashMap::new();
peer_map.insert(primary, primary_addr);

let mut io = ProdIo::new(peer_map);

let db = sled::open(opts.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(primary, height).unwrap_or_else(|e| {
println!("[ error ] could not retrieve trusted header: {}", e);
std::process::exit(1);
});

light_store.insert(trusted_state, VerifiedStatus::Verified);
}

let peers = Peers {
primary,
witnesses: Vec::new(),
};

let state = State {
peers,
light_store: Box::new(light_store),
verification_trace: HashMap::new(),
};

let options = Options {
trust_threshold: TrustThreshold {
numerator: 1,
denominator: 3,
},
trusting_period: Duration::from_secs(36000),
now: Time::now(),
};

let predicates = ProdPredicates;
let voting_power_calculator = ProdVotingPowerCalculator;
let commit_validator = ProdCommitValidator;
let header_hasher = ProdHeaderHasher;

let verifier = ProdVerifier::new(
predicates,
voting_power_calculator,
commit_validator,
header_hasher,
);

let clock = SystemClock;
let scheduler = scheduler::schedule;
let fork_detector = RealForkDetector::new(header_hasher);

let mut light_client = LightClient::new(
state,
options,
clock,
scheduler,
verifier,
fork_detector,
io,
);

loop {
match light_client.verify_to_highest() {
Ok(light_block) => {
println!("[ info ] synced to block {}", light_block.height());
}
Err(e) => {
println!("[ error ] sync failed: {}", e);
}
}
std::thread::sleep(Duration::from_millis(800));
}
}
5 changes: 5 additions & 0 deletions light-client/src/components.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pub mod clock;
pub mod fork_detector;
pub mod io;
pub mod scheduler;
pub mod verifier;
15 changes: 15 additions & 0 deletions light-client/src/components/clock.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
use crate::prelude::*;

/// Abstracts over the current time.
pub trait Clock {
/// Get the current time.
fn now(&self) -> Time;
}

/// Provides the current wall clock time.
pub struct SystemClock;
impl Clock for SystemClock {
fn now(&self) -> Time {
Time::now()
}
}
48 changes: 48 additions & 0 deletions light-client/src/components/fork_detector.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
use serde::{Deserialize, Serialize};

use crate::prelude::*;

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum ForkDetection {
// NOTE: We box the fields to reduce the overall size of the enum.
// See https://rust-lang.github.io/rust-clippy/master/index.html#large_enum_variant
Detected(Box<LightBlock>, Box<LightBlock>),
NotDetected,
}

pub trait ForkDetector {
fn detect(&self, light_blocks: Vec<LightBlock>) -> ForkDetection;
}

pub struct RealForkDetector {
header_hasher: Box<dyn HeaderHasher>,
}

impl RealForkDetector {
pub fn new(header_hasher: impl HeaderHasher + 'static) -> Self {
Self {
header_hasher: Box::new(header_hasher),
}
}
}

impl ForkDetector for RealForkDetector {
fn detect(&self, mut light_blocks: Vec<LightBlock>) -> ForkDetection {
if light_blocks.is_empty() {
return ForkDetection::NotDetected;
}

let first_block = light_blocks.pop().unwrap(); // Safety: checked above that not empty.
let first_hash = self.header_hasher.hash(&first_block.signed_header.header);

for light_block in light_blocks {
let hash = self.header_hasher.hash(&light_block.signed_header.header);

if first_hash != hash {
return ForkDetection::Detected(Box::new(first_block), Box::new(light_block));
}
}

ForkDetection::NotDetected
}
}
126 changes: 126 additions & 0 deletions light-client/src/components/io.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
use contracts::pre;
use serde::{Deserialize, Serialize};
use tendermint::{block, rpc};
use thiserror::Error;

use tendermint::block::signed_header::SignedHeader as TMSignedHeader;
use tendermint::validator::Set as TMValidatorSet;

use crate::prelude::*;
use std::collections::HashMap;

pub const LATEST_HEIGHT: Height = 0;

#[derive(Clone, Debug, Error, PartialEq, Serialize, Deserialize)]
pub enum IoError {
/// Wrapper for a `tendermint::rpc::Error`.
#[error(transparent)]
IoError(#[from] rpc::Error),
}

/// Interface for fetching light blocks from a full node, typically via the RPC client.
// TODO: Enable contracts on the trait once the provider field is available.
// #[contract_trait]
pub trait Io {
/// Fetch a light block at the given height from the peer with the given peer ID.
// #[post(ret.map(|lb| lb.provider == peer).unwrap_or(true))]
fn fetch_light_block(&mut self, peer: PeerId, height: Height) -> Result<LightBlock, IoError>;
}

// #[contract_trait]
impl<F> Io for F
where
F: FnMut(PeerId, Height) -> Result<LightBlock, IoError>,
{
fn fetch_light_block(&mut self, peer: PeerId, height: Height) -> Result<LightBlock, IoError> {
self(peer, height)
}
}

/// Production implementation of the Io component, which fetches
/// light blocks from full nodes via RPC.
pub struct ProdIo {
rpc_clients: HashMap<PeerId, rpc::Client>,
peer_map: HashMap<PeerId, tendermint::net::Address>,
}

// #[contract_trait]
impl Io for ProdIo {
fn fetch_light_block(&mut self, peer: PeerId, height: Height) -> Result<LightBlock, IoError> {
let signed_header = self.fetch_signed_header(peer, height)?;
let height = signed_header.header.height.into();

let validator_set = self.fetch_validator_set(peer, height)?;
let next_validator_set = self.fetch_validator_set(peer, height + 1)?;

let light_block = LightBlock::new(signed_header, validator_set, next_validator_set, peer);

Ok(light_block)
}
}

impl ProdIo {
/// Constructs a new ProdIo component.
///
/// A peer map which maps peer IDS to their network address must be supplied.
pub fn new(peer_map: HashMap<PeerId, tendermint::net::Address>) -> Self {
Self {
rpc_clients: HashMap::new(),
peer_map,
}
}

#[pre(self.peer_map.contains_key(&peer))]
fn fetch_signed_header(
&mut self,
peer: PeerId,
height: Height,
) -> Result<TMSignedHeader, IoError> {
let height: block::Height = height.into();
let rpc_client = self.rpc_client_for(peer);

let res = block_on(async {
match height.value() {
0 => rpc_client.latest_commit().await,
_ => rpc_client.commit(height).await,
}
});

match res {
Ok(response) => Ok(response.signed_header),
Err(err) => Err(IoError::IoError(err)),
}
}

#[pre(self.peer_map.contains_key(&peer))]
fn fetch_validator_set(
&mut self,
peer: PeerId,
height: Height,
) -> Result<TMValidatorSet, IoError> {
let res = block_on(self.rpc_client_for(peer).validators(height));

match res {
Ok(response) => Ok(TMValidatorSet::new(response.validators)),
Err(err) => Err(IoError::IoError(err)),
}
}

// FIXME: Cannot enable precondition because of "autoref lifetime" issue
// #[pre(self.peer_map.contains_key(&peer))]
fn rpc_client_for(&mut self, peer: PeerId) -> &mut rpc::Client {
let peer_addr = self.peer_map.get(&peer).unwrap().to_owned();
self.rpc_clients
.entry(peer)
.or_insert_with(|| rpc::Client::new(peer_addr))
}
}

fn block_on<F: std::future::Future>(f: F) -> F::Output {
tokio::runtime::Builder::new()
.basic_scheduler()
.enable_all()
.build()
.unwrap()
.block_on(f)
}
Loading

0 comments on commit 1ba8a2c

Please sign in to comment.