// Copyright 2017-2018 Parity Technologies (UK) Ltd. // This file is part of Substrate. // Substrate is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // Substrate is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // You should have received a copy of the GNU General Public License // along with Substrate. If not, see . // tag::description[] //! BFT Agreement based on a rotating proposer in different rounds. //! //! Where this crate refers to input stream, should never logically conclude. //! The logic in this crate assumes that messages flushed to the output stream //! will eventually reach other nodes and that our own messages are not included //! in the input stream. //! //! Note that it is possible to witness agreement being reached without ever //! seeing the candidate. Any candidates seen will be checked for validity. //! //! Although technically the agreement will always complete (given the eventual //! delivery of messages), in practice it is possible for this future to //! conclude without having witnessed the conclusion. //! In general, this future should be pre-empted by the import of a justification //! set for this block height. // end::description[] #![cfg(feature = "rhd")] #![recursion_limit="128"] extern crate parity_codec as codec; extern crate substrate_primitives as primitives; extern crate srml_support as runtime_support; extern crate sr_primitives as runtime_primitives; extern crate sr_version as runtime_version; extern crate sr_io as runtime_io; extern crate tokio; #[cfg(test)] extern crate substrate_keyring as keyring; extern crate parking_lot; extern crate rhododendron; #[macro_use] extern crate log; extern crate futures; #[macro_use] extern crate error_chain; #[macro_use] extern crate serde; #[macro_use] extern crate parity_codec_derive; pub mod error; use std::sync::Arc; use std::sync::atomic::{AtomicUsize, Ordering}; use std::time::{Instant, Duration}; use codec::Encode; use runtime_primitives::{generic::BlockId, Justification}; use runtime_primitives::traits::{Block, Header}; use primitives::{AuthorityId, ed25519, ed25519::LocalizedSignature}; use futures::{Async, Stream, Sink, Future, IntoFuture}; use futures::sync::oneshot; use tokio::timer::Delay; use parking_lot::Mutex; pub use rhododendron::{InputStreamConcluded, AdvanceRoundReason, Message as RhdMessage, Vote as RhdMessageVote}; pub use error::{Error, ErrorKind}; // pub mod misbehaviour_check; // statuses for an agreement mod status { pub const LIVE: usize = 0; pub const BAD: usize = 1; pub const GOOD: usize = 2; } /// Localized message type. pub type LocalizedMessage = rhododendron::LocalizedMessage< B, ::Hash, AuthorityId, LocalizedSignature >; /// Justification of some hash. pub struct RhdJustification(rhododendron::Justification); /// Justification of a prepare message. pub struct PrepareJustification(rhododendron::PrepareJustification); /// Unchecked justification. pub struct UncheckedJustification(rhododendron::UncheckedJustification); impl UncheckedJustification { /// Create a new, unchecked justification. pub fn new(digest: H, signatures: Vec, round_number: usize) -> Self { UncheckedJustification(rhododendron::UncheckedJustification { digest, signatures, round_number, }) } } impl Into for RhdJustification { fn into(self) -> Justification { let p : Justification = UncheckedJustification(self.0.uncheck()).into(); p } } impl From> for UncheckedJustification { fn from(inner: rhododendron::UncheckedJustification) -> Self { UncheckedJustification(inner) } } impl From for UncheckedJustification { fn from(just: Justification) -> Self { UncheckedJustification(rhododendron::UncheckedJustification { round_number: just.round_number as usize, digest: just.hash, signatures: just.signatures.into_iter().map(|(from, sig)| LocalizedSignature { signer: from.into(), signature: sig, }).collect(), }) } } impl Into for UncheckedJustification { fn into(self) -> Justification { Justification { round_number: self.0.round_number as u32, hash: self.0.digest, signatures: self.0.signatures.into_iter().map(|s| (s.signer.into(), s.signature)).collect(), } } } /// Result of a committed round of BFT pub type Committed = rhododendron::Committed::Hash, LocalizedSignature>; /// Communication between BFT participants. pub type Communication = rhododendron::Communication::Hash, AuthorityId, LocalizedSignature>; /// Misbehavior observed from BFT participants. pub type Misbehavior = rhododendron::Misbehavior; /// Environment producer for a BFT instance. Creates proposer instance and communication streams. pub trait Environment { /// The proposer type this creates. type Proposer: Proposer; /// The input stream type. type Input: Stream, Error=>::Error>; /// The output stream type. type Output: Sink, SinkError=>::Error>; /// Error which can occur upon creation. type Error: From; /// Initialize the proposal logic on top of a specific header. /// Produces the proposer and message streams for this instance of BFT agreement. // TODO: provide state context explicitly? fn init(&self, parent_header: &B::Header, authorities: &[AuthorityId], sign_with: Arc) -> Result<(Self::Proposer, Self::Input, Self::Output), Self::Error>; } /// Logic for a proposer. /// /// This will encapsulate creation and evaluation of proposals at a specific /// block. pub trait Proposer { /// Error type which can occur when proposing or evaluating. type Error: From + From + ::std::fmt::Debug + 'static; /// Future that resolves to a committed proposal. type Create: IntoFuture; /// Future that resolves when a proposal is evaluated. type Evaluate: IntoFuture; /// Create a proposal. fn propose(&self) -> Self::Create; /// Evaluate proposal. True means valid. fn evaluate(&self, proposal: &B) -> Self::Evaluate; /// Import witnessed misbehavior. fn import_misbehavior(&self, misbehavior: Vec<(AuthorityId, Misbehavior)>); /// Determine the proposer for a given round. This should be a deterministic function /// with consistent results across all authorities. fn round_proposer(&self, round_number: usize, authorities: &[AuthorityId]) -> AuthorityId; /// Hook called when a BFT round advances without a proposal. fn on_round_end(&self, _round_number: usize, _proposed: bool) { } } /// Trait for getting the authorities at a given block. pub trait Authorities { /// Get the authorities at the given block. fn authorities(&self, at: &BlockId) -> Result, Error>; } // caches the round number to start at if we end up with BFT consensus on the same // parent hash more than once (happens if block is bad). // // this will force a committed but locally-bad block to be considered analogous to // a round advancement vote. #[derive(Debug)] struct RoundCache { hash: Option, start_round: usize, } /// Instance of BFT agreement. struct BftInstance { key: Arc, authorities: Vec, parent_hash: B::Hash, round_timeout_multiplier: u64, cache: Arc>>, proposer: P, } impl> BftInstance where B: Clone + Eq, B::Hash: ::std::hash::Hash { fn round_timeout_duration(&self, round: usize) -> Duration { // 2^(min(6, x/8)) * 10 // Grows exponentially starting from 10 seconds, capped at 640 seconds. const ROUND_INCREMENT_STEP: usize = 8; let round = round / ROUND_INCREMENT_STEP; let round = ::std::cmp::min(6, round) as u32; let timeout = 1u64.checked_shl(round) .unwrap_or_else(u64::max_value) .saturating_mul(self.round_timeout_multiplier); Duration::from_secs(timeout) } fn update_round_cache(&self, current_round: usize) { let mut cache = self.cache.lock(); if cache.hash.as_ref() == Some(&self.parent_hash) { cache.start_round = current_round + 1; } } } impl> rhododendron::Context for BftInstance where B: Clone + Eq, B::Hash: ::std::hash::Hash, { type Error = P::Error; type AuthorityId = AuthorityId; type Digest = B::Hash; type Signature = LocalizedSignature; type Candidate = B; type RoundTimeout = Box>; type CreateProposal = ::Future; type EvaluateProposal = ::Future; fn local_id(&self) -> AuthorityId { self.key.public().into() } fn proposal(&self) -> Self::CreateProposal { self.proposer.propose().into_future() } fn candidate_digest(&self, proposal: &B) -> B::Hash { proposal.hash() } fn sign_local(&self, message: RhdMessage) -> LocalizedMessage { sign_message(message, &*self.key, self.parent_hash.clone()) } fn round_proposer(&self, round: usize) -> AuthorityId { self.proposer.round_proposer(round, &self.authorities[..]) } fn proposal_valid(&self, proposal: &B) -> Self::EvaluateProposal { self.proposer.evaluate(proposal).into_future() } fn begin_round_timeout(&self, round: usize) -> Self::RoundTimeout { let timeout = self.round_timeout_duration(round); let fut = Delay::new(Instant::now() + timeout) .map_err(|e| Error::from(ErrorKind::FaultyTimer(e))) .map_err(Into::into); Box::new(fut) } fn on_advance_round( &self, accumulator: &::rhododendron::Accumulator, round: usize, next_round: usize, reason: AdvanceRoundReason, ) { use std::collections::HashSet; let collect_pubkeys = |participants: HashSet<&Self::AuthorityId>| participants.into_iter() .map(|p| ::ed25519::Public::from_raw(p.0)) .collect::>(); let round_timeout = self.round_timeout_duration(next_round); debug!(target: "bft", "Advancing to round {} from {}", next_round, round); debug!(target: "bft", "Participating authorities: {:?}", collect_pubkeys(accumulator.participants())); debug!(target: "bft", "Voting authorities: {:?}", collect_pubkeys(accumulator.voters())); debug!(target: "bft", "Round {} should end in at most {} seconds from now", next_round, round_timeout.as_secs()); self.update_round_cache(next_round); if let AdvanceRoundReason::Timeout = reason { self.proposer.on_round_end(round, accumulator.proposal().is_some()); } } } /// A future that resolves either when canceled (witnessing a block from the network at same height) /// or when agreement completes. pub struct BftFuture where B: Block + Clone + Eq, B::Hash: ::std::hash::Hash, P: Proposer, InStream: Stream, Error=P::Error>, OutSink: Sink, SinkError=P::Error>, { inner: rhododendron::Agreement, InStream, OutSink>, status: Arc, cancel: oneshot::Receiver<()>, import: Arc, } impl Future for BftFuture where B: Block + Clone + Eq, B::Hash: ::std::hash::Hash, P: Proposer, P::Error: ::std::fmt::Display, I: BlockImport, InStream: Stream, Error=P::Error>, OutSink: Sink, SinkError=P::Error>, { type Item = (); type Error = (); fn poll(&mut self) -> ::futures::Poll<(), ()> { // service has canceled the future. bail let cancel = match self.cancel.poll() { Ok(Async::Ready(())) | Err(_) => true, Ok(Async::NotReady) => false, }; // TODO: handle and log this error in a way which isn't noisy on exit. let committed = match self.inner.poll().map_err(|_| ()) { Ok(Async::Ready(x)) => x, Ok(Async::NotReady) => return Ok(if cancel { Async::Ready(()) } else { Async::NotReady }), Err(()) => return Err(()), }; // if something was committed, the round leader must have proposed. self.inner.context().proposer.on_round_end(committed.round_number, true); // If we didn't see the proposal (very unlikely), // we will get the block from the network later. if let Some(justified_block) = committed.candidate { let hash = justified_block.hash(); info!(target: "bft", "Importing block #{} ({}) directly from BFT consensus", justified_block.header().number(), hash); let just : Justification = RhdJustification(committed.justification).into(); let import_ok = self.import.import_block( justified_block, just, &self.inner.context().authorities ); if !import_ok { warn!(target: "bft", "{:?} was bad block agreed on in round #{}", hash, committed.round_number); self.status.store(status::BAD, Ordering::Release); } else { self.status.store(status::GOOD, Ordering::Release); } } else { // assume good unless we received the proposal. self.status.store(status::GOOD, Ordering::Release); } self.inner.context().update_round_cache(committed.round_number); Ok(Async::Ready(())) } } impl Drop for BftFuture where B: Block + Clone + Eq, B::Hash: ::std::hash::Hash, P: Proposer, InStream: Stream, Error=P::Error>, OutSink: Sink, SinkError=P::Error>, { fn drop(&mut self) { // TODO: have a trait member to pass misbehavior reports into. let misbehavior = self.inner.drain_misbehavior().collect::>(); self.inner.context().proposer.import_misbehavior(misbehavior); } } struct AgreementHandle { status: Arc, send_cancel: Option>, } impl AgreementHandle { fn status(&self) -> usize { self.status.load(Ordering::Acquire) } } impl Drop for AgreementHandle { fn drop(&mut self) { if let Some(sender) = self.send_cancel.take() { let _ = sender.send(()); } } } /// The BftService kicks off the agreement process on top of any blocks it /// is notified of. /// /// This assumes that it is being run in the context of a tokio runtime. pub struct BftService { client: Arc, live_agreement: Mutex>, round_cache: Arc>>, round_timeout_multiplier: u64, key: Arc, // TODO: key changing over time. factory: P, } impl BftService where B: Block + Clone + Eq, P: Environment, >::Error: ::std::fmt::Display, I: BlockImport + Authorities, { /// Create a new service instance. pub fn new(client: Arc, key: Arc, factory: P) -> BftService { BftService { client: client, live_agreement: Mutex::new(None), round_cache: Arc::new(Mutex::new(RoundCache { hash: None, start_round: 0, })), round_timeout_multiplier: 10, key: key, // TODO: key changing over time. factory, } } /// Get the local Authority ID. pub fn local_id(&self) -> AuthorityId { // TODO: based on a header and some keystore. self.key.public().into() } /// Signal that a valid block with the given header has been imported. /// /// If the local signing key is an authority, this will begin the consensus process to build a /// block on top of it. If the executor fails to run the future, an error will be returned. /// Returns `None` if the agreement on the block with given parent is already in progress. pub fn build_upon(&self, header: &B::Header) -> Result>::Proposer, I,

>::Input,

>::Output, >>, P::Error> where { let hash = header.hash(); let mut live_agreement = self.live_agreement.lock(); let can_build = live_agreement.as_ref() .map_or(true, |x| self.can_build_on_inner(header, x)); if !can_build { return Ok(None) } let authorities = self.client.authorities(&BlockId::Hash(hash.clone()))?; let n = authorities.len(); let max_faulty = max_faulty_of(n); trace!(target: "bft", "Initiating agreement on top of #{}, {:?}", header.number(), hash); trace!(target: "bft", "max_faulty_of({})={}", n, max_faulty); let local_id = self.local_id(); if !authorities.contains(&local_id) { // cancel current agreement live_agreement.take(); Err(ErrorKind::InvalidAuthority(local_id).into())?; } let (proposer, input, output) = self.factory.init(header, &authorities, self.key.clone())?; let bft_instance = BftInstance { proposer, parent_hash: hash.clone(), cache: self.round_cache.clone(), round_timeout_multiplier: self.round_timeout_multiplier, key: self.key.clone(), authorities: authorities, }; let mut agreement = rhododendron::agree( bft_instance, n, max_faulty, input, output, ); // fast forward round number if necessary. { let mut cache = self.round_cache.lock(); trace!(target: "bft", "Round cache: {:?}", &*cache); if cache.hash.as_ref() == Some(&hash) { trace!(target: "bft", "Fast-forwarding to round {}", cache.start_round); let start_round = cache.start_round; cache.start_round += 1; drop(cache); agreement.fast_forward(start_round); } else { *cache = RoundCache { hash: Some(hash.clone()), start_round: 1, }; } } let status = Arc::new(AtomicUsize::new(status::LIVE)); let (tx, rx) = oneshot::channel(); // cancel current agreement. *live_agreement = Some((header.clone(), AgreementHandle { send_cancel: Some(tx), status: status.clone(), })); Ok(Some(BftFuture { inner: agreement, status: status, cancel: rx, import: self.client.clone(), })) } /// Cancel current agreement if any. pub fn cancel_agreement(&self) { self.live_agreement.lock().take(); } /// Whether we can build using the given header. pub fn can_build_on(&self, header: &B::Header) -> bool { self.live_agreement.lock().as_ref() .map_or(true, |x| self.can_build_on_inner(header, x)) } /// Get a reference to the underyling client. pub fn client(&self) -> &I { &*self.client } fn can_build_on_inner(&self, header: &B::Header, live: &(B::Header, AgreementHandle)) -> bool { let hash = header.hash(); let &(ref live_header, ref handle) = live; match handle.status() { _ if *header != *live_header && *live_header.parent_hash() != hash => true, // can always follow with next block. status::BAD => hash == live_header.hash(), // bad block can be re-agreed on. _ => false, // canceled won't appear since we overwrite the handle before returning. } } } /// Given a total number of authorities, yield the maximum faulty that would be allowed. /// This will always be under 1/3. pub fn max_faulty_of(n: usize) -> usize { n.saturating_sub(1) / 3 } /// Given a total number of authorities, yield the minimum required signatures. /// This will always be over 2/3. pub fn bft_threshold(n: usize) -> usize { n - max_faulty_of(n) } // /// Sign a BFT message with the given key. pub fn sign_message( message: RhdMessage, key: &ed25519::Pair, parent_hash: B::Hash ) -> LocalizedMessage { let signer = key.public(); let sign_action = |action: ::rhododendron::Vote| { let primitive = ::rhododendron::LocalizedVote { parent: parent_hash.clone(), action, }; let to_sign = Encode::encode(&primitive); LocalizedSignature { signer: signer.clone(), signature: key.sign(&to_sign), } }; match message { RhdMessage::Propose(r, proposal) => { let header_hash = proposal.hash(); let action_header = ::rhododendron::ProposeHeader(r as u32, header_hash.clone()); let action_propose = ::rhododendron::Propose(r as u32, proposal.clone()); ::rhododendron::LocalizedMessage::Propose(::rhododendron::LocalizedProposal { round_number: r, proposal, digest: header_hash, sender: signer.clone().into(), digest_signature: sign_action(action_header), full_signature: sign_action(action_propose), }) } RhdMessage::Vote(vote) => ::rhododendron::LocalizedMessage::Vote( ::rhododendron::LocalizedVote { vote: vote, sender: signer.clone().into(), signature: sign_action(action), } ) } } #[cfg(test)] mod tests { use super::*; use std::collections::HashSet; use runtime_primitives::testing::{Block as GenericTestBlock, Header as TestHeader}; use primitives::H256; use self::keyring::Keyring; extern crate substrate_keyring as keyring; type TestBlock = GenericTestBlock<()>; struct FakeClient { authorities: Vec, imported_heights: Mutex> } impl BlockImport for FakeClient { fn import_block(&self, block: TestBlock, _justification: Justification, _authorities: &[AuthorityId]) -> bool { assert!(self.imported_heights.lock().insert(block.header.number)); true } } impl Authorities for FakeClient { fn authorities(&self, _at: &BlockId) -> Result, Error> { Ok(self.authorities.clone()) } } // "black hole" output sink. struct Comms(::std::marker::PhantomData); impl Sink for Comms { type SinkItem = Communication; type SinkError = E; fn start_send(&mut self, _item: Communication) -> ::futures::StartSend, E> { Ok(::futures::AsyncSink::Ready) } fn poll_complete(&mut self) -> ::futures::Poll<(), E> { Ok(Async::Ready(())) } } impl Stream for Comms { type Item = Communication; type Error = E; fn poll(&mut self) -> ::futures::Poll, Self::Error> { Ok(::futures::Async::NotReady) } } struct DummyFactory; struct DummyProposer(u64); impl Environment for DummyFactory { type Proposer = DummyProposer; type Input = Comms; type Output = Comms; type Error = Error; fn init(&self, parent_header: &TestHeader, _authorities: &[AuthorityId], _sign_with: Arc) -> Result<(DummyProposer, Self::Input, Self::Output), Error> { Ok((DummyProposer(parent_header.number + 1), Comms(::std::marker::PhantomData), Comms(::std::marker::PhantomData))) } } impl Proposer for DummyProposer { type Error = Error; type Create = Result; type Evaluate = Result; fn propose(&self) -> Result { Ok(TestBlock { header: from_block_number(self.0), extrinsics: Default::default() }) } fn evaluate(&self, proposal: &TestBlock) -> Result { Ok(proposal.header.number == self.0) } fn import_misbehavior(&self, _misbehavior: Vec<(AuthorityId, Misbehavior)>) {} fn round_proposer(&self, round_number: usize, authorities: &[AuthorityId]) -> AuthorityId { authorities[round_number % authorities.len()].clone() } } fn make_service(client: FakeClient) -> BftService { BftService { client: Arc::new(client), live_agreement: Mutex::new(None), round_cache: Arc::new(Mutex::new(RoundCache { hash: None, start_round: 0, })), round_timeout_multiplier: 10, key: Arc::new(Keyring::One.into()), factory: DummyFactory } } fn sign_vote(vote: ::rhododendron::Vote, key: &ed25519::Pair, parent_hash: H256) -> LocalizedSignature { match sign_message::(vote.into(), key, parent_hash) { ::rhododendron::LocalizedMessage::Vote(vote) => vote.signature, _ => panic!("signing vote leads to signed vote"), } } fn from_block_number(num: u64) -> TestHeader { TestHeader::new( num, Default::default(), Default::default(), Default::default(), Default::default(), ) } #[test] fn future_gets_preempted() { let client = FakeClient { authorities: vec![ Keyring::One.to_raw_public().into(), Keyring::Two.to_raw_public().into(), Keyring::Alice.to_raw_public().into(), Keyring::Eve.to_raw_public().into(), ], imported_heights: Mutex::new(HashSet::new()), }; let service = make_service(client); let first = from_block_number(2); let first_hash = first.hash(); let mut second = from_block_number(3); second.parent_hash = first_hash; let _second_hash = second.hash(); let mut first_bft = service.build_upon(&first).unwrap().unwrap(); assert!(service.live_agreement.lock().as_ref().unwrap().0 == first); let _second_bft = service.build_upon(&second).unwrap(); assert!(service.live_agreement.lock().as_ref().unwrap().0 != first); assert!(service.live_agreement.lock().as_ref().unwrap().0 == second); // first_bft has been cancelled. need to swap out so we can check it. let (_tx, mut rx) = oneshot::channel(); ::std::mem::swap(&mut rx, &mut first_bft.cancel); assert!(rx.wait().is_ok()); } #[test] fn max_faulty() { assert_eq!(max_faulty_of(3), 0); assert_eq!(max_faulty_of(4), 1); assert_eq!(max_faulty_of(100), 33); assert_eq!(max_faulty_of(0), 0); assert_eq!(max_faulty_of(11), 3); assert_eq!(max_faulty_of(99), 32); } #[test] fn justification_check_works() { let parent_hash = Default::default(); let hash = [0xff; 32].into(); let authorities = vec![ Keyring::One.to_raw_public().into(), Keyring::Two.to_raw_public().into(), Keyring::Alice.to_raw_public().into(), Keyring::Eve.to_raw_public().into(), ]; let authorities_keys = vec![ Keyring::One.into(), Keyring::Two.into(), Keyring::Alice.into(), Keyring::Eve.into(), ]; let unchecked = UncheckedJustification(rhododendron::UncheckedJustification { digest: hash, round_number: 1, signatures: authorities_keys.iter().take(3).map(|key| { sign_vote(rhododendron::Vote::Commit(1, hash).into(), key, parent_hash) }).collect(), }); assert!(check_justification::(&authorities, parent_hash, unchecked).is_ok()); let unchecked = UncheckedJustification(rhododendron::UncheckedJustification { digest: hash, round_number: 0, // wrong round number (vs. the signatures) signatures: authorities_keys.iter().take(3).map(|key| { sign_vote(rhododendron::Vote::Commit(1, hash).into(), key, parent_hash) }).collect(), }); assert!(check_justification::(&authorities, parent_hash, unchecked).is_err()); // not enough signatures. let unchecked = UncheckedJustification(rhododendron::UncheckedJustification { digest: hash, round_number: 1, signatures: authorities_keys.iter().take(2).map(|key| { sign_vote(rhododendron::Vote::Commit(1, hash).into(), key, parent_hash) }).collect(), }); assert!(check_justification::(&authorities, parent_hash, unchecked).is_err()); // wrong hash. let unchecked = UncheckedJustification(rhododendron::UncheckedJustification { digest: [0xfe; 32].into(), round_number: 1, signatures: authorities_keys.iter().take(3).map(|key| { sign_vote(rhododendron::Vote::Commit(1, hash).into(), key, parent_hash) }).collect(), }); assert!(check_justification::(&authorities, parent_hash, unchecked).is_err()); } #[test] fn propose_check_works() { let parent_hash = Default::default(); let authorities = vec![ Keyring::Alice.to_raw_public().into(), Keyring::Eve.to_raw_public().into(), ]; let block = TestBlock { header: from_block_number(1), extrinsics: Default::default() }; let proposal = sign_message(::rhododendron::Message::Propose(1, block.clone()), &Keyring::Alice.pair(), parent_hash);; if let ::rhododendron::LocalizedMessage::Propose(proposal) = proposal { assert!(check_proposal(&authorities, &parent_hash, &proposal).is_ok()); let mut invalid_round = proposal.clone(); invalid_round.round_number = 0; assert!(check_proposal(&authorities, &parent_hash, &invalid_round).is_err()); let mut invalid_digest = proposal.clone(); invalid_digest.digest = [0xfe; 32].into(); assert!(check_proposal(&authorities, &parent_hash, &invalid_digest).is_err()); } else { assert!(false); } // Not an authority let proposal = sign_message::(::rhododendron::Message::Propose(1, block), &Keyring::Bob.pair(), parent_hash);; if let ::rhododendron::LocalizedMessage::Propose(proposal) = proposal { assert!(check_proposal(&authorities, &parent_hash, &proposal).is_err()); } else { assert!(false); } } #[test] fn vote_check_works() { let parent_hash: H256 = Default::default(); let hash: H256 = [0xff; 32].into(); let authorities = vec![ Keyring::Alice.to_raw_public().into(), Keyring::Eve.to_raw_public().into(), ]; let vote = sign_message::(::rhododendron::Message::Vote(::rhododendron::Vote::Prepare(1, hash)), &Keyring::Alice.pair(), parent_hash);; if let ::rhododendron::LocalizedMessage::Vote(vote) = vote { assert!(check_vote::(&authorities, &parent_hash, &vote).is_ok()); let mut invalid_sender = vote.clone(); invalid_sender.signature.signer = Keyring::Eve.into(); assert!(check_vote::(&authorities, &parent_hash, &invalid_sender).is_err()); } else { assert!(false); } // Not an authority let vote = sign_message::(::rhododendron::Message::Vote(::rhododendron::Vote::Prepare(1, hash)), &Keyring::Bob.pair(), parent_hash);; if let ::rhododendron::LocalizedMessage::Vote(vote) = vote { assert!(check_vote::(&authorities, &parent_hash, &vote).is_err()); } else { assert!(false); } } #[test] fn drop_bft_future_does_not_deadlock() { let client = FakeClient { authorities: vec![ Keyring::One.to_raw_public().into(), Keyring::Two.to_raw_public().into(), Keyring::Alice.to_raw_public().into(), Keyring::Eve.to_raw_public().into(), ], imported_heights: Mutex::new(HashSet::new()), }; let service = make_service(client); let first = from_block_number(2); let first_hash = first.hash(); let mut second = from_block_number(3); second.parent_hash = first_hash; let _ = service.build_upon(&first).unwrap(); assert!(service.live_agreement.lock().as_ref().unwrap().0 == first); service.live_agreement.lock().take(); } #[test] fn bft_can_build_though_skipped() { let client = FakeClient { authorities: vec![ Keyring::One.to_raw_public().into(), Keyring::Two.to_raw_public().into(), Keyring::Alice.to_raw_public().into(), Keyring::Eve.to_raw_public().into(), ], imported_heights: Mutex::new(HashSet::new()), }; let service = make_service(client); let first = from_block_number(2); let first_hash = first.hash(); let mut second = from_block_number(3); second.parent_hash = first_hash; let mut third = from_block_number(4); third.parent_hash = second.hash(); let _ = service.build_upon(&first).unwrap(); assert!(service.live_agreement.lock().as_ref().unwrap().0 == first); // BFT has not seen second, but will move forward on third service.build_upon(&third).unwrap(); assert!(service.live_agreement.lock().as_ref().unwrap().0 == third); // but we are not walking backwards service.build_upon(&second).unwrap(); assert!(service.live_agreement.lock().as_ref().unwrap().0 == third); } }