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

MVP Generic Sequential & Skipping Lite Client Verifiers #31

Merged
merged 39 commits into from
Nov 28, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
0ca7e23
cargo workspace with tendermint crate
ebuchman Sep 7, 2019
a22ae6e
initial lite client traits
ebuchman Sep 8, 2019
9496c7c
comments
ebuchman Sep 8, 2019
8c1e94b
use associated types
ebuchman Sep 8, 2019
1c408c5
remove Validator.id()
ebuchman Sep 8, 2019
64966f5
remove block_id
ebuchman Sep 8, 2019
7d31cbc
break up files into types.rs and sequential.rs
ebuchman Sep 8, 2019
111c722
verify_commit_full -> verify_commit
ebuchman Sep 8, 2019
354e6bd
error types
ebuchman Sep 9, 2019
08b5425
skipping verifier
ebuchman Sep 9, 2019
58836b6
fmt
ebuchman Sep 9, 2019
b40087f
update comments
ebuchman Sep 10, 2019
9cbe108
fmt
ebuchman Sep 10, 2019
1dd09b1
fix some names
ebuchman Sep 15, 2019
90bdfff
consolidate sequential and skipping and clean more
ebuchman Sep 15, 2019
924b1be
update names, remove next vals
ebuchman Sep 15, 2019
c3dc678
intersection -> trusting
ebuchman Sep 15, 2019
211ea55
Merge remote-tracking branch 'origin/master' into bucky/lite
ebuchman Sep 15, 2019
05549e4
Validators -> ValidatorSet
ebuchman Sep 15, 2019
6bfc8b8
Update lite/src/verifier.rs
ebuchman Sep 23, 2019
5c8a7dd
add comment about trust level
ebuchman Oct 2, 2019
5d4650c
Merge branch 'master' into bucky/lite
ebuchman Oct 3, 2019
c48f5b4
add more comments
ebuchman Oct 3, 2019
4bb3f00
into_iter -> iter
ebuchman Oct 3, 2019
f521f04
into_vec -> iter
ebuchman Oct 3, 2019
a28f145
more comments
ebuchman Oct 3, 2019
23be147
Revert "into_vec -> iter"
ebuchman Oct 29, 2019
93e98d2
note about into_vec vs. iter
ebuchman Oct 29, 2019
35b511a
Use concrete basic types for time, hash, bytes, validator id
liamsi Oct 29, 2019
f07f97d
cargo fmt
liamsi Oct 29, 2019
b5d0a37
make the lite client a module instead of a separate crate:
liamsi Oct 29, 2019
8dcc1e4
clippy: ineffective operation `total_power * 1`
liamsi Oct 29, 2019
b23d745
uncomment `expired` logic
liamsi Oct 29, 2019
2e9f2d6
tests wip
ebuchman Oct 30, 2019
1fb9a0f
Merge pull request #51 from interchainio/ismail/lite_concrete_types
ebuchman Nov 1, 2019
dd0347a
Merge branch 'bucky/lite' of https://github.com/interchainio/tendermi…
ebuchman Nov 1, 2019
2b44899
Merge branch 'master' into bucky/lite
ebuchman Nov 1, 2019
737181b
remove mock tests for now
ebuchman Nov 1, 2019
53d61eb
address comments from review
ebuchman Nov 1, 2019
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
2 changes: 1 addition & 1 deletion tendermint/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ authors = [
]

[badges]
circle-ci = { repository = "tendermint/kms" }
circle-ci = { repository = "interchainio/tendermint-rs" }

[dependencies]
byteorder = { version = "1.2" }
Expand Down
25 changes: 23 additions & 2 deletions tendermint/src/block/header.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
//! Block headers

use crate::{account, block, chain, Hash, Time};
use crate::{account, block, chain, lite, Hash, Time};
use {
crate::serializers,
serde::{Deserialize, Serialize},
Expand Down Expand Up @@ -70,6 +69,28 @@ pub struct Header {
pub proposer_address: account::Id,
}

impl lite::Header for Header {
fn height(&self) -> block::Height {
self.height
}

fn bft_time(&self) -> Time {
self.time
}

fn validators_hash(&self) -> Hash {
self.validators_hash
}

fn next_validators_hash(&self) -> Hash {
unimplemented!()
}

fn hash(&self) -> Hash {
unimplemented!()
}
}

/// `Version` contains the protocol version for the blockchain and the
/// application.
///
Expand Down
2 changes: 2 additions & 0 deletions tendermint/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ pub mod consensus;
pub mod evidence;
pub mod genesis;
pub mod hash;
#[allow(dead_code, missing_docs)]
pub mod lite;
pub mod merkle;
mod moniker;
pub mod net;
Expand Down
4 changes: 4 additions & 0 deletions tendermint/src/lite.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
pub use self::types::*;
pub mod types;
pub mod verifier;
pub use self::verifier::*;
115 changes: 115 additions & 0 deletions tendermint/src/lite/types.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// can we abstract this away and use a generic identifier instead ?
// Ie. something that just implements Eq ?
use crate::account::Id;

use crate::block::Height;
use crate::Hash;
#[allow(clippy::all)]
use crate::Time;

/// TrustedState stores the latest state trusted by a lite client,
/// including the last header and the validator set to use to verify
/// the next header.
pub struct TrustedState<H, V>
where
H: Header,
V: ValidatorSet,
{
pub last_header: H, // height H-1
pub validators: V, // height H
}

/// Header contains meta data about the block -
/// the height, the time, the hash of the validator set
/// that should sign this header, and the hash of the validator
/// set that should sign the next header.
pub trait Header {
fn height(&self) -> Height;
fn bft_time(&self) -> Time;
fn validators_hash(&self) -> Hash;
fn next_validators_hash(&self) -> Hash;

/// Hash of the header (ie. the hash of the block).
fn hash(&self) -> Hash;
}

/// ValidatorSet is the full validator set.
/// It exposes its hash, which should match whats in a header,
/// and its total power. It also has an underlying
/// Validator type which can be used for verifying signatures.
pub trait ValidatorSet {
type Validator: Validator;

/// Hash of the validator set.
fn hash(&self) -> Hash;

/// Total voting power of the set
fn total_power(&self) -> u64;

/// For iterating over the underlying validators.
/// NOTE: can try to make this iter() but requires
/// a `type ValidatorIter: ExactSizeIterator<Item = Self::Validator>`
/// which seems to greatly complicate implementation ...
fn into_vec(&self) -> Vec<Self::Validator>;
}

/// ValidatorSetLookup allows validator to be fetched via their ID
/// (ie. their address).
pub trait ValidatorSetLookup: ValidatorSet {
fn validator(&self, val_id: Id) -> Option<Self::Validator>;
}

/// Validator has a voting power and can verify
/// its own signatures. Note it must have implicit access
/// to its public key material to verify signatures.
pub trait Validator {
fn power(&self) -> u64;
fn verify_signature(&self, sign_bytes: &[u8], signature: &[u8]) -> bool;
}

/// Commit is proof a Header is valid.
/// It has an underlying Vote type with the relevant vote data
/// for verification.
pub trait Commit {
type Vote: Vote;

/// Hash of the header this commit is for.
fn header_hash(&self) -> Hash;

/// Return the underlying votes for iteration.
/// All votes here are for the correct block id -
/// we ignore absent votes and votes for nil here.
/// NOTE: we may want to check signatures for nil votes,
/// and thus use an ternary enum here instead of the binary Option.
fn into_vec(&self) -> Vec<Option<Self::Vote>>;
}

/// Vote contains the data to verify a validator voted correctly in the commit.
/// In an ideal world, votes contain only signatures, and all votes are for the same
/// message. For now, Tendermint votes also sign over the validator's local timestamp,
/// so each vote is for a slightly different message.
/// Note the Vote must also know which validator it is from.
/// Note that implementers are responsible for ensuring that the vote's sign_bytes
/// are a function of the block id and the chain id. These don't appear directly in the trait
/// since the particular values aren't relevant to correctness here - the Vote is already
/// within an enum at the VoteSet level indicating which block it is for, and the chain id
/// is only necessary to avoid slashing in the multi chain context.
pub trait Vote {
fn validator_id(&self) -> Id;
fn sign_bytes(&self) -> &[u8];
fn signature(&self) -> &[u8];
}

pub enum Error {
Expired,
NonSequentialHeight,
NonIncreasingHeight,

InvalidValidatorSet,
InvalidNextValidatorSet,
InvalidCommitValue, // commit is not for the header we expected
InvalidCommitLength,
InvalidSignature,

InsufficientVotingPower,
}
193 changes: 193 additions & 0 deletions tendermint/src/lite/verifier.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
#[allow(clippy::all)]
use crate::lite::{Commit, Error, Header, Validator, ValidatorSet, ValidatorSetLookup, Vote};
use crate::Time;
use std::time::Duration;

/// Returns an error if the header has expired according to the given
/// trusting_period and current time. If so, the verifier must be reset subjectively.
/// NOTE: this doesn't belong here. It should be called by something that handles whether to trust
/// a verified commit. Verified here is really just about the header/commit/validators. Time is an
/// external concern :)
fn expired<H>(last_header: &H, trusting_period: Duration, now: Time) -> Result<(), Error>
where
H: Header,
{
if let Ok(passed) = now.duration_since(last_header.bft_time()) {
if passed > trusting_period {
return Err(Error::Expired);
}
}
// TODO move this out of the verifier and deal with overflows etc (proper err handling)
Ok(())
}

fn validate_next_vals<H, V>(header: H, next_vals: &V) -> Result<(), Error>
where
H: Header,
V: ValidatorSet,
{
// ensure the next validators in the header matches what was supplied.
if header.next_validators_hash() != next_vals.hash() {
return Err(Error::InvalidNextValidatorSet);
}

Ok(())
}

// Validate the validators and commit against the header.
fn validate_vals_and_commit<H, V, C>(header: H, commit: &C, vals: &V) -> Result<(), Error>
where
H: Header,
V: ValidatorSet,
C: Commit,
{
// ensure the validators in the header matches what we expect from our state.
if header.validators_hash() != vals.hash() {
return Err(Error::InvalidValidatorSet);
}

// ensure the commit matches the header.
if header.hash() != commit.header_hash() {
return Err(Error::InvalidCommitValue);
}

Ok(())
}

/// Verify the commit is valid from the given validators for the header.
pub fn verify<H, V, C>(header: H, commit: C, validators: V) -> Result<(), Error>
where
H: Header,
V: ValidatorSet,
C: Commit,
{
if let Err(e) = validate_vals_and_commit(header, &commit, &validators) {
return Err(e);
}

// ensure that +2/3 validators signed correctly
verify_commit_full(&validators, commit)
}

/// Verify the commit is trusted according to the last validators and is valid
/// from the current validators for the header.
pub fn verify_trusting<H, V, C>(
header: H,
commit: C,
last_validators: V,
validators: V,
) -> Result<(), Error>
where
H: Header,
V: ValidatorSetLookup,
C: Commit,
{
// NOTE it might be more prudent to do the cheap validations first
// before we even call verify_commit_trusting, but not doing that
// makes the code cleaner and allows us to just call verify directly.

// ensure that +1/3 of last trusted validators signed correctly
if let Err(e) = verify_commit_trusting(&last_validators, &commit) {
return Err(e);
}

// perform same verification as in sequential case
verify(header, commit, validators)
}

/// Verify that +2/3 of the correct validator set signed this commit.
/// NOTE: these validators are expected to be the correct validators for the commit.
fn verify_commit_full<V, C>(vals: &V, commit: C) -> Result<(), Error>
where
V: ValidatorSet,
C: Commit,
{
let total_power = vals.total_power();
let mut signed_power: u64 = 0;

let vals_vec = vals.into_vec();
let commit_vec = commit.into_vec();

if vals_vec.len() != commit_vec.len() {
return Err(Error::InvalidCommitLength);
}

// The vals and commit have a 1-to-1 correspondence.
// This means we don't need the validator IDs or to do any lookup,
// we can just zip the iterators.
let vals_iter = vals_vec.iter();
let commit_iter = commit_vec.iter();
for (val, vote_opt) in vals_iter.zip(commit_iter) {
// skip absent and nil votes
// NOTE: do we want to check the validity of votes
// for nil ?
let vote = match vote_opt {
Some(v) => v,
None => continue,
};

// check vote is valid from validator
if !val.verify_signature(vote.sign_bytes(), vote.signature()) {
return Err(Error::InvalidSignature);
}
signed_power += val.power();
}

// check the signers account for +2/3 of the voting power
if signed_power * 3 <= total_power * 2 {
return Err(Error::InsufficientVotingPower);
}

Ok(())
}

/// Verify that +1/3 of the given validator set signed this commit.
/// NOTE the given validators do not necessarily correspond to the validator set for this commit,
/// but there may be some intersection.
/// TODO: this should take a "trust_level" param to allow clients to require more
/// than +1/3. How should this be defined semantically? Probably shouldn't be a float, maybe
/// and enum of options, eg. 1/3, 1/2, 2/3, 1 ?
fn verify_commit_trusting<V, C>(validators: &V, commit: &C) -> Result<(), Error>
where
V: ValidatorSetLookup,
C: Commit,
{
let total_power = validators.total_power();
let mut signed_power: u64 = 0;

// NOTE we don't know the validators that committed this block,
// so we have to check for each vote if its validator is already known.
let commit_vec = commit.into_vec();
let commit_iter = commit_vec.iter();
for vote_opt in commit_iter {
// skip absent and nil votes
// NOTE: do we want to check the validity of votes
// for nil ?
let vote = match vote_opt {
Some(v) => v,
None => continue,
};

// check if this vote is from a known validator
let val_id = vote.validator_id();
let val = match validators.validator(val_id) {
Some(v) => v,
None => continue,
};

// check vote is valid from validator
if !val.verify_signature(vote.sign_bytes(), vote.signature()) {
return Err(Error::InvalidSignature);
}
signed_power += val.power();
}

// check the signers account for +1/3 of the voting power
// TODO: incorporate "trust_level" in here to possibly increase
// beyond 1/3.
if signed_power * 3 <= total_power {
return Err(Error::InsufficientVotingPower);
}

Ok(())
}