-
Notifications
You must be signed in to change notification settings - Fork 225
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
Changes from all commits
Commits
Show all changes
102 commits
Select commit
Hold shift + click to select a range
cceaa70
Rework predicates
romac a982821
WIP: Add tracing
romac 7e2a9d5
Fix verification procedure
romac 8509a13
Rename requester component to rpc
romac cdc2294
Rename Trace::run to Trace::collect
romac be3012f
Return meaningful data in errors
romac 6c8a011
Proper error handling with thiserror+anomaly
romac 333afd5
Make events PartialEq+Eq
romac 93dbe53
Implement verifier
romac 1f325e0
Implement scheduler and bisection
romac 93b148b
Remove write access to trusted store for scheduler
romac 614c870
Add a couple of FIXMEs
romac ded21c9
Formatting
romac 745c001
Fix clippy warnings
romac 0969b7b
Fix misplaced attribute
romac 5948c7b
Enable VerificationPredicates to be made into a trait object
romac 0661ee9
Allow cloning TSReader
romac de049fb
Shorter method name
romac 799caaa
Decouple components using Router trait
romac 79b26c6
Silence a couple Clippy warnings
romac 66ef29f
Cleanup trace module
romac ede2b76
Revamp errors
romac 884eebd
Revamp error, part 2
romac af46005
Bundle verification options together
romac fcb8c82
Cleanup
romac 18f22ac
Use output enum for all components
romac e1158a8
Split queries out
romac a50286f
Rewrite using coroutines
romac 3fd1d2a
cleanup
romac e309862
Add fork detector prototype
romac 64a322f
Add stub example
romac 0ce586c
Add traits to abstract of each concrete component
romac eca7872
Add actual commit to Commit struct
romac 4c9ea47
Refactor and simplify
romac 51f6939
Implement Store::latest
romac e789f8b
Better verification loop
romac cab2d44
Add pre/post conditions to demuxer::verify
romac a77f1fc
Add contract for schedule
romac 2b80f14
Convert between tendermint and spike types
romac 811500f
Working example
romac 8f7cff0
Better working version
romac 2173ec8
Implement production header hasher
romac dc1e104
Re-add proper Error type for whole client
romac d3ffe43
Cleanup
romac e43aca3
Add peers to demuxer state
romac c27ec46
Cleanup
romac bd9971c
Trace blocks needed for verification of a target block
romac 2ffb7da
Split validation and trust check into their own top-level predicates
romac 417be25
Add provider to LightBlock struct
romac 2fa229b
Split overlap verification into its own verifier input
romac bef36ab
Cleanup
romac c31e5fa
Don't mix verifier and scheduler concerns
romac 8d0db51
Validate commits and compute actual voting power
romac eaa4abf
Cleanup
romac f9817e8
Remove scheduler events
romac a23b2d9
Remove verifier events
romac 63c6147
Remove fork detector events
romac c1935ec
Remove IO events
romac d5e7ca2
Cleanup
romac 3c19d22
Simplify code flow by using an iterator of highest trusted states
romac 40900e9
Fix example
romac 772a16f
Update example to prod implementations
romac 2b5de61
Stop verification when reaching already trusted state
romac 5b668cb
Use height of fetched header to fetch validators (avoids issues when …
romac 6aa6baf
Allow tracing same block, just ignore it
romac 38d1e01
Better error reporting
romac fd94323
Fix bug in is_monotonic_height
romac 9ce271b
Shorten ProductionPredicates name
romac 7fb8f32
Cleanup
romac cec952f
Port over single-step tests
romac ceae1df
Port bisection tests
romac 483077d
Move test types into their own module
romac 29edfc0
Refactor
romac 2485f76
Fix bug in validators overlap check
romac c462e05
Refactor LightStore, and introduce proper contracts
romac 0853896
Cleanup
romac 3a71fad
Simply LightStore trait
romac 742441f
Use tendermint::node::Id as PeerId, as per the spec
romac e978af0
Add LightStore::update method
romac 5605ef7
Fix clippy warnings
romac 489cfbd
Extract get_or_fetch_block method from demuxer loop
romac c915d0a
Rename Demuxer to LightClient
romac 202bdc1
Rename LightClientOptions to Options
romac 0988f61
Implement on-disk store backed by Sled
romac 3602b08
Formatting
romac 56c020d
Cleanup
romac 076932f
Properly implement SledStore::update
romac 6b70979
Cleanup
romac e8b23fc
Add LightClient CLI to continuously pull headers from a node
romac 6777956
Fix tests
romac 8bbb194
Merge branch 'master' into romac/light-spike-bis
romac 948ecb4
Adapt test to new JSON files organization
romac a17beb7
Rename light-spike crate to light-client
romac 5323bb8
Turn production predicates into default trait impl
romac e287152
Comment out provider field of LightBlock until conformance tests are …
romac 3ecdec2
Refactor is_within_trust_period to better match the spec
romac 4934cbc
Add core verification loop invariant
romac 862ff72
WIP: Documentation
romac 7b39443
Merge branch 'master' into romac/light-spike-bis
romac 5f3f914
Make cargo fmt happy
romac 97e27b1
Make clippy happy
romac 1f12e72
Re-enable `provider` field in LightBlock struct
romac File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 <[email protected]>"] | ||
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>, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 forsled
:)There was a problem hiding this comment.
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?