From 1de37708f8a8174a8ade0ddb9f9ae7cb788e90b3 Mon Sep 17 00:00:00 2001 From: Ismail Khoffi Date: Sun, 29 Dec 2019 19:45:15 +0100 Subject: [PATCH 1/5] Light client: bisection, store, requester (#100) * Add bisection to light client module: - implement `can_trust` according to the latest spec CanTrustBisection (wip) - add a `Requester` and a `Store` trait - sue refs in parameters (as they are used in several places and we don't want to Copy) * Group params h1 & ha_next_vals into a type `TrustedState` - reduce params to clippy threshold (7); still a lot ... - rename vars for better readability * Add VerifyHeader logic from spec: - rename expired to is_within_trust_period to be closer to the spec - use TrusteState trait in check_support params - use TrusteState in lite tests * Review: doc improvements and minor improvements on naming * Review: doc improvements * More doc improvements * Minor doc improvement * MockHeader and test_is_within_trust_period (#104) * MockHeader and test_is_within_trust_period * remove Validator trait; make ValidatorSet associated to Commit (#105) * remove Validator trait; make ValidatorSet associated to Commit * remove stale comment * Fix clippy errors (#108) * Review comments: (#107) * Review comments: - update some comments / documentation - verify vals (not next vals) - store header and vals in trusted state * One offs & renaming related to trusted state * Rename & remove redundant store of trusted state: c - an_trust -> can_trust_bisection - remove a redundant check for trust period - remove redundant adding state to the store (added TODO) * Bucky/some light follow up (#109) * use ? instead of return Err(err) * remove unused errors * move a validation and add TODOs about them * check_support returns early for sequential case * drop let _ = * add comment to voting_power_in about signers Co-authored-by: Ethan Buchman --- tendermint/src/block/commit.rs | 2 +- tendermint/src/block/signed_header.rs | 21 +- tendermint/src/consensus/state.rs | 20 +- tendermint/src/lite/types.rs | 102 +++++---- tendermint/src/lite/verifier.rs | 316 ++++++++++++++++++++++---- tendermint/src/validator.rs | 28 ++- tendermint/tests/lite.rs | 48 +++- 7 files changed, 407 insertions(+), 130 deletions(-) diff --git a/tendermint/src/block/commit.rs b/tendermint/src/block/commit.rs index 9ecd4a0e7..b61f3608e 100644 --- a/tendermint/src/block/commit.rs +++ b/tendermint/src/block/commit.rs @@ -32,7 +32,7 @@ impl Precommits { /// Convert this collection of precommits into a vector pub fn into_vec(self) -> Vec> { - self.0.clone() + self.0 } /// Iterate over the precommits in the collection diff --git a/tendermint/src/block/signed_header.rs b/tendermint/src/block/signed_header.rs index 42689f9ee..1d0fb717e 100644 --- a/tendermint/src/block/signed_header.rs +++ b/tendermint/src/block/signed_header.rs @@ -1,12 +1,7 @@ //! SignedHeader contains commit and and block header. //! It is what the rpc endpoint /commit returns and hence can be used by a //! light client. -use crate::{ - block, hash, lite, - lite::types::Validator, - lite::{Error, ValidatorSet}, - vote::SignedVote, -}; +use crate::{block, hash, lite, lite::Error, validator::Set, vote::SignedVote}; use serde::{Deserialize, Serialize}; /// Signed block headers @@ -54,14 +49,16 @@ impl SignedHeader { } impl lite::Commit for SignedHeader { + type ValidatorSet = Set; + fn header_hash(&self) -> hash::Hash { self.commit.block_id.hash } + fn votes_len(&self) -> usize { + self.commit.precommits.len() + } - fn voting_power_in(&self, validators: &V) -> Result - where - V: ValidatorSet, - { + fn voting_power_in(&self, validators: &Set) -> Result { // 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 mut signed_power = 0u64; @@ -93,8 +90,4 @@ impl lite::Commit for SignedHeader { Ok(signed_power) } - - fn votes_len(&self) -> usize { - self.commit.precommits.len() - } } diff --git a/tendermint/src/consensus/state.rs b/tendermint/src/consensus/state.rs index 00fe29795..37f18d499 100644 --- a/tendermint/src/consensus/state.rs +++ b/tendermint/src/consensus/state.rs @@ -49,18 +49,14 @@ impl fmt::Display for State { impl Ord for State { fn cmp(&self, other: &State) -> Ordering { - if self.height < other.height { - Ordering::Less - } else if self.height == other.height { - if self.round < other.round { - Ordering::Less - } else if self.round == other.round { - self.step.cmp(&other.step) - } else { - Ordering::Greater - } - } else { - Ordering::Greater + match self.height.cmp(&other.height) { + Ordering::Greater => Ordering::Greater, + Ordering::Less => Ordering::Less, + Ordering::Equal => match self.round.cmp(&other.round) { + Ordering::Greater => Ordering::Greater, + Ordering::Less => Ordering::Less, + Ordering::Equal => self.step.cmp(&other.step), + }, } } } diff --git a/tendermint/src/lite/types.rs b/tendermint/src/lite/types.rs index e55860d6b..9d0e74e26 100644 --- a/tendermint/src/lite/types.rs +++ b/tendermint/src/lite/types.rs @@ -1,14 +1,6 @@ //! All traits that are necessary and need to be implemented to use the main //! verification logic in `super::verifier` for a light client. -// TODO can we abstract this away and use a generic identifier instead ? -// Ie. something that just implements Eq ? -// (Ismail): a really easy solution would be have a trait that expects an -// as_bytes(&self) -> &[u8] method. It's unlikely that a hash won't be -// representable as bytes, or an Id (that is basically also a hash) -// but this feels a a bit like cheating -use crate::account::Id; - use crate::block::Height; use crate::Hash; @@ -16,15 +8,19 @@ use failure::_core::fmt::Debug; use std::time::SystemTime; /// 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 -where - H: Header, - V: ValidatorSet, -{ - pub last_header: H, // height H-1 - pub validators: V, // height H +/// including the last header (at height h-1) and the validator set +/// (at height h) to use to verify the next header. +pub trait TrustedState { + type LastHeader: SignedHeader; + type ValidatorSet: ValidatorSet; + + /// Initialize the TrustedState with the given signed header and validator set. + /// Note that if the height of the passed in header is h-1, the passed in validator set + /// must have been requested for height h. + fn new(last_header: &Self::LastHeader, vals: &Self::ValidatorSet) -> Self; + + fn last_header(&self) -> &Self::LastHeader; // height H-1 + fn validators(&self) -> &Self::ValidatorSet; // height H } /// SignedHeader bundles a Header and a Commit for convenience. @@ -61,17 +57,12 @@ pub trait Header: Debug { /// It also provides a lookup method to fetch a validator by /// its identifier. 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; - /// Fetch validator via their ID (ie. their address). - fn validator(&self, val_id: Id) -> Option; - /// Return the number of validators in this validator set. fn len(&self) -> usize; @@ -79,30 +70,25 @@ pub trait ValidatorSet { fn is_empty(&self) -> bool; } -/// 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 ValidatorSet: ValidatorSet; + /// Hash of the header this commit is for. fn header_hash(&self) -> Hash; /// Compute the voting power of the validators that correctly signed the commit, - /// have according to their voting power in the passed in validator set. + /// according to their voting power in the passed in validator set. /// Will return an error in case an invalid signature was included. /// - /// This method corresponds to the (pure) auxiliary function int the spec: + /// This method corresponds to the (pure) auxiliary function in the spec: /// `votingpower_in(signers(h.Commit),h.Header.V)`. - fn voting_power_in(&self, vals: &V) -> Result - where - V: ValidatorSet; + /// Note this expects the Commit to be able to compute `signers(h.Commit)`, + /// ie. the identity of the validators that signed it, so they + /// can be cross-referenced with the given `vals`. + fn voting_power_in(&self, vals: &Self::ValidatorSet) -> Result; /// Return the number of votes included in this commit /// (including nil/empty votes). @@ -122,18 +108,54 @@ pub trait TrustThreshold { } } -#[derive(Debug)] +/// Requester can be used to request `SignedHeaders` and `ValidatorSet`s for a +/// given height, e.g., by talking to a tendermint fullnode through RPC. +pub trait Requester { + // TODO(Liamsi): consider putting this trait and the Store into a separate module / file... + type SignedHeader: SignedHeader; + type ValidatorSet: ValidatorSet; + + /// Request the signed header at height h. + fn signed_header(&self, h: H) -> Result + where + H: Into; + + /// Request the validator set at height h. + fn validator_set(&self, h: H) -> Result + where + H: Into; +} + +/// This store can be used to store all the headers that have passed basic verification +/// and that are within the light client's trust period. +pub trait Store { + type TrustedState: TrustedState; + + /// Add this state (header at height h, validators at height h+1) as trusted to the store. + fn add(&mut self, trusted: &Self::TrustedState) -> Result<(), Error>; + + /// Retrieve the trusted state at height h if it exists. + /// If it does not exist return an error. + fn get(&self, h: Height) -> Result<&Self::TrustedState, Error>; + + /// Retrieve the trusted signed header with the largest height h' with h' <= h, if it exists. + /// If it does not exist return an error. + fn get_smaller_or_equal(&self, h: Height) -> Result; +} + +#[derive(Debug, PartialEq)] pub enum Error { Expired, DurationOutOfRange, - NonSequentialHeight, - NonIncreasingHeight, + + InvalidSignature, // TODO: deduplicate with ErrorKind::SignatureInvalid InvalidValidatorSet, InvalidNextValidatorSet, InvalidCommitValue, // commit is not for the header we expected InvalidCommitLength, - InvalidSignature, - InsufficientVotingPower, + InsufficientVotingPower, // TODO(Liamsi): change to same name as spec if this changes (curently ErrTooMuchChange) + + RequestFailed, } diff --git a/tendermint/src/lite/verifier.rs b/tendermint/src/lite/verifier.rs index 436edf6fd..46d36015d 100644 --- a/tendermint/src/lite/verifier.rs +++ b/tendermint/src/lite/verifier.rs @@ -8,18 +8,26 @@ //! // looks using the types and methods in this crate/module. //!``` -use crate::lite::{Commit, Error, Header, SignedHeader, TrustThreshold, ValidatorSet}; +use crate::block::Height; +use crate::lite::{ + Commit, Error, Header, Requester, SignedHeader, Store, TrustThreshold, TrustedState, + ValidatorSet, +}; use std::time::{Duration, SystemTime}; /// 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. -pub fn expired(last_header: &H, trusting_period: Duration, now: SystemTime) -> Result<(), Error> +pub fn is_within_trust_period( + last_header: &H, + trusting_period: &Duration, + now: &SystemTime, +) -> Result<(), Error> where H: Header, { match now.duration_since(last_header.bft_time().into()) { Ok(passed) => { - if passed > trusting_period { + if passed > *trusting_period { return Err(Error::Expired); } Ok(()) @@ -42,66 +50,93 @@ where } /// Captures the skipping condition, i.e., it defines when we can trust the header -/// h2 based on header h1. -/// Note that h1 and h2 have already passed basic validation by calling `verify`. +/// h2 based on a known trusted state. +/// Note that the trusted header included in the trusted state and h2 have already +/// passed basic validation by calling `verify`. /// `Error::InsufficientVotingPower`is returned when there is not enough intersection /// between validator sets to have skipping condition true. -pub fn check_support( - h1: &S, - h1_next_vals: &V, - h2: &S, - trust_threshold: L, - trusting_period: Duration, - now: SystemTime, +pub fn check_support( + trusted_state: &TS, + h2: &SH, + trust_threshold: &L, + trusting_period: &Duration, + now: &SystemTime, ) -> Result<(), Error> where - S: SignedHeader, - V: ValidatorSet, + TS: TrustedState, + SH: SignedHeader, L: TrustThreshold, + C: Commit, { - if let Err(err) = expired(h1.header(), trusting_period, now) { - return Err(err); - } + let h1 = trusted_state.last_header(); + let h1_next_vals = trusted_state.validators(); - if h2.header().height() == h1.header().height().increment() - && h2.header().validators_hash() != h1_next_vals.hash() - { - return Err(Error::InvalidNextValidatorSet); + // TODO(EB): can we move all these checks? it seems the + // is_within doesn't need to happen here and the sequential check + // for vals hash should be part of validate_vals_and_commit. then this function + // is basically just verify_commit_trusting. Would need to update spec as well. + // In sequential case, would still need to return early or not call check_support at all. + is_within_trust_period(h1.header(), trusting_period, now)?; + + // check the sequential case + if h2.header().height() == h1.header().height().increment() { + if h2.header().validators_hash() == h1_next_vals.hash() { + // It's sequential, so verify_commit_trusting would be + // redundant with verify since the validators are the same + // when we're not skipping. + return Ok(()); + } else { + return Err(Error::InvalidNextValidatorSet); + } } + + // check if enough trusted validators signed to skip to the new height. verify_commit_trusting(h1_next_vals, h2.commit(), trust_threshold) } /// Validate the validators and commit against the header. +// TODO(EB): consider making this a method on Commit so the details are hidden, +// and so we can remove the votes_len() method (that check would be part of the +// methods implementation). These checks aren't reflected +// explicitly in the spec yet, only in the sentence "Additional checks should +// be done in the implementation to ensure header is well formed". fn validate_vals_and_commit(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. + // ensure the header validators match these validators if header.validators_hash() != vals.hash() { return Err(Error::InvalidValidatorSet); } - // ensure the commit matches the header. + // ensure the header matches the commit if header.hash() != commit.header_hash() { return Err(Error::InvalidCommitValue); } + // ensure the validator size matches the commit size + // NOTE: this is commit structure specifc and should be + // hidden from the light client ... + if vals.len() != commit.votes_len() { + return Err(Error::InvalidCommitLength); + } + Ok(()) } /// Verify the commit is valid from the given validators for the header. -pub fn verify(signed_header: &SH, validators: &V) -> Result<(), Error> +pub fn verify(signed_header: &SH, validators: &C::ValidatorSet) -> Result<(), Error> where - SH: SignedHeader, - V: ValidatorSet, + SH: SignedHeader, + C: Commit, { let header = signed_header.header(); let commit = signed_header.commit(); - if let Err(e) = validate_vals_and_commit(header, commit, validators) { - return Err(e); - } + + // basic validatity checks that header, commit, and vals match up + validate_vals_and_commit(header, commit, validators)?; // ensure that +2/3 validators signed correctly verify_commit_full(validators, commit) @@ -109,16 +144,11 @@ where /// 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(vals: &V, commit: &C) -> Result<(), Error> +fn verify_commit_full(vals: &C::ValidatorSet, commit: &C) -> Result<(), Error> where - V: ValidatorSet, C: Commit, { let total_power = vals.total_power(); - if vals.len() != commit.votes_len() { - return Err(Error::InvalidCommitLength); - } - let signed_power = commit.voting_power_in(vals)?; // check the signers account for +2/3 of the voting power @@ -133,13 +163,12 @@ where /// NOTE the given validators do not necessarily correspond to the validator set for this commit, /// but there may be some intersection. The trust_level parameter allows clients to require more /// than +1/3 by implementing the TrustLevel trait accordingly. -pub fn verify_commit_trusting( - validators: &V, +pub fn verify_commit_trusting( + validators: &C::ValidatorSet, commit: &C, - trust_level: L, + trust_level: &L, ) -> Result<(), Error> where - V: ValidatorSet, C: Commit, L: TrustThreshold, { @@ -154,3 +183,212 @@ where Ok(()) } + +/// Returns Ok if we can trust the passed in (untrusted) header +/// based on the given trusted state, otherwise returns an Error. +fn can_trust_bisection( + trusted_state: &TS, // h1 in spec + untrusted_header: &SH, // h2 in spec + trust_threshold: &L, + trusting_period: &Duration, + now: &SystemTime, + req: &R, + store: &mut S, +) -> Result<(), Error> +where + TS: TrustedState, + SH: SignedHeader, + C: Commit, + L: TrustThreshold, + S: Store, + R: Requester, +{ + // can we trust the still untrusted header based on the given trusted state? + match check_support( + trusted_state, + untrusted_header, + trust_threshold, + trusting_period, + now, + ) { + Ok(_) => { + let untrusted_next_vals = + req.validator_set(untrusted_header.header().height().increment())?; + let untrusted_state = TS::new(untrusted_header, &untrusted_next_vals); + store.add(&untrusted_state)?; + return Ok(()); + } + Err(e) => { + if e != Error::InsufficientVotingPower { + return Err(e); + } + } + } + + // Here we can't trust the passed in untrusted header based on the known trusted state. + // Run bisection: try again with a pivot header whose height is in the + // middle of the trusted height and the desired height (h2.height). + + let trusted_header = trusted_state.last_header(); + let trusted_height: u64 = trusted_header.header().height().value(); + let untrusted_height: u64 = untrusted_header.header().height().value(); + let pivot: u64 = (trusted_height + untrusted_height) / 2; + let pivot_header = req.signed_header(pivot)?; + let pivot_vals = req.validator_set(pivot)?; + + verify(&pivot_header, &pivot_vals)?; + + // Can we trust pivot header based on trusted_state? + can_trust_bisection( + trusted_state, + &pivot_header, + trust_threshold, + trusting_period, + now, + req, + store, + )?; + // Trust the header in between the trusted and (still) untrusted height: + let pivot_next_vals = req.validator_set(pivot + 1)?; + let pivot_trusted = TS::new(&pivot_header, &pivot_next_vals); + store.add(&pivot_trusted)?; + + // Can we trust the (still) untrusted header based on the (now trusted) "pivot header"? + can_trust_bisection( + &pivot_trusted, + untrusted_header, + trust_threshold, + trusting_period, + now, + req, + store, + )?; + // Add header (and corresponding next validators) to trusted state store: + let untrusted_next_vals = req.validator_set(untrusted_header.header().height().increment())?; + let untrusted_state = TS::new(untrusted_header, &untrusted_next_vals); + store.add(&untrusted_state)?; + + Ok(()) +} + +/// This function captures the high level logic of the light client verification, i.e., +/// an application call to the light client module to (optionally download) and +/// verify a header for some height. +pub fn verify_header( + height: Height, + trust_threshold: &L, + trusting_period: &Duration, + now: &SystemTime, + req: &R, + store: &mut S, +) -> Result<(), Error> +where + TS: TrustedState, + L: TrustThreshold, + S: Store, + R: Requester, + C: Commit, + SH: SignedHeader, +{ + // Check if we already trusted a header at the given height and it didn't expire: + if let Ok(ts2) = store.get(height) { + is_within_trust_period(ts2.last_header().header(), trusting_period, now)? + } + + // We haven't trusted a header at given height yet. Request it: + let sh2 = req.signed_header(height)?; + let sh2_vals = req.validator_set(height)?; + verify(&sh2, &sh2_vals)?; + is_within_trust_period(sh2.header(), trusting_period, now)?; + + // Get the highest trusted header with height lower than sh2's. + let sh1_trusted = store.get_smaller_or_equal(height)?; + can_trust_bisection( + &sh1_trusted, + &sh2, + trust_threshold, + trusting_period, + now, + req, + store, + )?; + + // TODO(Liamsi): The spec re-checks if we are still in trust period here + // and stores the now trusted header again. + // Figure out if we want to store it here or in can_trust_bisection! + // My understanding is: with the current impl, we either bubbled up an + // error, or, successfully added sh2 (and its nex vals) to the trusted store here. + + Ok(()) +} + +mod tests { + use super::*; + use crate::{hash::Algorithm, Hash}; + use serde::Serialize; + use sha2::{Digest, Sha256}; + + #[derive(Debug, Serialize)] + struct MockHeader { + height: u64, + time: SystemTime, + vals: Hash, + next_vals: Hash, + } + + impl MockHeader { + fn new(height: u64, time: SystemTime, vals: Hash, next_vals: Hash) -> MockHeader { + MockHeader { + height, + time, + vals, + next_vals, + } + } + } + + impl Header for MockHeader { + type Time = SystemTime; + + fn height(&self) -> Height { + Height::from(self.height) + } + fn bft_time(&self) -> Self::Time { + self.time + } + fn validators_hash(&self) -> Hash { + self.vals + } + fn next_validators_hash(&self) -> Hash { + self.next_vals + } + fn hash(&self) -> Hash { + let encoded = serde_json::to_vec(self).unwrap(); + let hashed = Sha256::digest(&encoded); + Hash::new(Algorithm::Sha256, &hashed).unwrap() + } + } + + fn fixed_hash() -> Hash { + Hash::new(Algorithm::Sha256, &Sha256::digest(&[5])).unwrap() + } + + #[test] + fn test_is_within_trust_period() { + let header_time = SystemTime::UNIX_EPOCH; + let period = Duration::new(100, 0); + let now = header_time + Duration::new(10, 0); + + // less than the period, OK + let header = MockHeader::new(4, header_time, fixed_hash(), fixed_hash()); + assert!(is_within_trust_period(&header, &period, &now).is_ok()); + + // equal to the period, OK + let now = header_time + period; + assert!(is_within_trust_period(&header, &period, &now).is_ok()); + + // greater than the period, not OK + let now = header_time + period + Duration::new(1, 0); + assert!(is_within_trust_period(&header, &period, &now).is_err()); + } +} diff --git a/tendermint/src/validator.rs b/tendermint/src/validator.rs index d2583aa28..dc2090ec6 100644 --- a/tendermint/src/validator.rs +++ b/tendermint/src/validator.rs @@ -26,9 +26,17 @@ impl Set { } } -impl lite::ValidatorSet for Set { - type Validator = Info; +impl Set { + /// Returns the validator with the given Id if its in the Set. + pub fn validator(&self, val_id: account::Id) -> Option { + self.validators + .iter() + .find(|val| val.address == val_id) + .cloned() + } +} +impl lite::ValidatorSet for Set { /// Compute the Merkle root of the validator set fn hash(&self) -> Hash { let validator_bytes: Vec> = self @@ -45,13 +53,6 @@ impl lite::ValidatorSet for Set { }) } - fn validator(&self, val_id: account::Id) -> Option { - self.validators - .iter() - .find(|val| val.address == val_id) - .cloned() - } - fn len(&self) -> usize { self.validators.len() } @@ -87,12 +88,15 @@ pub struct Info { pub proposer_priority: Option, } -impl lite::Validator for Info { - fn power(&self) -> u64 { +impl Info { + /// Return the voting power of the validator. + pub fn power(&self) -> u64 { self.voting_power.value() } - fn verify_signature(&self, sign_bytes: &[u8], signature: &[u8]) -> bool { + /// Verify the given signature against the given sign_bytes using the validators + /// public key. + pub fn verify_signature(&self, sign_bytes: &[u8], signature: &[u8]) -> bool { if let Some(pk) = &self.pub_key.ed25519() { let verifier = Ed25519Verifier::from(pk); if let Ok(sig) = ed25519::Signature::from_bytes(signature) { diff --git a/tendermint/tests/lite.rs b/tendermint/tests/lite.rs index 4aba58546..a799990c9 100644 --- a/tendermint/tests/lite.rs +++ b/tendermint/tests/lite.rs @@ -1,6 +1,7 @@ use serde::{de::Error as _, Deserialize, Deserializer, Serialize}; use serde_json; use std::{fs, path::PathBuf}; +use tendermint::lite::TrustedState; use tendermint::{block::signed_header::SignedHeader, lite, validator, validator::Set, Time}; #[derive(Serialize, Deserialize, Clone, Debug)] @@ -68,13 +69,38 @@ fn header_tests_verify() { run_test_cases(cases); } +struct Trusted { + last_signed_header: SignedHeader, + validators: Set, +} + +impl lite::TrustedState for Trusted { + type LastHeader = SignedHeader; + type ValidatorSet = Set; + + fn new(last_header: &Self::LastHeader, vals: &Self::ValidatorSet) -> Self { + Self { + last_signed_header: last_header.clone(), + validators: vals.clone(), + } + } + + fn last_header(&self) -> &Self::LastHeader { + &self.last_signed_header + } + + fn validators(&self) -> &Self::ValidatorSet { + &self.validators + } +} + fn run_test_cases(cases: TestCases) { for (_, tc) in cases.test_cases.iter().enumerate() { - let mut trusted_signed_header = &tc.initial.signed_header; - let mut trusted_next_vals = tc.initial.clone().next_validator_set; + let trusted_next_vals = tc.initial.clone().next_validator_set; + let mut trusted_state = Trusted::new(&tc.initial.signed_header.clone(), &trusted_next_vals); let trusting_period: std::time::Duration = tc.initial.clone().trusting_period.into(); let now = tc.initial.now; - let expexts_err = match &tc.expected_output { + let expects_err = match &tc.expected_output { Some(eo) => eo.eq("error"), None => false, }; @@ -91,21 +117,19 @@ fn run_test_cases(cases: TestCases) { let mut check_support_res: Result<(), lite::Error> = Ok(()); if h2_verif_res.is_ok() { check_support_res = lite::check_support( - trusted_signed_header, - &trusted_next_vals, + &trusted_state, &new_signed_header, - DefaultTrustLevel {}, - trusting_period, - now.into(), + &DefaultTrustLevel {}, + &trusting_period, + &now.into(), ); - assert_eq!(check_support_res.is_err(), expexts_err); + assert_eq!(check_support_res.is_err(), expects_err); if check_support_res.is_ok() { - trusted_signed_header = new_signed_header; - trusted_next_vals = input.next_validator_set.clone(); + trusted_state = Trusted::new(&new_signed_header, &input.next_validator_set); } } let got_err = check_support_res.is_err() || h2_verif_res.is_err(); - assert_eq!(expexts_err, got_err); + assert_eq!(expects_err, got_err); } } } From f177f0fb86dc5395a11ca8a7231011ed0b1276b4 Mon Sep 17 00:00:00 2001 From: Ethan Buchman Date: Tue, 31 Dec 2019 13:46:47 -0500 Subject: [PATCH 2/5] New public API for the light client (#114) * new verify_and_update_single func * verify_and_update funcs * fmt * clippy * simplest test for verify_single * test_verify_single_1_val_skip * convenience funcs and more tests * cleanup and more tests * fmt * comments * clippy * consolidate validate_ funcs * fixes to bisection func from zarko review * check next validators are correct for sequential * fix tests * lite json tests use verify_single * remove old functions * limit the public API * Apply suggestions from code review Co-Authored-By: Alexander Simmerl * Improve context of comment Co-Authored-By: Ethan Buchman * Please rustfmt * Improve function name Co-authored-by: Alexander Simmerl --- tendermint/src/lite.rs | 7 +- tendermint/src/lite/types.rs | 21 +- tendermint/src/lite/verifier.rs | 658 +++++++++++++++++++++++--------- tendermint/tests/lite.rs | 44 ++- 4 files changed, 522 insertions(+), 208 deletions(-) diff --git a/tendermint/src/lite.rs b/tendermint/src/lite.rs index 156273495..c5de8e64f 100644 --- a/tendermint/src/lite.rs +++ b/tendermint/src/lite.rs @@ -4,4 +4,9 @@ pub mod types; pub mod verifier; pub use self::types::*; -pub use self::verifier::*; + +// TODO: don't expose this once the json tests +// switch to using one of the other functions +pub use self::verifier::verify_single; + +pub use self::verifier::{verify_and_update_bisection, verify_and_update_single}; diff --git a/tendermint/src/lite/types.rs b/tendermint/src/lite/types.rs index 9d0e74e26..adff840ec 100644 --- a/tendermint/src/lite/types.rs +++ b/tendermint/src/lite/types.rs @@ -82,6 +82,12 @@ pub trait Commit { /// Compute the voting power of the validators that correctly signed the commit, /// according to their voting power in the passed in validator set. /// Will return an error in case an invalid signature was included. + /// TODO/XXX: This cannot detect if a signature from an incorrect validator + /// is included. That's fine when we're just trying to see if we can skip, + /// but when actually verifying it means we might accept commits that have sigs from + /// outside the correct validator set, which is something we expect to be able to detect + /// (it's not a real issue, but it would indicate a faulty full node). + /// /// /// This method corresponds to the (pure) auxiliary function in the spec: /// `votingpower_in(signers(h.Commit),h.Header.V)`. @@ -136,18 +142,20 @@ pub trait Store { /// Retrieve the trusted state at height h if it exists. /// If it does not exist return an error. + /// If h=0, return the latest trusted state. + /// TODO: use an enum instead of special-casing 0, see + /// https://github.com/interchainio/tendermint-rs/issues/118 fn get(&self, h: Height) -> Result<&Self::TrustedState, Error>; - - /// Retrieve the trusted signed header with the largest height h' with h' <= h, if it exists. - /// If it does not exist return an error. - fn get_smaller_or_equal(&self, h: Height) -> Result; } -#[derive(Debug, PartialEq)] +// NOTE: Copy/Clone for convenience in testing ... +#[derive(Copy, Clone, Debug, PartialEq)] pub enum Error { Expired, DurationOutOfRange, + NonIncreasingHeight, + InvalidSignature, // TODO: deduplicate with ErrorKind::SignatureInvalid InvalidValidatorSet, @@ -155,6 +163,9 @@ pub enum Error { InvalidCommitValue, // commit is not for the header we expected InvalidCommitLength, + // TODO(EB): we need to differentiate when this is from + // skipping and when it's from verifying ! + // https://github.com/interchainio/tendermint-rs/issues/119 InsufficientVotingPower, // TODO(Liamsi): change to same name as spec if this changes (curently ErrTooMuchChange) RequestFailed, diff --git a/tendermint/src/lite/verifier.rs b/tendermint/src/lite/verifier.rs index 46d36015d..cac8d2a85 100644 --- a/tendermint/src/lite/verifier.rs +++ b/tendermint/src/lite/verifier.rs @@ -13,6 +13,7 @@ use crate::lite::{ Commit, Error, Header, Requester, SignedHeader, Store, TrustThreshold, TrustedState, ValidatorSet, }; +use std::cmp::Ordering; use std::time::{Duration, SystemTime}; /// Returns an error if the header has expired according to the given @@ -36,80 +37,25 @@ where } } -fn validate_next_vals(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(()) -} - -/// Captures the skipping condition, i.e., it defines when we can trust the header -/// h2 based on a known trusted state. -/// Note that the trusted header included in the trusted state and h2 have already -/// passed basic validation by calling `verify`. -/// `Error::InsufficientVotingPower`is returned when there is not enough intersection -/// between validator sets to have skipping condition true. -pub fn check_support( - trusted_state: &TS, - h2: &SH, - trust_threshold: &L, - trusting_period: &Duration, - now: &SystemTime, -) -> Result<(), Error> -where - TS: TrustedState, - SH: SignedHeader, - L: TrustThreshold, - C: Commit, -{ - let h1 = trusted_state.last_header(); - let h1_next_vals = trusted_state.validators(); - - // TODO(EB): can we move all these checks? it seems the - // is_within doesn't need to happen here and the sequential check - // for vals hash should be part of validate_vals_and_commit. then this function - // is basically just verify_commit_trusting. Would need to update spec as well. - // In sequential case, would still need to return early or not call check_support at all. - is_within_trust_period(h1.header(), trusting_period, now)?; - - // check the sequential case - if h2.header().height() == h1.header().height().increment() { - if h2.header().validators_hash() == h1_next_vals.hash() { - // It's sequential, so verify_commit_trusting would be - // redundant with verify since the validators are the same - // when we're not skipping. - return Ok(()); - } else { - return Err(Error::InvalidNextValidatorSet); - } - } - - // check if enough trusted validators signed to skip to the new height. - verify_commit_trusting(h1_next_vals, h2.commit(), trust_threshold) -} - -/// Validate the validators and commit against the header. +/// Validate the validators, next validators, and commit against the header. // TODO(EB): consider making this a method on Commit so the details are hidden, // and so we can remove the votes_len() method (that check would be part of the // methods implementation). These checks aren't reflected // explicitly in the spec yet, only in the sentence "Additional checks should // be done in the implementation to ensure header is well formed". -fn validate_vals_and_commit(header: &H, commit: &C, vals: &V) -> Result<(), Error> +fn validate_untrusted(header: &H, commit: &C, vals: &V, next_vals: &V) -> Result<(), Error> where H: Header, V: ValidatorSet, C: Commit, { - // ensure the header validators match these validators + // ensure the header validator hashes match the given validators if header.validators_hash() != vals.hash() { return Err(Error::InvalidValidatorSet); } + if header.next_validators_hash() != next_vals.hash() { + return Err(Error::InvalidNextValidatorSet); + } // ensure the header matches the commit if header.hash() != commit.header_hash() { @@ -126,25 +72,11 @@ where Ok(()) } -/// Verify the commit is valid from the given validators for the header. -pub fn verify(signed_header: &SH, validators: &C::ValidatorSet) -> Result<(), Error> -where - SH: SignedHeader, - C: Commit, -{ - let header = signed_header.header(); - let commit = signed_header.commit(); - - // basic validatity checks that header, commit, and vals match up - validate_vals_and_commit(header, commit, validators)?; - - // ensure that +2/3 validators signed correctly - verify_commit_full(validators, commit) -} - /// 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(vals: &C::ValidatorSet, commit: &C) -> Result<(), Error> +/// NOTE: These validators are expected to be the correct validators for the commit, +/// but since we're using voting_power_in, we can't actually detect if there's +/// votes from validators not in the set. +pub fn verify_commit_full(vals: &C::ValidatorSet, commit: &C) -> Result<(), Error> where C: Commit, { @@ -153,6 +85,9 @@ where // check the signers account for +2/3 of the voting power if signed_power * 3 <= total_power * 2 { + // TODO(EB): Use a different error from + // verify_commit_trusting else bisection + // will happen when the commit is actually just invalid! return Err(Error::InsufficientVotingPower); } @@ -184,98 +119,118 @@ where Ok(()) } -/// Returns Ok if we can trust the passed in (untrusted) header -/// based on the given trusted state, otherwise returns an Error. -fn can_trust_bisection( - trusted_state: &TS, // h1 in spec - untrusted_header: &SH, // h2 in spec +// Verify a single untrusted header against a trusted state. +// Includes all validation and signature verification. +// Not publicly exposed since it does not check for expiry +// and hence it's possible to use it incorrectly. +// If trusted_state is not expired and this returns Ok, the +// untrusted_sh and untrusted_next_vals can be considered trusted. +pub fn verify_single( + trusted_state: &TS, + untrusted_sh: &SH, + untrusted_vals: &C::ValidatorSet, + untrusted_next_vals: &C::ValidatorSet, trust_threshold: &L, - trusting_period: &Duration, - now: &SystemTime, - req: &R, - store: &mut S, ) -> Result<(), Error> where TS: TrustedState, SH: SignedHeader, C: Commit, L: TrustThreshold, - S: Store, - R: Requester, { - // can we trust the still untrusted header based on the given trusted state? - match check_support( - trusted_state, + // validate the untrusted header against its commit, vals, and next_vals + let untrusted_header = untrusted_sh.header(); + let untrusted_commit = untrusted_sh.commit(); + validate_untrusted( untrusted_header, - trust_threshold, - trusting_period, - now, - ) { - Ok(_) => { - let untrusted_next_vals = - req.validator_set(untrusted_header.header().height().increment())?; - let untrusted_state = TS::new(untrusted_header, &untrusted_next_vals); - store.add(&untrusted_state)?; - return Ok(()); - } - Err(e) => { - if e != Error::InsufficientVotingPower { - return Err(e); + untrusted_commit, + untrusted_vals, + untrusted_next_vals, + )?; + + // ensure the new height is higher. + // if its +1, ensure the vals are correct. + // if its >+1, ensure we can skip to it + let trusted_header = trusted_state.last_header().header(); + let trusted_height = trusted_header.height(); + let untrusted_height = untrusted_sh.header().height(); + match untrusted_height.cmp(&trusted_height.increment()) { + Ordering::Less => return Err(Error::NonIncreasingHeight), + Ordering::Equal => { + let trusted_vals_hash = trusted_header.next_validators_hash(); + let untrusted_vals_hash = untrusted_header.validators_hash(); + if trusted_vals_hash != untrusted_vals_hash { + // TODO: more specific error + // ie. differentiate from when next_vals.hash() doesnt + // match the header hash ... + return Err(Error::InvalidNextValidatorSet); } } + Ordering::Greater => { + let trusted_vals = trusted_state.validators(); + verify_commit_trusting(trusted_vals, untrusted_commit, trust_threshold)?; + } } - // Here we can't trust the passed in untrusted header based on the known trusted state. - // Run bisection: try again with a pivot header whose height is in the - // middle of the trusted height and the desired height (h2.height). - - let trusted_header = trusted_state.last_header(); - let trusted_height: u64 = trusted_header.header().height().value(); - let untrusted_height: u64 = untrusted_header.header().height().value(); - let pivot: u64 = (trusted_height + untrusted_height) / 2; - let pivot_header = req.signed_header(pivot)?; - let pivot_vals = req.validator_set(pivot)?; + // All validation passed successfully. Verify the validators correctly committed the block. + verify_commit_full(untrusted_vals, untrusted_sh.commit()) +} - verify(&pivot_header, &pivot_vals)?; +/// Attempt to update the store to the given untrusted header. +/// Ensures our last trusted header hasn't expired yet, and that +/// the untrusted header can be verified using only our latest trusted +/// state from the store. +/// This function is primarily for use by IBC handlers. +pub fn verify_and_update_single( + untrusted_sh: &SH, + untrusted_vals: &C::ValidatorSet, + untrusted_next_vals: &C::ValidatorSet, + trust_threshold: &L, + trusting_period: &Duration, + now: &SystemTime, + store: &mut S, +) -> Result<(), Error> +where + TS: TrustedState, + SH: SignedHeader, + C: Commit, + L: TrustThreshold, + S: Store, +{ + // Fetch the latest state and ensure it hasn't expired. + let trusted_state = store.get(Height::from(0))?; + let trusted_sh = trusted_state.last_header(); + is_within_trust_period(trusted_sh.header(), trusting_period, now)?; - // Can we trust pivot header based on trusted_state? - can_trust_bisection( + verify_single( trusted_state, - &pivot_header, - trust_threshold, - trusting_period, - now, - req, - store, - )?; - // Trust the header in between the trusted and (still) untrusted height: - let pivot_next_vals = req.validator_set(pivot + 1)?; - let pivot_trusted = TS::new(&pivot_header, &pivot_next_vals); - store.add(&pivot_trusted)?; - - // Can we trust the (still) untrusted header based on the (now trusted) "pivot header"? - can_trust_bisection( - &pivot_trusted, - untrusted_header, + untrusted_sh, + untrusted_vals, + untrusted_next_vals, trust_threshold, - trusting_period, - now, - req, - store, )?; - // Add header (and corresponding next validators) to trusted state store: - let untrusted_next_vals = req.validator_set(untrusted_header.header().height().increment())?; - let untrusted_state = TS::new(untrusted_header, &untrusted_next_vals); - store.add(&untrusted_state)?; - Ok(()) + // The untrusted header is now trusted; Update the store + let new_trusted_state = TS::new(untrusted_sh, untrusted_next_vals); + store.add(&new_trusted_state) } -/// This function captures the high level logic of the light client verification, i.e., -/// an application call to the light client module to (optionally download) and -/// verify a header for some height. -pub fn verify_header( - height: Height, +/// Attempt to update the store to the given untrusted height +/// by requesting the necessary data (signed headers and validators). +/// Returns an error if: +/// - we're already at or past that height +/// - our latest state expired +/// - any requests fail +/// - requested data is inconsistent (eg. vals don't match hashes in header) +/// - validators did not correctly commit their blocks +/// This function is recursive: it uses a bisection algorithm +/// to request data for intermediate heights as necessary. +/// Ensures our last trusted header hasn't expired yet, and that +/// data from the untrusted height can be verified, possibly using +/// data from intermediate heights. +/// This function is primarily for use by a light node. +pub fn verify_and_update_bisection( + untrusted_height: Height, trust_threshold: &L, trusting_period: &Duration, now: &SystemTime, @@ -284,42 +239,100 @@ pub fn verify_header( ) -> Result<(), Error> where TS: TrustedState, + SH: SignedHeader, + C: Commit, L: TrustThreshold, - S: Store, R: Requester, - C: Commit, + S: Store, +{ + // Fetch the latest state and ensure it hasn't expired. + // Note we only check for expiry once in this + // verify_and_update_bisection function since we assume the + // time is passed in and we don't access a clock internall. + // Thus the trust_period must be long enough to incorporate the + // expected time to complete this function. + let trusted_state = store.get(Height::from(0))?; + let trusted_sh = trusted_state.last_header(); + is_within_trust_period(trusted_sh.header(), trusting_period, now)?; + + // TODO: consider fetching the header we're trying to sync to and + // checking that it's time is less then `now + X` for some small X. + // If not, it means that either our local clock is really slow + // or the blockchains BFT time is really wrong. + // In either case, we should probably raise an error. + // Note this would be stronger than checking that the untrusted + // header is within the trusting period, as it could still diverge + // significantly from `now`. + + // inner recursive function which assumes + // trusting_period check is already done. + _verify_and_update_bisection(untrusted_height, trust_threshold, req, store) +} + +// inner recursive function for verify_and_update_bisection. +// see that function's docs. +fn _verify_and_update_bisection( + untrusted_height: Height, + trust_threshold: &L, + req: &R, + store: &mut S, +) -> Result<(), Error> +where + TS: TrustedState, SH: SignedHeader, + C: Commit, + L: TrustThreshold, + R: Requester, + S: Store, { - // Check if we already trusted a header at the given height and it didn't expire: - if let Ok(ts2) = store.get(height) { - is_within_trust_period(ts2.last_header().header(), trusting_period, now)? - } - - // We haven't trusted a header at given height yet. Request it: - let sh2 = req.signed_header(height)?; - let sh2_vals = req.validator_set(height)?; - verify(&sh2, &sh2_vals)?; - is_within_trust_period(sh2.header(), trusting_period, now)?; - - // Get the highest trusted header with height lower than sh2's. - let sh1_trusted = store.get_smaller_or_equal(height)?; - can_trust_bisection( - &sh1_trusted, - &sh2, + // this get is redundant the first time. + // TODO: possibly refactor so this func takes and returns + // trusted_state. + let trusted_state = store.get(Height::from(0))?; + let trusted_sh = trusted_state.last_header(); + + // fetch the header and vals for the new height + let untrusted_sh = &req.signed_header(untrusted_height)?; + let untrusted_vals = &req.validator_set(untrusted_height)?; + let untrusted_next_vals = &req.validator_set(untrusted_height.increment())?; + + // check if we can skip to this height and if it verifies. + match verify_single( + trusted_state, + untrusted_sh, + untrusted_vals, + untrusted_next_vals, trust_threshold, - trusting_period, - now, - req, - store, - )?; + ) { + Ok(_) => { + // Successfully verified! + // Trust the new state and return. + let new_trusted_state = TS::new(untrusted_sh, untrusted_next_vals); + return store.add(&new_trusted_state); + } + Err(e) => { + // If something went wrong, return the error. + if e != Error::InsufficientVotingPower { + return Err(e); + } - // TODO(Liamsi): The spec re-checks if we are still in trust period here - // and stores the now trusted header again. - // Figure out if we want to store it here or in can_trust_bisection! - // My understanding is: with the current impl, we either bubbled up an - // error, or, successfully added sh2 (and its nex vals) to the trusted store here. + // Insufficient voting power to update. + // Engage bisection, below. + } + } - Ok(()) + // Get the pivot height for bisection. + let trusted_h = trusted_sh.header().height().value(); + let untrusted_h = untrusted_height.value(); + let pivot_height = Height::from((trusted_h + untrusted_h) / 2); + + // Recursive call to update to the pivot height. + // When this completes, we will either return an error or + // have updated the store to the pivot height. + _verify_and_update_bisection(pivot_height, trust_threshold, req, store)?; + + // Recursive call to update to the original untrusted_height. + _verify_and_update_bisection(untrusted_height, trust_threshold, req, store) } mod tests { @@ -328,7 +341,7 @@ mod tests { use serde::Serialize; use sha2::{Digest, Sha256}; - #[derive(Debug, Serialize)] + #[derive(Clone, Debug, Serialize)] struct MockHeader { height: u64, time: SystemTime, @@ -363,12 +376,291 @@ mod tests { self.next_vals } fn hash(&self) -> Hash { - let encoded = serde_json::to_vec(self).unwrap(); - let hashed = Sha256::digest(&encoded); - Hash::new(Algorithm::Sha256, &hashed).unwrap() + json_hash(self) } } + fn json_hash(value: &T) -> Hash { + let encoded = serde_json::to_vec(value).unwrap(); + let hashed = Sha256::digest(&encoded); + Hash::new(Algorithm::Sha256, &hashed).unwrap() + } + + // vals are just ints, each has power 1 + #[derive(Clone, Debug, Serialize)] + struct MockValSet { + // NOTE: use HashSet instead? + vals: Vec, + } + + impl MockValSet { + fn new(vals: Vec) -> MockValSet { + MockValSet { vals } + } + } + + impl ValidatorSet for MockValSet { + fn hash(&self) -> Hash { + json_hash(&self) + } + fn total_power(&self) -> u64 { + self.vals.len() as u64 + } + fn len(&self) -> usize { + self.vals.len() + } + fn is_empty(&self) -> bool { + self.len() == 0 + } + } + + // commit is a list of vals that signed. + // use None if the val didn't sign. + #[derive(Clone, Debug, Serialize)] + struct MockCommit { + hash: Hash, + vals: Vec>, + } + + impl MockCommit { + fn new(hash: Hash, vals: Vec>) -> MockCommit { + MockCommit { hash, vals } + } + } + + impl Commit for MockCommit { + type ValidatorSet = MockValSet; + + fn header_hash(&self) -> Hash { + self.hash + } + + // just the intersection + fn voting_power_in(&self, vals: &Self::ValidatorSet) -> Result { + let mut power = 0; + + // we only care about the Somes. + // if there's a signer thats not in the val set, + // we can't detect it... + for signer_opt in self.vals.iter() { + if let Some(signer) = signer_opt { + for val in vals.vals.iter() { + if signer == val { + power += 1 + } + } + } + } + Ok(power) + } + + fn votes_len(&self) -> usize { + self.vals.len() + } + } + + #[derive(Clone)] + struct MockSignedHeader { + header: MockHeader, + commit: MockCommit, + } + + impl MockSignedHeader { + fn new(header: MockHeader, commit: MockCommit) -> Self { + MockSignedHeader { header, commit } + } + } + + impl SignedHeader for MockSignedHeader { + type Header = MockHeader; + type Commit = MockCommit; + fn header(&self) -> &Self::Header { + &self.header + } + fn commit(&self) -> &Self::Commit { + &self.commit + } + } + + // uses refs because the trait defines `new` to take refs ... + struct MockState { + header: MockSignedHeader, + vals: MockValSet, + } + + impl TrustedState for MockState { + type LastHeader = MockSignedHeader; + type ValidatorSet = MockValSet; + + // XXX: how to do this without cloning?! + fn new(header: &Self::LastHeader, vals: &Self::ValidatorSet) -> MockState { + MockState { + header: header.clone(), + vals: vals.clone(), + } + } + fn last_header(&self) -> &Self::LastHeader { + &self.header + } + fn validators(&self) -> &Self::ValidatorSet { + &self.vals + } + } + + // XXX: Can we do without this mock and global since we have a default impl? + struct MockThreshold {} + impl TrustThreshold for MockThreshold {} + static THRESHOLD: &MockThreshold = &MockThreshold {}; + + // start all blockchains from here ... + fn init_time() -> SystemTime { + SystemTime::UNIX_EPOCH + } + + // create an initial trusted state from the given vals + fn init_state(vals_vec: Vec) -> MockState { + let time = init_time(); + let height = 1; + let vals = &MockValSet::new(vals_vec.clone()); + let header = MockHeader::new(height, time, vals.hash(), vals.hash()); + let commit = MockCommit::new(header.hash(), vals_vec.into_iter().map(Some).collect()); + let sh = &MockSignedHeader::new(header, commit); + MockState::new(sh, vals) + } + + // create the next state with the given vals and commit. + fn next_state( + vals_vec: Vec, + commit_vec: Vec>, + ) -> (MockSignedHeader, MockValSet, MockValSet) { + let time = init_time() + Duration::new(10, 0); + let height = 10; + let vals = MockValSet::new(vals_vec); + let next_vals = vals.clone(); + let header = MockHeader::new(height, time, vals.hash(), next_vals.hash()); + let commit = MockCommit::new(header.hash(), commit_vec); + (MockSignedHeader::new(header, commit), vals, next_vals) + } + + // make a state with the given vals and commit and ensure we get the error. + fn assert_err(ts: &MockState, vals: Vec, commit: Vec>, err: Error) { + let (un_sh, un_vals, un_next_vals) = next_state(vals, commit); + let result = verify_single(ts, &un_sh, &un_vals, &un_next_vals, THRESHOLD); + assert_eq!(result, Err(err)); + } + + // make a state with the given vals and commit and ensure we get no error. + fn assert_ok(ts: &MockState, vals: Vec, commit: Vec>) { + let (un_sh, un_vals, un_next_vals) = next_state(vals, commit); + assert!(verify_single(ts, &un_sh, &un_vals, &un_next_vals, THRESHOLD).is_ok()); + } + + // convenience vars for validators that signed commit + static S0: Option = Some(0); + static S1: Option = Some(1); + static S2: Option = Some(2); + static S3: Option = Some(3); + static S4: Option = Some(4); + static S5: Option = Some(5); + + // valid to skip, but invalid commit. 1 validator. + #[test] + fn test_verify_single_skip_1_val_verify() { + let ts = &init_state(vec![0]); + + // 100% overlap, but wrong commit. + // NOTE: This should be an invalid commit error since there's + // a vote from a validator not in the set ! + // but voting_power_in isn't smart enough to see this ... + assert_err(ts, vec![1], vec![S0], Error::InsufficientVotingPower); + } + + // valid commit and data, starting with 1 validator. + // test if we can skip to it. + #[test] + fn test_verify_single_skip_1_val_skip() { + let ts = &init_state(vec![0]); + let err = Error::InsufficientVotingPower; + + //***** + // Ok + + // 100% overlap (original signer is present in commit) + assert_ok(ts, vec![0], vec![S0]); + assert_ok(ts, vec![0, 1], vec![S0, S1]); + assert_ok(ts, vec![0, 1, 2], vec![S0, S1, S2]); + assert_ok(ts, vec![0, 1, 2, 3], vec![S0, S1, S2, S3]); + + //***** + // Err + + // 0% overlap - new val set without the original signer + assert_err(ts, vec![1], vec![S1], err); + + // 0% overlap - val set contains original signer, but they didn't sign + assert_err(ts, vec![0, 1, 2, 3], vec![None, S1, S2, S3], err); + } + + // valid commit and data, starting with 2 validators. + // test if we can skip to it. + #[test] + fn test_verify_single_skip_2_val_skip() { + let ts = &init_state(vec![0, 1]); + let err = Error::InsufficientVotingPower; + + //************* + // OK + + // 100% overlap (both original signers still present) + assert_ok(ts, vec![0, 1], vec![S0, S1]); + assert_ok(ts, vec![0, 1, 2], vec![S0, S1, S2]); + + // 50% overlap (one original signer still present) + assert_ok(ts, vec![0], vec![S0]); + assert_ok(ts, vec![0, 1, 2, 3], vec![None, S1, S2, S3]); + + //************* + // Err + + // 0% overlap (neither original signer still present) + assert_err(ts, vec![2], vec![S2], err); + + // 0% overlap (original signer is still in val set but not in commit) + assert_err(ts, vec![0, 2, 3, 4], vec![None, S2, S3, S4], err); + } + + // valid commit and data, starting with 3 validators. + // test if we can skip to it. + #[test] + fn test_verify_single_skip_3_val_skip() { + let ts = &init_state(vec![0, 1, 2]); + let err = Error::InsufficientVotingPower; + + //************* + // OK + + // 100% overlap (both original signers still present) + assert_ok(ts, vec![0, 1, 2], vec![S0, S1, S2]); + assert_ok(ts, vec![0, 1, 2, 3], vec![S0, S1, S2, S3]); + + // 66% overlap (two original signers still present) + assert_ok(ts, vec![0, 1], vec![S0, S1]); + assert_ok(ts, vec![0, 1, 2, 3], vec![None, S1, S2, S3]); + + //************* + // Err + + // 33% overlap (one original signer still present) + assert_err(ts, vec![0], vec![S0], err); + assert_err(ts, vec![0, 3], vec![S0, S3], err); + + // 0% overlap (neither original signer still present) + assert_err(ts, vec![3], vec![S2], err); + + // 0% overlap (original signer is still in val set but not in commit) + assert_err(ts, vec![0, 3, 4, 5], vec![None, S3, S4, S5], err); + } + fn fixed_hash() -> Hash { Hash::new(Algorithm::Sha256, &Sha256::digest(&[5])).unwrap() } diff --git a/tendermint/tests/lite.rs b/tendermint/tests/lite.rs index a799990c9..8bebc1be6 100644 --- a/tendermint/tests/lite.rs +++ b/tendermint/tests/lite.rs @@ -98,38 +98,44 @@ fn run_test_cases(cases: TestCases) { for (_, tc) in cases.test_cases.iter().enumerate() { let trusted_next_vals = tc.initial.clone().next_validator_set; let mut trusted_state = Trusted::new(&tc.initial.signed_header.clone(), &trusted_next_vals); - let trusting_period: std::time::Duration = tc.initial.clone().trusting_period.into(); - let now = tc.initial.now; let expects_err = match &tc.expected_output { Some(eo) => eo.eq("error"), None => false, }; + // TODO - we're currently using lite::verify_single which + // shouldn't even be exposed and doesn't check time. + // but the public functions take a store, which do check time, + // also take a store, so we need to mock one ... + /* + let trusting_period: std::time::Duration = tc.initial.clone().trusting_period.into(); + let now = tc.initial.now; + */ + for (_, input) in tc.input.iter().enumerate() { println!("{}", tc.description); - let new_signed_header = &input.signed_header; - let new_vals = &input.validator_set; + let untrusted_signed_header = &input.signed_header; + let untrusted_vals = &input.validator_set; + let untrusted_next_vals = &input.next_validator_set; // Note that in the provided test files the other header is either assumed to // be "trusted" (verification already happened), or, it's the signed header verified in // the previous iteration of this loop. In both cases it is assumed that h1 was already // verified. - let h2_verif_res = lite::verify(new_signed_header, new_vals); - let mut check_support_res: Result<(), lite::Error> = Ok(()); - if h2_verif_res.is_ok() { - check_support_res = lite::check_support( - &trusted_state, - &new_signed_header, - &DefaultTrustLevel {}, - &trusting_period, - &now.into(), - ); - assert_eq!(check_support_res.is_err(), expects_err); - if check_support_res.is_ok() { - trusted_state = Trusted::new(&new_signed_header, &input.next_validator_set); + match lite::verify_single( + &trusted_state, + &untrusted_signed_header, + &untrusted_vals, + &untrusted_next_vals, + &DefaultTrustLevel {}, + ) { + Ok(_) => { + trusted_state = Trusted::new(&untrusted_signed_header, &untrusted_next_vals); + assert!(!expects_err); + } + Err(_) => { + assert!(expects_err); } } - let got_err = check_support_res.is_err() || h2_verif_res.is_err(); - assert_eq!(expects_err, got_err); } } } From 1cc2071cead14533e71a69e8db4051eb0ea09c60 Mon Sep 17 00:00:00 2001 From: Alexander Simmerl Date: Fri, 3 Jan 2020 13:58:20 +0100 Subject: [PATCH 3/5] Setup rustfmt config (#121) For consistent code formatting this change adds a rustfmt configuration to be used by developer tooling and CI. * Add fmt config * Please rustfmt --- .rustfmt.toml | 12 ++++++++++++ tendermint/src/amino_types/ed25519.rs | 3 ++- tendermint/src/amino_types/proposal.rs | 7 ++++--- tendermint/src/amino_types/vote.rs | 11 +++++++---- tendermint/src/block/commit.rs | 3 ++- tendermint/src/config.rs | 9 ++++++--- tendermint/src/lite/types.rs | 3 ++- tendermint/src/lite/verifier.rs | 2 +- 8 files changed, 36 insertions(+), 14 deletions(-) create mode 100644 .rustfmt.toml diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 000000000..ff119513c --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1,12 @@ +unstable_features = true + +# comments +comment_width = 100 +wrap_comments = true + +# imports +reorder_imports = true + +# strings +format_strings = false +max_width = 100 diff --git a/tendermint/src/amino_types/ed25519.rs b/tendermint/src/amino_types/ed25519.rs index 3dc1f8bcb..41bcd6842 100644 --- a/tendermint/src/amino_types/ed25519.rs +++ b/tendermint/src/amino_types/ed25519.rs @@ -102,7 +102,8 @@ mod tests { // var pubKey [32]byte // copy(pubKey[:],[]byte{0x79, 0xce, 0xd, 0xe0, 0x43, 0x33, 0x4a, 0xec, 0xe0, 0x8b, 0x7b, // 0xb5, 0x61, 0xbc, 0xe7, 0xc1, - // 0xd4, 0x69, 0xc3, 0x44, 0x26, 0xec, 0xef, 0xc0, 0x72, 0xa, 0x52, 0x4d, 0x37, 0x32, 0xef, 0xed}) + // 0xd4, 0x69, 0xc3, 0x44, 0x26, 0xec, 0xef, 0xc0, 0x72, 0xa, 0x52, 0x4d, 0x37, 0x32, 0xef, + // 0xed}) // // b, _ = cdc.MarshalBinary(&privval.PubKeyResponse{PubKey: crypto.PubKeyEd25519(pubKey)}) // fmt.Printf("%#v\n\n", b) diff --git a/tendermint/src/amino_types/proposal.rs b/tendermint/src/amino_types/proposal.rs index c871cb7de..a018a2d15 100644 --- a/tendermint/src/amino_types/proposal.rs +++ b/tendermint/src/amino_types/proposal.rs @@ -52,7 +52,8 @@ pub struct SignProposalRequest { #[derive(Clone, PartialEq, Message)] struct CanonicalProposal { #[prost(uint32, tag = "1")] - msg_type: u32, // this is a byte in golang, which is a varint encoded UInt8 (using amino's EncodeUvarint) + msg_type: u32, /* this is a byte in golang, which is a varint encoded UInt8 (using amino's + * EncodeUvarint) */ #[prost(sfixed64)] height: i64, #[prost(sfixed64)] @@ -228,8 +229,8 @@ mod tests { // cdc := amino.NewCodec() // privval.RegisterRemoteSignerMsg(cdc) // stamp, _ := time.Parse(time.RFC3339Nano, "2018-02-11T07:09:22.765Z") - // data, _ := cdc.MarshalBinaryLengthPrefixed(privval.SignProposalRequest{Proposal: &types.Proposal{ - // Type: types.ProposalType, // 0x20 + // data, _ := cdc.MarshalBinaryLengthPrefixed(privval.SignProposalRequest{Proposal: + // &types.Proposal{ Type: types.ProposalType, // 0x20 // Height: 12345, // Round: 23456, // POLRound: -1, diff --git a/tendermint/src/amino_types/vote.rs b/tendermint/src/amino_types/vote.rs index af29fa436..57b787970 100644 --- a/tendermint/src/amino_types/vote.rs +++ b/tendermint/src/amino_types/vote.rs @@ -269,7 +269,10 @@ mod tests { ], validator_index: 56789, signature: vec![], - //signature: vec![130u8, 246, 183, 50, 153, 248, 28, 57, 51, 142, 55, 217, 194, 24, 134, 212, 233, 100, 211, 10, 24, 174, 179, 117, 41, 65, 141, 134, 149, 239, 65, 174, 217, 42, 6, 184, 112, 17, 7, 97, 255, 221, 252, 16, 60, 144, 30, 212, 167, 39, 67, 35, 118, 192, 133, 130, 193, 115, 32, 206, 152, 91, 173, 10], + /* signature: vec![130u8, 246, 183, 50, 153, 248, 28, 57, 51, 142, 55, 217, 194, 24, + * 134, 212, 233, 100, 211, 10, 24, 174, 179, 117, 41, 65, 141, 134, 149, 239, 65, + * 174, 217, 42, 6, 184, 112, 17, 7, 97, 255, 221, 252, 16, 60, 144, 30, 212, 167, + * 39, 67, 35, 118, 192, 133, 130, 193, 115, 32, 206, 152, 91, 173, 10], */ }; let sign_vote_msg = SignVoteRequest { vote: Some(vote) }; let mut got = vec![]; @@ -292,9 +295,9 @@ mod tests { // Hash: []byte("parts_hash"), // }, // }, - // ValidatorAddress: []byte{0xa3, 0xb2, 0xcc, 0xdd, 0x71, 0x86, 0xf1, 0x68, 0x5f, 0x21, 0xf2, 0x48, 0x2a, 0xf4, 0xfb, 0x34, 0x46, 0xa8, 0x4b, 0x35}, - // ValidatorIndex: 56789, - // }}) + // ValidatorAddress: []byte{0xa3, 0xb2, 0xcc, 0xdd, 0x71, 0x86, 0xf1, 0x68, 0x5f, 0x21, + // 0xf2, 0x48, 0x2a, 0xf4, 0xfb, 0x34, 0x46, 0xa8, 0x4b, 0x35}, ValidatorIndex: + // 56789, }}) // fmt.Println(strings.Join(strings.Split(fmt.Sprintf("%v",data), " "), ", ")) let want = vec![ diff --git a/tendermint/src/block/commit.rs b/tendermint/src/block/commit.rs index b61f3608e..4fe60338e 100644 --- a/tendermint/src/block/commit.rs +++ b/tendermint/src/block/commit.rs @@ -4,7 +4,8 @@ use crate::{block, Vote}; use serde::{Deserialize, Serialize}; use std::{ops::Deref, slice}; -/// Commit contains the justification (ie. a set of signatures) that a block was committed by a set of validators. +/// Commit contains the justification (ie. a set of signatures) that a block was committed by a set +/// of validators. /// /// /// diff --git a/tendermint/src/config.rs b/tendermint/src/config.rs index d483cc522..bedf11004 100644 --- a/tendermint/src/config.rs +++ b/tendermint/src/config.rs @@ -55,7 +55,8 @@ pub struct TendermintConfig { /// Path to the JSON file containing the initial validator set and other meta data pub genesis_file: PathBuf, - /// Path to the JSON file containing the private key to use as a validator in the consensus protocol + /// Path to the JSON file containing the private key to use as a validator in the consensus + /// protocol pub priv_validator_key_file: Option, /// Path to the JSON file containing the last sign state of a validator @@ -66,7 +67,8 @@ pub struct TendermintConfig { #[serde(deserialize_with = "deserialize_optional_value")] pub priv_validator_laddr: Option, - /// Path to the JSON file containing the private key to use for node authentication in the p2p protocol + /// Path to the JSON file containing the private key to use for node authentication in the p2p + /// protocol pub node_key_file: PathBuf, /// Mechanism to connect to the ABCI application: socket | grpc @@ -536,7 +538,8 @@ pub enum TxIndexer { #[serde(rename = "null")] Null, - /// "kv" (default) - the simplest possible indexer, backed by key-value storage (defaults to levelDB; see DBBackend). + /// "kv" (default) - the simplest possible indexer, backed by key-value storage (defaults to + /// levelDB; see DBBackend). #[serde(rename = "kv")] Kv, } diff --git a/tendermint/src/lite/types.rs b/tendermint/src/lite/types.rs index adff840ec..bbc45c7c5 100644 --- a/tendermint/src/lite/types.rs +++ b/tendermint/src/lite/types.rs @@ -166,7 +166,8 @@ pub enum Error { // TODO(EB): we need to differentiate when this is from // skipping and when it's from verifying ! // https://github.com/interchainio/tendermint-rs/issues/119 - InsufficientVotingPower, // TODO(Liamsi): change to same name as spec if this changes (curently ErrTooMuchChange) + InsufficientVotingPower, /* TODO(Liamsi): change to same name as spec if this changes + * (curently ErrTooMuchChange) */ RequestFailed, } diff --git a/tendermint/src/lite/verifier.rs b/tendermint/src/lite/verifier.rs index cac8d2a85..66441335d 100644 --- a/tendermint/src/lite/verifier.rs +++ b/tendermint/src/lite/verifier.rs @@ -6,7 +6,7 @@ //! ``` //! // TODO: add a proper example maybe showing how a `can_trust_bisection` //! // looks using the types and methods in this crate/module. -//!``` +//! ``` use crate::block::Height; use crate::lite::{ From a6dcab46f7f6e78f988cd2df610dcd236f204c80 Mon Sep 17 00:00:00 2001 From: Ethan Buchman Date: Thu, 9 Jan 2020 12:36:25 -0500 Subject: [PATCH 4/5] lite: impl Requester, Store, and a basic syncing daemon (#116) * new verify_and_update_single func * verify_and_update funcs * fmt * clippy * simplest test for verify_single * test_verify_single_1_val_skip * convenience funcs and more tests * cleanup and more tests * fmt * comments * clippy * impl Requester * rename * cargo * impl state * impl store * TrustedState impls Clone * first take at hooking up a node * fmt * Please check and clippy * Switch to qualified reference for algo * Shorten height field assignment * consolidate validate_ funcs * fixes to bisection func from zarko review * check next validators are correct for sequential * fix tests * lite json tests use verify_single * remove old functions * limit the public API * compile fix * fix header.app_hash encoding it's not actually a hash! it's whatever is returned by the application, which can be arbitrary bytes. * only try bisection on higher heights * fix parse_hex * comments about time * remove printlns * use latest_commit * Requester -> RPCRequester * fix * comment out integration test * fmt * Apply suggestions from code review Co-Authored-By: Ismail Khoffi Co-authored-by: Alexander Simmerl Co-authored-by: Ismail Khoffi --- Cargo.toml | 1 + tendermint-lite/Cargo.toml | 11 +++ tendermint-lite/src/lib.rs | 4 + tendermint-lite/src/main.rs | 135 +++++++++++++++++++++++++++++++ tendermint-lite/src/requester.rs | 83 +++++++++++++++++++ tendermint-lite/src/state.rs | 30 +++++++ tendermint-lite/src/store.rs | 42 ++++++++++ tendermint-lite/src/threshold.rs | 11 +++ tendermint/src/block/header.rs | 6 +- tendermint/src/lite/types.rs | 4 +- tendermint/src/lite/verifier.rs | 9 ++- tendermint/src/serializers.rs | 5 +- tendermint/tests/lite.rs | 1 + 13 files changed, 335 insertions(+), 7 deletions(-) create mode 100644 tendermint-lite/Cargo.toml create mode 100644 tendermint-lite/src/lib.rs create mode 100644 tendermint-lite/src/main.rs create mode 100644 tendermint-lite/src/requester.rs create mode 100644 tendermint-lite/src/state.rs create mode 100644 tendermint-lite/src/store.rs create mode 100644 tendermint-lite/src/threshold.rs diff --git a/Cargo.toml b/Cargo.toml index 40857dd30..08269b4ce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,4 +2,5 @@ members = [ "tendermint", + "tendermint-lite", ] diff --git a/tendermint-lite/Cargo.toml b/tendermint-lite/Cargo.toml new file mode 100644 index 000000000..85e388609 --- /dev/null +++ b/tendermint-lite/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "tendermint-lite" +version = "0.1.0" +authors = ["Ethan Buchman "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +tendermint = { path = "../tendermint" } +tokio = "0.2" diff --git a/tendermint-lite/src/lib.rs b/tendermint-lite/src/lib.rs new file mode 100644 index 000000000..c06b501c2 --- /dev/null +++ b/tendermint-lite/src/lib.rs @@ -0,0 +1,4 @@ +pub mod requester; +pub mod state; +pub mod store; +pub mod threshold; diff --git a/tendermint-lite/src/main.rs b/tendermint-lite/src/main.rs new file mode 100644 index 000000000..14573f48b --- /dev/null +++ b/tendermint-lite/src/main.rs @@ -0,0 +1,135 @@ +use tendermint::hash; +use tendermint::lite; +use tendermint::lite::Error; +use tendermint::lite::{ + Header as _, Requester as _, SignedHeader as _, Store as _, TrustedState as _, + ValidatorSet as _, +}; +use tendermint::rpc; +use tendermint::{block::Height, Hash}; + +use tendermint_lite::{ + requester::RPCRequester, state::State, store::MemStore, threshold::TrustThresholdOneThird, +}; + +use core::future::Future; +use std::time::{Duration, SystemTime}; +use tokio::runtime::Builder; + +// TODO: these should be config/args +static SUBJECTIVE_HEIGHT: u64 = 1; +static SUBJECTIVE_VALS_HASH_HEX: &str = + "A5A7DEA707ADE6156F8A981777CA093F178FC790475F6EC659B6617E704871DD"; +static RPC_ADDR: &str = "localhost:26657"; + +// TODO: this should somehow be configurable ... +static THRESHOLD: &TrustThresholdOneThird = &TrustThresholdOneThird {}; + +pub fn block_on(future: F) -> F::Output { + Builder::new() + .basic_scheduler() + .enable_all() + .build() + .unwrap() + .block_on(future) +} + +fn main() { + // TODO: this should be config + let trusting_period = Duration::new(6000, 0); + + // setup requester for primary peer + let client = block_on(rpc::Client::new(&RPC_ADDR.parse().unwrap())).unwrap(); + let req = RPCRequester::new(client); + let mut store = MemStore::new(); + + let vals_hash = + Hash::from_hex_upper(hash::Algorithm::Sha256, SUBJECTIVE_VALS_HASH_HEX).unwrap(); + + subjective_init(Height::from(SUBJECTIVE_HEIGHT), vals_hash, &mut store, &req).unwrap(); + + loop { + let latest = (&req).signed_header(0).unwrap(); + let latest_peer_height = latest.header().height(); + + let latest = store.get(Height::from(0)).unwrap(); + let latest_height = latest.last_header().header().height(); + + // only bisect to higher heights + if latest_peer_height <= latest_height { + std::thread::sleep(Duration::new(1, 0)); + continue; + } + + println!( + "attempting bisection from height {:?} to height {:?}", + store + .get(Height::from(0)) + .unwrap() + .last_header() + .header() + .height(), + latest_peer_height, + ); + + let now = &SystemTime::now(); + lite::verify_and_update_bisection( + latest_peer_height, + THRESHOLD, + &trusting_period, + now, + &req, + &mut store, + ) + .unwrap(); + + println!("Succeeded bisecting!"); + + // notifications ? + + // sleep for a few secs ? + } +} + +/* + * The following is initialization logic that should have a + * function in the lite crate like: + * `subjective_init(height, vals_hash, store, requester) -> Result<(), Error` + * it would fetch the initial header/vals from the requester and populate a + * trusted state and store it in the store ... + * TODO: this should take traits ... but how to deal with the State ? + * TODO: better name ? +*/ +fn subjective_init( + height: Height, + vals_hash: Hash, + store: &mut MemStore, + req: &RPCRequester, +) -> Result<(), Error> { + if store.get(height).is_ok() { + // we already have this ! + return Ok(()); + } + + // check that the val hash matches + let vals = req.validator_set(height)?; + + if vals.hash() != vals_hash { + // TODO + panic!("vals hash dont match") + } + + let signed_header = req.signed_header(SUBJECTIVE_HEIGHT)?; + + // TODO: validate signed_header.commit() with the vals ... + + let next_vals = req.validator_set(height.increment())?; + + // TODO: check next_vals ... + + let trusted_state = &State::new(&signed_header, &next_vals); + + store.add(trusted_state)?; + + Ok(()) +} diff --git a/tendermint-lite/src/requester.rs b/tendermint-lite/src/requester.rs new file mode 100644 index 000000000..b7ffdb042 --- /dev/null +++ b/tendermint-lite/src/requester.rs @@ -0,0 +1,83 @@ +use tendermint::block; +use tendermint::lite; +use tendermint::rpc; +use tendermint::validator; + +use core::future::Future; +use tokio::runtime::Builder; + +/// RPCRequester wraps the Tendermint rpc::Client. +pub struct RPCRequester { + client: rpc::Client, +} + +impl RPCRequester { + pub fn new(client: rpc::Client) -> Self { + RPCRequester { client } + } +} + +impl lite::types::Requester for RPCRequester { + type SignedHeader = block::signed_header::SignedHeader; + type ValidatorSet = validator::Set; + + /// Request the signed header at height h. + /// If h==0, request the latest signed header. + /// TODO: use an enum instead of h==0. + fn signed_header(&self, h: H) -> Result + where + H: Into, + { + let height: block::Height = h.into(); + let r = match height.value() { + 0 => block_on(self.client.latest_commit()), + _ => block_on(self.client.commit(height)), + }; + match r { + Ok(response) => Ok(response.signed_header), + Err(_error) => Err(lite::Error::RequestFailed), + } + } + + /// Request the validator set at height h. + fn validator_set(&self, h: H) -> Result + where + H: Into, + { + let r = block_on(self.client.validators(h)); + match r { + Ok(response) => Ok(validator::Set::new(response.validators)), + Err(_error) => Err(lite::Error::RequestFailed), + } + } +} + +pub fn block_on(future: F) -> F::Output { + Builder::new() + .basic_scheduler() + .enable_all() + .build() + .unwrap() + .block_on(future) +} + +#[cfg(test)] +mod tests { + use super::*; + use tendermint::lite::types::Header as LiteHeader; + use tendermint::lite::types::Requester as LiteRequester; + use tendermint::lite::types::SignedHeader as LiteSignedHeader; + use tendermint::lite::types::ValidatorSet as LiteValSet; + use tendermint::rpc; + + // TODO: integration test + #[test] + #[ignore] + fn test_val_set() { + let client = block_on(rpc::Client::new(&"localhost:26657".parse().unwrap())).unwrap(); + let req = RPCRequester::new(client); + let r1 = req.validator_set(5).unwrap(); + let r2 = req.signed_header(5).unwrap(); + assert_eq!(r1.hash(), r2.header().validators_hash()); + } +} diff --git a/tendermint-lite/src/state.rs b/tendermint-lite/src/state.rs new file mode 100644 index 000000000..e1601080e --- /dev/null +++ b/tendermint-lite/src/state.rs @@ -0,0 +1,30 @@ +use tendermint::lite::TrustedState; +use tendermint::{block::signed_header::SignedHeader, validator::Set}; + +#[derive(Clone)] +pub struct State { + last_header: SignedHeader, + vals: Set, +} + +impl TrustedState for State { + type LastHeader = SignedHeader; + type ValidatorSet = Set; + + fn new(last_header: &Self::LastHeader, vals: &Self::ValidatorSet) -> Self { + State { + last_header: last_header.clone(), + vals: vals.clone(), + } + } + + // height H-1 + fn last_header(&self) -> &Self::LastHeader { + &self.last_header + } + + // height H + fn validators(&self) -> &Self::ValidatorSet { + &self.vals + } +} diff --git a/tendermint-lite/src/store.rs b/tendermint-lite/src/store.rs new file mode 100644 index 000000000..cbebe6697 --- /dev/null +++ b/tendermint-lite/src/store.rs @@ -0,0 +1,42 @@ +use crate::state::State; +use tendermint::block::Height; +use tendermint::lite::{Error, Header, SignedHeader, Store, TrustedState}; + +use std::collections::HashMap; + +#[derive(Default)] +pub struct MemStore { + height: Height, + store: HashMap, +} + +impl MemStore { + pub fn new() -> MemStore { + MemStore { + height: Height::from(0), + store: HashMap::new(), + } + } +} + +impl Store for MemStore { + type TrustedState = State; + + fn add(&mut self, trusted: &Self::TrustedState) -> Result<(), Error> { + let height = trusted.last_header().header().height(); + self.height = height; + self.store.insert(height, trusted.clone()); + Ok(()) + } + + fn get(&self, h: Height) -> Result<&Self::TrustedState, Error> { + let mut height = h; + if h.value() == 0 { + height = self.height + } + match self.store.get(&height) { + Some(state) => Ok(state), + None => Err(Error::RequestFailed), + } + } +} diff --git a/tendermint-lite/src/threshold.rs b/tendermint-lite/src/threshold.rs new file mode 100644 index 000000000..b1978fa73 --- /dev/null +++ b/tendermint-lite/src/threshold.rs @@ -0,0 +1,11 @@ +use tendermint::lite::TrustThreshold; + +pub struct TrustThresholdOneThird {} +impl TrustThreshold for TrustThresholdOneThird {} + +pub struct TrustThresholdTwoThirds {} +impl TrustThreshold for TrustThresholdTwoThirds { + fn is_enough_power(&self, signed_voting_power: u64, total_voting_power: u64) -> bool { + signed_voting_power * 3 > total_voting_power * 2 + } +} diff --git a/tendermint/src/block/header.rs b/tendermint/src/block/header.rs index 427177569..b80ce2306 100644 --- a/tendermint/src/block/header.rs +++ b/tendermint/src/block/header.rs @@ -61,8 +61,8 @@ pub struct Header { pub consensus_hash: Hash, /// State after txs from the previous block - #[serde(deserialize_with = "serializers::parse_non_empty_hash")] - pub app_hash: Option, + #[serde(deserialize_with = "serializers::parse_hex")] + pub app_hash: Vec, /// Root hash of all results from the txs from the previous block #[serde(deserialize_with = "serializers::parse_non_empty_hash")] @@ -120,7 +120,7 @@ impl lite::Header for Header { fields_bytes.push(encode_hash(&self.validators_hash)); fields_bytes.push(encode_hash(&self.next_validators_hash)); fields_bytes.push(encode_hash(&self.consensus_hash)); - fields_bytes.push(self.app_hash.as_ref().map_or(vec![], encode_hash)); + fields_bytes.push(bytes_enc(&self.app_hash)); fields_bytes.push(self.last_results_hash.as_ref().map_or(vec![], encode_hash)); fields_bytes.push(self.evidence_hash.as_ref().map_or(vec![], encode_hash)); fields_bytes.push(bytes_enc(self.proposer_address.as_bytes())); diff --git a/tendermint/src/lite/types.rs b/tendermint/src/lite/types.rs index bbc45c7c5..3e979532e 100644 --- a/tendermint/src/lite/types.rs +++ b/tendermint/src/lite/types.rs @@ -10,7 +10,7 @@ use std::time::SystemTime; /// TrustedState stores the latest state trusted by a lite client, /// including the last header (at height h-1) and the validator set /// (at height h) to use to verify the next header. -pub trait TrustedState { +pub trait TrustedState: Clone { type LastHeader: SignedHeader; type ValidatorSet: ValidatorSet; @@ -138,6 +138,7 @@ pub trait Store { type TrustedState: TrustedState; /// Add this state (header at height h, validators at height h+1) as trusted to the store. + // TODO: should this really take a reference? fn add(&mut self, trusted: &Self::TrustedState) -> Result<(), Error>; /// Retrieve the trusted state at height h if it exists. @@ -168,6 +169,5 @@ pub enum Error { // https://github.com/interchainio/tendermint-rs/issues/119 InsufficientVotingPower, /* TODO(Liamsi): change to same name as spec if this changes * (curently ErrTooMuchChange) */ - RequestFailed, } diff --git a/tendermint/src/lite/verifier.rs b/tendermint/src/lite/verifier.rs index 66441335d..cdee7cfdb 100644 --- a/tendermint/src/lite/verifier.rs +++ b/tendermint/src/lite/verifier.rs @@ -141,6 +141,7 @@ where // validate the untrusted header against its commit, vals, and next_vals let untrusted_header = untrusted_sh.header(); let untrusted_commit = untrusted_sh.commit(); + validate_untrusted( untrusted_header, untrusted_commit, @@ -154,6 +155,9 @@ where let trusted_header = trusted_state.last_header().header(); let trusted_height = trusted_header.height(); let untrusted_height = untrusted_sh.header().height(); + + // TODO: ensure the untrusted_header.bft_time() > trusted_header.bft_time() + match untrusted_height.cmp(&trusted_height.increment()) { Ordering::Less => return Err(Error::NonIncreasingHeight), Ordering::Equal => { @@ -263,6 +267,9 @@ where // Note this would be stronger than checking that the untrusted // header is within the trusting period, as it could still diverge // significantly from `now`. + // NOTE: we actually have to do this for every header we fetch, + // We do check bft_time is monotonic, but that check might happen too late. + // So every header we fetch must be checked to be less than now+X // inner recursive function which assumes // trusting_period check is already done. @@ -482,7 +489,7 @@ mod tests { } } - // uses refs because the trait defines `new` to take refs ... + #[derive(Clone)] struct MockState { header: MockSignedHeader, vals: MockValSet, diff --git a/tendermint/src/serializers.rs b/tendermint/src/serializers.rs index 307847a8a..26056ace4 100644 --- a/tendermint/src/serializers.rs +++ b/tendermint/src/serializers.rs @@ -89,13 +89,16 @@ where serializer.serialize_str(&hex_string) } +// decode upper or lowercase hex pub(crate) fn parse_hex<'de, D>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, { use serde::de::Error; let string = String::deserialize(deserializer)?; - hex::decode(&string).map_err(Error::custom) + hex::decode_upper(&string) + .or_else(|_| hex::decode(&string)) + .map_err(Error::custom) } pub(crate) fn serialize_base64(bytes: T, serializer: S) -> Result diff --git a/tendermint/tests/lite.rs b/tendermint/tests/lite.rs index 8bebc1be6..313ab0d2a 100644 --- a/tendermint/tests/lite.rs +++ b/tendermint/tests/lite.rs @@ -69,6 +69,7 @@ fn header_tests_verify() { run_test_cases(cases); } +#[derive(Clone)] struct Trusted { last_signed_header: SignedHeader, validators: Set, From ede8a9d0e3c1c3f190a0b17421e16c255daff185 Mon Sep 17 00:00:00 2001 From: Ismail Khoffi Date: Fri, 10 Jan 2020 20:14:06 +0100 Subject: [PATCH 5/5] Refactor lite impls into their own module within tendermint (#124) * move header impl into separate module (where all impls will live) - names are going to change and usage of mod.rs instead of cur structure * move signed header, commit, and validator set impls into separate module * move helper method to where it is only used (in lite impl) * Some doc improvements --- tendermint/src/block/header.rs | 75 +------------------- tendermint/src/block/signed_header.rs | 82 +--------------------- tendermint/src/lib.rs | 1 + tendermint/src/lite/types.rs | 4 +- tendermint/src/lite/verifier.rs | 13 ++-- tendermint/src/lite_impl.rs | 5 ++ tendermint/src/lite_impl/header.rs | 76 ++++++++++++++++++++ tendermint/src/lite_impl/signed_header.rs | 84 +++++++++++++++++++++++ tendermint/src/lite_impl/validator_set.rs | 30 ++++++++ tendermint/src/validator.rs | 50 +++++--------- 10 files changed, 226 insertions(+), 194 deletions(-) create mode 100644 tendermint/src/lite_impl.rs create mode 100644 tendermint/src/lite_impl/header.rs create mode 100644 tendermint/src/lite_impl/signed_header.rs create mode 100644 tendermint/src/lite_impl/validator_set.rs diff --git a/tendermint/src/block/header.rs b/tendermint/src/block/header.rs index b80ce2306..2f65e632d 100644 --- a/tendermint/src/block/header.rs +++ b/tendermint/src/block/header.rs @@ -1,8 +1,7 @@ //! Block headers -use crate::merkle::simple_hash_from_byte_vectors; + use crate::serializers; -use crate::{account, amino_types, block, chain, lite, Hash, Time}; -use amino_types::{message::AminoMessage, BlockId, ConsensusVersion, TimeMsg}; +use crate::{account, block, chain, Hash, Time}; use serde::{de::Error as _, Deserialize, Deserializer, Serialize}; use std::str::FromStr; @@ -76,59 +75,6 @@ pub struct Header { pub proposer_address: account::Id, } -impl lite::Header for Header { - type Time = Time; - - 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 { - self.next_validators_hash - } - - fn hash(&self) -> Hash { - // Note that if there is an encoding problem this will - // panic (as the golang code would): - // https://github.com/tendermint/tendermint/blob/134fe2896275bb926b49743c1e25493f6b24cc31/types/block.go#L393 - // https://github.com/tendermint/tendermint/blob/134fe2896275bb926b49743c1e25493f6b24cc31/types/encoding_helper.go#L9:6 - - let mut fields_bytes: Vec> = Vec::with_capacity(16); - fields_bytes.push(AminoMessage::bytes_vec(&ConsensusVersion::from( - &self.version, - ))); - fields_bytes.push(bytes_enc(self.chain_id.as_bytes())); - fields_bytes.push(encode_varint(self.height.value())); - fields_bytes.push(AminoMessage::bytes_vec(&TimeMsg::from(self.time))); - fields_bytes.push(encode_varint(self.num_txs)); - fields_bytes.push(encode_varint(self.total_txs)); - fields_bytes.push( - self.last_block_id - .as_ref() - .map_or(vec![], |id| AminoMessage::bytes_vec(&BlockId::from(id))), - ); - fields_bytes.push(self.last_commit_hash.as_ref().map_or(vec![], encode_hash)); - fields_bytes.push(self.data_hash.as_ref().map_or(vec![], encode_hash)); - fields_bytes.push(encode_hash(&self.validators_hash)); - fields_bytes.push(encode_hash(&self.next_validators_hash)); - fields_bytes.push(encode_hash(&self.consensus_hash)); - fields_bytes.push(bytes_enc(&self.app_hash)); - fields_bytes.push(self.last_results_hash.as_ref().map_or(vec![], encode_hash)); - fields_bytes.push(self.evidence_hash.as_ref().map_or(vec![], encode_hash)); - fields_bytes.push(bytes_enc(self.proposer_address.as_bytes())); - - Hash::Sha256(simple_hash_from_byte_vectors(fields_bytes)) - } -} - /// Parse empty block id as None. pub fn parse_non_empty_block_id<'de, D>(deserializer: D) -> Result, D::Error> where @@ -185,20 +131,3 @@ pub struct Version { )] pub app: u64, } - -fn bytes_enc(bytes: &[u8]) -> Vec { - let mut chain_id_enc = vec![]; - prost_amino::encode_length_delimiter(bytes.len(), &mut chain_id_enc).unwrap(); - chain_id_enc.append(&mut bytes.to_vec()); - chain_id_enc -} - -fn encode_hash(hash: &Hash) -> Vec { - bytes_enc(hash.as_bytes()) -} - -fn encode_varint(val: u64) -> Vec { - let mut val_enc = vec![]; - prost_amino::encoding::encode_varint(val, &mut val_enc); - val_enc -} diff --git a/tendermint/src/block/signed_header.rs b/tendermint/src/block/signed_header.rs index 1d0fb717e..07772909b 100644 --- a/tendermint/src/block/signed_header.rs +++ b/tendermint/src/block/signed_header.rs @@ -1,9 +1,10 @@ //! SignedHeader contains commit and and block header. //! It is what the rpc endpoint /commit returns and hence can be used by a //! light client. -use crate::{block, hash, lite, lite::Error, validator::Set, vote::SignedVote}; use serde::{Deserialize, Serialize}; +use crate::block; + /// Signed block headers #[derive(Clone, Debug, Deserialize, Serialize)] pub struct SignedHeader { @@ -12,82 +13,3 @@ pub struct SignedHeader { /// Commit containing signatures for the header pub commit: block::Commit, } - -impl lite::SignedHeader for SignedHeader { - type Header = block::Header; - type Commit = SignedHeader; - - fn header(&self) -> &block::Header { - &self.header - } - - fn commit(&self) -> &Self { - &self - } -} - -impl SignedHeader { - /// This is a private helper method to iterate over the underlying - /// votes to compute the voting power (see `voting_power_in` below). - fn iter(&self) -> Vec> { - let chain_id = self.header.chain_id.to_string(); - let mut votes = self.commit.precommits.clone().into_vec(); - votes - .drain(..) - .map(|opt| { - opt.map(|vote| { - SignedVote::new( - (&vote).into(), - &chain_id, - vote.validator_address, - vote.signature, - ) - }) - }) - .collect() - } -} - -impl lite::Commit for SignedHeader { - type ValidatorSet = Set; - - fn header_hash(&self) -> hash::Hash { - self.commit.block_id.hash - } - fn votes_len(&self) -> usize { - self.commit.precommits.len() - } - - fn voting_power_in(&self, validators: &Set) -> Result { - // 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 mut signed_power = 0u64; - for vote_opt in &self.iter() { - // skip absent and nil votes - // NOTE: do we want to check the validity of votes - // for nil ? - // TODO: clarify this! - 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 - let sign_bytes = vote.sign_bytes(); - - if !val.verify_signature(&sign_bytes, vote.signature()) { - return Err(Error::InvalidSignature); - } - signed_power += val.power(); - } - - Ok(signed_power) - } -} diff --git a/tendermint/src/lib.rs b/tendermint/src/lib.rs index 5edfdc293..fd597b5d6 100644 --- a/tendermint/src/lib.rs +++ b/tendermint/src/lib.rs @@ -43,6 +43,7 @@ pub mod genesis; pub mod hash; #[allow(dead_code, missing_docs)] pub mod lite; +pub mod lite_impl; pub mod merkle; mod moniker; pub mod net; diff --git a/tendermint/src/lite/types.rs b/tendermint/src/lite/types.rs index 3e979532e..534768e65 100644 --- a/tendermint/src/lite/types.rs +++ b/tendermint/src/lite/types.rs @@ -1,5 +1,5 @@ //! All traits that are necessary and need to be implemented to use the main -//! verification logic in `super::verifier` for a light client. +//! verification logic in [`super::verifier`] for a light client. use crate::block::Height; use crate::Hash; @@ -114,7 +114,7 @@ pub trait TrustThreshold { } } -/// Requester can be used to request `SignedHeaders` and `ValidatorSet`s for a +/// Requester can be used to request [`SignedHeader`]s and [`ValidatorSet`]s for a /// given height, e.g., by talking to a tendermint fullnode through RPC. pub trait Requester { // TODO(Liamsi): consider putting this trait and the Store into a separate module / file... diff --git a/tendermint/src/lite/verifier.rs b/tendermint/src/lite/verifier.rs index cdee7cfdb..9bd4e3f9b 100644 --- a/tendermint/src/lite/verifier.rs +++ b/tendermint/src/lite/verifier.rs @@ -8,13 +8,14 @@ //! // looks using the types and methods in this crate/module. //! ``` +use std::cmp::Ordering; +use std::time::{Duration, SystemTime}; + use crate::block::Height; use crate::lite::{ Commit, Error, Header, Requester, SignedHeader, Store, TrustThreshold, TrustedState, ValidatorSet, }; -use std::cmp::Ordering; -use std::time::{Duration, SystemTime}; /// 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. @@ -63,7 +64,7 @@ where } // ensure the validator size matches the commit size - // NOTE: this is commit structure specifc and should be + // NOTE: this is commit structure specific and should be // hidden from the light client ... if vals.len() != commit.votes_len() { return Err(Error::InvalidCommitLength); @@ -343,11 +344,13 @@ where } mod tests { - use super::*; - use crate::{hash::Algorithm, Hash}; use serde::Serialize; use sha2::{Digest, Sha256}; + use crate::{hash::Algorithm, Hash}; + + use super::*; + #[derive(Clone, Debug, Serialize)] struct MockHeader { height: u64, diff --git a/tendermint/src/lite_impl.rs b/tendermint/src/lite_impl.rs new file mode 100644 index 000000000..850600aba --- /dev/null +++ b/tendermint/src/lite_impl.rs @@ -0,0 +1,5 @@ +//! Implementation of all light client traits. + +pub mod header; +pub mod signed_header; +pub mod validator_set; diff --git a/tendermint/src/lite_impl/header.rs b/tendermint/src/lite_impl/header.rs new file mode 100644 index 000000000..fdbbc09e5 --- /dev/null +++ b/tendermint/src/lite_impl/header.rs @@ -0,0 +1,76 @@ +//! [`lite::Header`] implementation for [`block::Header`]. + +use crate::amino_types::{message::AminoMessage, BlockId, ConsensusVersion, TimeMsg}; +use crate::merkle::simple_hash_from_byte_vectors; +use crate::Hash; +use crate::{block, lite, Time}; + +impl lite::Header for block::Header { + type Time = Time; + + 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 { + self.next_validators_hash + } + + fn hash(&self) -> Hash { + // Note that if there is an encoding problem this will + // panic (as the golang code would): + // https://github.com/tendermint/tendermint/blob/134fe2896275bb926b49743c1e25493f6b24cc31/types/block.go#L393 + // https://github.com/tendermint/tendermint/blob/134fe2896275bb926b49743c1e25493f6b24cc31/types/encoding_helper.go#L9:6 + + let mut fields_bytes: Vec> = Vec::with_capacity(16); + fields_bytes.push(AminoMessage::bytes_vec(&ConsensusVersion::from( + &self.version, + ))); + fields_bytes.push(bytes_enc(self.chain_id.as_bytes())); + fields_bytes.push(encode_varint(self.height.value())); + fields_bytes.push(AminoMessage::bytes_vec(&TimeMsg::from(self.time))); + fields_bytes.push(encode_varint(self.num_txs)); + fields_bytes.push(encode_varint(self.total_txs)); + fields_bytes.push( + self.last_block_id + .as_ref() + .map_or(vec![], |id| AminoMessage::bytes_vec(&BlockId::from(id))), + ); + fields_bytes.push(self.last_commit_hash.as_ref().map_or(vec![], encode_hash)); + fields_bytes.push(self.data_hash.as_ref().map_or(vec![], encode_hash)); + fields_bytes.push(encode_hash(&self.validators_hash)); + fields_bytes.push(encode_hash(&self.next_validators_hash)); + fields_bytes.push(encode_hash(&self.consensus_hash)); + fields_bytes.push(bytes_enc(&self.app_hash)); + fields_bytes.push(self.last_results_hash.as_ref().map_or(vec![], encode_hash)); + fields_bytes.push(self.evidence_hash.as_ref().map_or(vec![], encode_hash)); + fields_bytes.push(bytes_enc(self.proposer_address.as_bytes())); + + Hash::Sha256(simple_hash_from_byte_vectors(fields_bytes)) + } +} + +fn bytes_enc(bytes: &[u8]) -> Vec { + let mut chain_id_enc = vec![]; + prost_amino::encode_length_delimiter(bytes.len(), &mut chain_id_enc).unwrap(); + chain_id_enc.append(&mut bytes.to_vec()); + chain_id_enc +} + +fn encode_hash(hash: &Hash) -> Vec { + bytes_enc(hash.as_bytes()) +} + +fn encode_varint(val: u64) -> Vec { + let mut val_enc = vec![]; + prost_amino::encoding::encode_varint(val, &mut val_enc); + val_enc +} diff --git a/tendermint/src/lite_impl/signed_header.rs b/tendermint/src/lite_impl/signed_header.rs new file mode 100644 index 000000000..744aad579 --- /dev/null +++ b/tendermint/src/lite_impl/signed_header.rs @@ -0,0 +1,84 @@ +//! [`lite::SignedHeader`] implementation for [`block::signed_header::SignedHeader`]. + +use crate::lite::Error; +use crate::validator::Set; +use crate::{block, hash, lite, vote}; + +impl lite::SignedHeader for block::signed_header::SignedHeader { + type Header = block::Header; + type Commit = block::signed_header::SignedHeader; + + fn header(&self) -> &block::Header { + &self.header + } + + fn commit(&self) -> &Self { + &self + } +} + +impl lite::Commit for block::signed_header::SignedHeader { + type ValidatorSet = Set; + + fn header_hash(&self) -> hash::Hash { + self.commit.block_id.hash + } + fn voting_power_in(&self, validators: &Set) -> Result { + // 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 mut signed_power = 0u64; + for vote_opt in &self.iter() { + // skip absent and nil votes + // NOTE: do we want to check the validity of votes + // for nil ? + // TODO: clarify this! + 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 + let sign_bytes = vote.sign_bytes(); + + if !val.verify_signature(&sign_bytes, vote.signature()) { + return Err(Error::InvalidSignature); + } + signed_power += val.power(); + } + + Ok(signed_power) + } + + fn votes_len(&self) -> usize { + self.commit.precommits.len() + } +} + +impl block::signed_header::SignedHeader { + /// This is a private helper method to iterate over the underlying + /// votes to compute the voting power (see `voting_power_in` below). + fn iter(&self) -> Vec> { + let chain_id = self.header.chain_id.to_string(); + let mut votes = self.commit.precommits.clone().into_vec(); + votes + .drain(..) + .map(|opt| { + opt.map(|vote| { + vote::SignedVote::new( + (&vote).into(), + &chain_id, + vote.validator_address, + vote.signature, + ) + }) + }) + .collect() + } +} diff --git a/tendermint/src/lite_impl/validator_set.rs b/tendermint/src/lite_impl/validator_set.rs new file mode 100644 index 000000000..389d5a59a --- /dev/null +++ b/tendermint/src/lite_impl/validator_set.rs @@ -0,0 +1,30 @@ +//! [`lite::ValidatorSet`] implementation for [`validator::Set`]. + +use crate::validator; +use crate::{lite, merkle, Hash}; + +impl lite::ValidatorSet for validator::Set { + /// Compute the Merkle root of the validator set + fn hash(&self) -> Hash { + let validator_bytes: Vec> = self + .validators() + .iter() + .map(|validator| validator.hash_bytes()) + .collect(); + Hash::Sha256(merkle::simple_hash_from_byte_vectors(validator_bytes)) + } + + fn total_power(&self) -> u64 { + self.validators().iter().fold(0u64, |total, val_info| { + total + val_info.voting_power.value() + }) + } + + fn len(&self) -> usize { + self.validators().len() + } + + fn is_empty(&self) -> bool { + self.validators().is_empty() + } +} diff --git a/tendermint/src/validator.rs b/tendermint/src/validator.rs index dc2090ec6..761754905 100644 --- a/tendermint/src/validator.rs +++ b/tendermint/src/validator.rs @@ -1,7 +1,5 @@ //! Tendermint validators -use crate::amino_types::message::AminoMessage; -use crate::{account, lite, merkle, vote, Hash, PublicKey}; use serde::{de::Error as _, Deserialize, Deserializer, Serialize, Serializer}; use signatory::{ ed25519, @@ -10,6 +8,9 @@ use signatory::{ use signatory_dalek::Ed25519Verifier; use subtle_encoding::base64; +use crate::amino_types::message::AminoMessage; +use crate::{account, vote, PublicKey}; + /// Validator set contains a vector of validators #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Set { @@ -24,6 +25,11 @@ impl Set { vals.sort_by(|v1, v2| v1.address.partial_cmp(&v2.address).unwrap()); Set { validators: vals } } + + /// Get Info of the underlying validators. + pub fn validators(&self) -> &Vec { + &self.validators + } } impl Set { @@ -36,32 +42,6 @@ impl Set { } } -impl lite::ValidatorSet for Set { - /// Compute the Merkle root of the validator set - fn hash(&self) -> Hash { - let validator_bytes: Vec> = self - .validators - .iter() - .map(|validator| validator.hash_bytes()) - .collect(); - Hash::Sha256(merkle::simple_hash_from_byte_vectors(validator_bytes)) - } - - fn total_power(&self) -> u64 { - self.validators.iter().fold(0u64, |total, val_info| { - total + val_info.voting_power.value() - }) - } - - fn len(&self) -> usize { - self.validators.len() - } - - fn is_empty(&self) -> bool { - self.validators.is_empty() - } -} - // TODO: maybe add a type (with an Option> field) instead // for light client integration tests only fn parse_vals<'de, D>(d: D) -> Result, D::Error> @@ -151,11 +131,11 @@ impl From<&Info> for InfoHashable { } } -// returns the bytes to be hashed into the Merkle tree - -// the leaves of the tree. this is an amino encoding of the -// pubkey and voting power, so it includes the pubkey's amino prefix. impl Info { - fn hash_bytes(&self) -> Vec { + /// Returns the bytes to be hashed into the Merkle tree - + /// the leaves of the tree. this is an amino encoding of the + /// pubkey and voting power, so it includes the pubkey's amino prefix. + pub fn hash_bytes(&self) -> Vec { AminoMessage::bytes_vec(&InfoHashable::from(self)) } } @@ -234,10 +214,12 @@ where #[cfg(test)] mod tests { - use super::*; - use crate::lite::ValidatorSet; use subtle_encoding::hex; + use crate::lite::ValidatorSet; + + use super::*; + // make a validator from a hex ed25519 pubkey and a voting power fn make_validator(pk_string: &str, vp: u64) -> Info { let pk = PublicKey::from_raw_ed25519(&hex::decode_upper(pk_string).unwrap()).unwrap();