Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Light Client refactoring #237

Merged
merged 102 commits into from
May 27, 2020
Merged
Show file tree
Hide file tree
Changes from 101 commits
Commits
Show all changes
102 commits
Select commit Hold shift + click to select a range
cceaa70
Rework predicates
romac Apr 23, 2020
a982821
WIP: Add tracing
romac Apr 23, 2020
7e2a9d5
Fix verification procedure
romac Apr 24, 2020
8509a13
Rename requester component to rpc
romac Apr 24, 2020
cdc2294
Rename Trace::run to Trace::collect
romac Apr 24, 2020
be3012f
Return meaningful data in errors
romac Apr 24, 2020
6c8a011
Proper error handling with thiserror+anomaly
romac Apr 24, 2020
333afd5
Make events PartialEq+Eq
romac Apr 24, 2020
93dbe53
Implement verifier
romac Apr 24, 2020
1f325e0
Implement scheduler and bisection
romac Apr 24, 2020
93b148b
Remove write access to trusted store for scheduler
romac Apr 24, 2020
614c870
Add a couple of FIXMEs
romac Apr 24, 2020
ded21c9
Formatting
romac Apr 24, 2020
745c001
Fix clippy warnings
romac Apr 24, 2020
0969b7b
Fix misplaced attribute
romac Apr 24, 2020
5948c7b
Enable VerificationPredicates to be made into a trait object
romac Apr 27, 2020
0661ee9
Allow cloning TSReader
romac Apr 27, 2020
de049fb
Shorter method name
romac Apr 27, 2020
799caaa
Decouple components using Router trait
romac Apr 28, 2020
79b26c6
Silence a couple Clippy warnings
romac Apr 28, 2020
66ef29f
Cleanup trace module
romac Apr 28, 2020
ede2b76
Revamp errors
romac Apr 28, 2020
884eebd
Revamp error, part 2
romac Apr 28, 2020
af46005
Bundle verification options together
romac Apr 28, 2020
fcb8c82
Cleanup
romac Apr 28, 2020
18f22ac
Use output enum for all components
romac Apr 28, 2020
e1158a8
Split queries out
romac Apr 29, 2020
a50286f
Rewrite using coroutines
romac Apr 30, 2020
3fd1d2a
cleanup
romac Apr 30, 2020
e309862
Add fork detector prototype
romac Apr 30, 2020
64a322f
Add stub example
romac Apr 30, 2020
0ce586c
Add traits to abstract of each concrete component
romac Apr 30, 2020
eca7872
Add actual commit to Commit struct
romac Apr 30, 2020
4c9ea47
Refactor and simplify
romac Apr 30, 2020
51f6939
Implement Store::latest
romac May 1, 2020
e789f8b
Better verification loop
romac May 1, 2020
cab2d44
Add pre/post conditions to demuxer::verify
romac May 1, 2020
a77f1fc
Add contract for schedule
romac May 1, 2020
2b80f14
Convert between tendermint and spike types
romac May 1, 2020
811500f
Working example
romac May 1, 2020
8f7cff0
Better working version
romac May 1, 2020
2173ec8
Implement production header hasher
romac May 4, 2020
dc1e104
Re-add proper Error type for whole client
romac May 5, 2020
d3ffe43
Cleanup
romac May 5, 2020
e43aca3
Add peers to demuxer state
romac May 5, 2020
c27ec46
Cleanup
romac May 5, 2020
bd9971c
Trace blocks needed for verification of a target block
romac May 5, 2020
2ffb7da
Split validation and trust check into their own top-level predicates
romac May 6, 2020
417be25
Add provider to LightBlock struct
romac May 6, 2020
2fa229b
Split overlap verification into its own verifier input
romac May 6, 2020
bef36ab
Cleanup
romac May 6, 2020
c31e5fa
Don't mix verifier and scheduler concerns
romac May 7, 2020
8d0db51
Validate commits and compute actual voting power
romac May 7, 2020
eaa4abf
Cleanup
romac May 7, 2020
f9817e8
Remove scheduler events
romac May 7, 2020
a23b2d9
Remove verifier events
romac May 7, 2020
63c6147
Remove fork detector events
romac May 7, 2020
c1935ec
Remove IO events
romac May 7, 2020
d5e7ca2
Cleanup
romac May 7, 2020
3c19d22
Simplify code flow by using an iterator of highest trusted states
romac May 7, 2020
40900e9
Fix example
romac May 7, 2020
772a16f
Update example to prod implementations
romac May 7, 2020
2b5de61
Stop verification when reaching already trusted state
romac May 7, 2020
5b668cb
Use height of fetched header to fetch validators (avoids issues when …
romac May 7, 2020
6aa6baf
Allow tracing same block, just ignore it
romac May 7, 2020
38d1e01
Better error reporting
romac May 7, 2020
fd94323
Fix bug in is_monotonic_height
romac May 7, 2020
9ce271b
Shorten ProductionPredicates name
romac May 7, 2020
7fb8f32
Cleanup
romac May 11, 2020
cec952f
Port over single-step tests
romac May 11, 2020
ceae1df
Port bisection tests
romac May 11, 2020
483077d
Move test types into their own module
romac May 12, 2020
29edfc0
Refactor
romac May 14, 2020
2485f76
Fix bug in validators overlap check
romac May 14, 2020
c462e05
Refactor LightStore, and introduce proper contracts
romac May 15, 2020
0853896
Cleanup
romac May 15, 2020
3a71fad
Simply LightStore trait
romac May 18, 2020
742441f
Use tendermint::node::Id as PeerId, as per the spec
romac May 18, 2020
e978af0
Add LightStore::update method
romac May 18, 2020
5605ef7
Fix clippy warnings
romac May 18, 2020
489cfbd
Extract get_or_fetch_block method from demuxer loop
romac May 18, 2020
c915d0a
Rename Demuxer to LightClient
romac May 18, 2020
202bdc1
Rename LightClientOptions to Options
romac May 18, 2020
0988f61
Implement on-disk store backed by Sled
romac May 19, 2020
3602b08
Formatting
romac May 19, 2020
56c020d
Cleanup
romac May 19, 2020
076932f
Properly implement SledStore::update
romac May 19, 2020
6b70979
Cleanup
romac May 20, 2020
e8b23fc
Add LightClient CLI to continuously pull headers from a node
romac May 20, 2020
6777956
Fix tests
romac May 25, 2020
8bbb194
Merge branch 'master' into romac/light-spike-bis
romac May 25, 2020
948ecb4
Adapt test to new JSON files organization
romac May 25, 2020
a17beb7
Rename light-spike crate to light-client
romac May 26, 2020
5323bb8
Turn production predicates into default trait impl
romac May 26, 2020
e287152
Comment out provider field of LightBlock until conformance tests are …
romac May 26, 2020
3ecdec2
Refactor is_within_trust_period to better match the spec
romac May 26, 2020
4934cbc
Add core verification loop invariant
romac May 26, 2020
862ff72
WIP: Documentation
romac May 26, 2020
7b39443
Merge branch 'master' into romac/light-spike-bis
romac May 26, 2020
5f3f914
Make cargo fmt happy
romac May 26, 2020
97e27b1
Make clippy happy
romac May 26, 2020
1f12e72
Re-enable `provider` field in LightBlock struct
romac May 27, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yet another binary encoding 😱 😄 I assume this is the most reasonable choice to use in combination with sled?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have to admit I didn't give it too much thought. I initially considered just serializing keys and values to JSON and then to bytes, but that seemed a bit wasteful, so I figured using a dedicated binary encoding was better. In the end, aside from the extra dependencies, the choice of encoding does not matter since it is internal to the light store and specific to the choice of sled as a database. But I'd be happy to discuss alternatives, either for the binary encoding, or for sled :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a big deal but couldnt prost work here for proto3?


[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
}
}
131 changes: 131 additions & 0 deletions light-client/src/components/io.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
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>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice to have the abstraction of peerIDs here from the onset 👍

}

// #[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