-
Notifications
You must be signed in to change notification settings - Fork 216
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* 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
Showing
35 changed files
with
2,531 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,4 +3,5 @@ | |
members = [ | ||
"tendermint", | ||
"light-node", | ||
"light-client", | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
Oops, something went wrong.