Generic engine utilities (#4258)
* move modules up * make structs generic * reound to view and tests * fix
This commit is contained in:
parent
a58fad06a7
commit
e9251a9325
@ -16,6 +16,8 @@
|
||||
|
||||
//! Consensus engine specification and basic implementations.
|
||||
|
||||
mod transition;
|
||||
mod vote_collector;
|
||||
mod null_engine;
|
||||
mod instant_seal;
|
||||
mod basic_authority;
|
||||
|
@ -17,14 +17,15 @@
|
||||
//! Tendermint message handling.
|
||||
|
||||
use util::*;
|
||||
use super::{Height, Round, BlockHash, Step};
|
||||
use super::{Height, View, BlockHash, Step};
|
||||
use error::Error;
|
||||
use header::Header;
|
||||
use rlp::*;
|
||||
use rlp::{Rlp, UntrustedRlp, RlpStream, Stream, Encodable, Decodable, Decoder, DecoderError, View as RlpView};
|
||||
use ethkey::{recover, public_to_address};
|
||||
use super::super::vote_collector::Message;
|
||||
|
||||
/// Message transmitted between consensus participants.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Hash)]
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Hash, Default)]
|
||||
pub struct ConsensusMessage {
|
||||
pub vote_step: VoteStep,
|
||||
pub block_hash: Option<BlockHash>,
|
||||
@ -35,42 +36,55 @@ pub struct ConsensusMessage {
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Hash)]
|
||||
pub struct VoteStep {
|
||||
pub height: Height,
|
||||
pub round: Round,
|
||||
pub view: View,
|
||||
pub step: Step,
|
||||
}
|
||||
|
||||
|
||||
impl VoteStep {
|
||||
pub fn new(height: Height, round: Round, step: Step) -> Self {
|
||||
VoteStep { height: height, round: round, step: step }
|
||||
pub fn new(height: Height, view: View, step: Step) -> Self {
|
||||
VoteStep { height: height, view: view, step: step }
|
||||
}
|
||||
|
||||
pub fn is_height(&self, height: Height) -> bool {
|
||||
self.height == height
|
||||
}
|
||||
|
||||
pub fn is_round(&self, height: Height, round: Round) -> bool {
|
||||
self.height == height && self.round == round
|
||||
pub fn is_view(&self, height: Height, view: View) -> bool {
|
||||
self.height == height && self.view == view
|
||||
}
|
||||
}
|
||||
|
||||
/// Header consensus round.
|
||||
pub fn consensus_round(header: &Header) -> Result<Round, ::rlp::DecoderError> {
|
||||
let round_rlp = header.seal().get(0).expect("seal passed basic verification; seal has 3 fields; qed");
|
||||
UntrustedRlp::new(round_rlp.as_slice()).as_val()
|
||||
/// Header consensus view.
|
||||
pub fn consensus_view(header: &Header) -> Result<View, ::rlp::DecoderError> {
|
||||
let view_rlp = header.seal().get(0).expect("seal passed basic verification; seal has 3 fields; qed");
|
||||
UntrustedRlp::new(view_rlp.as_slice()).as_val()
|
||||
}
|
||||
|
||||
impl Message for ConsensusMessage {
|
||||
type Round = VoteStep;
|
||||
|
||||
fn signature(&self) -> H520 { self.signature }
|
||||
|
||||
fn block_hash(&self) -> Option<H256> { self.block_hash }
|
||||
|
||||
fn round(&self) -> &VoteStep { &self.vote_step }
|
||||
|
||||
fn is_broadcastable(&self) -> bool { self.vote_step.step.is_pre() }
|
||||
}
|
||||
|
||||
impl ConsensusMessage {
|
||||
pub fn new(signature: H520, height: Height, round: Round, step: Step, block_hash: Option<BlockHash>) -> Self {
|
||||
pub fn new(signature: H520, height: Height, view: View, step: Step, block_hash: Option<BlockHash>) -> Self {
|
||||
ConsensusMessage {
|
||||
signature: signature,
|
||||
block_hash: block_hash,
|
||||
vote_step: VoteStep::new(height, round, step),
|
||||
vote_step: VoteStep::new(height, view, step),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_proposal(header: &Header) -> Result<Self, ::rlp::DecoderError> {
|
||||
Ok(ConsensusMessage {
|
||||
vote_step: VoteStep::new(header.number() as Height, consensus_round(header)?, Step::Propose),
|
||||
vote_step: VoteStep::new(header.number() as Height, consensus_view(header)?, Step::Propose),
|
||||
signature: UntrustedRlp::new(header.seal().get(1).expect("seal passed basic verification; seal has 3 fields; qed").as_slice()).as_val()?,
|
||||
block_hash: Some(header.bare_hash()),
|
||||
})
|
||||
@ -100,6 +114,12 @@ impl ConsensusMessage {
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for VoteStep {
|
||||
fn default() -> Self {
|
||||
VoteStep::new(0, 0, Step::Propose)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for VoteStep {
|
||||
fn partial_cmp(&self, m: &VoteStep) -> Option<Ordering> {
|
||||
Some(self.cmp(m))
|
||||
@ -110,8 +130,8 @@ impl Ord for VoteStep {
|
||||
fn cmp(&self, m: &VoteStep) -> Ordering {
|
||||
if self.height != m.height {
|
||||
self.height.cmp(&m.height)
|
||||
} else if self.round != m.round {
|
||||
self.round.cmp(&m.round)
|
||||
} else if self.view != m.view {
|
||||
self.view.cmp(&m.view)
|
||||
} else {
|
||||
self.step.number().cmp(&m.step.number())
|
||||
}
|
||||
@ -146,7 +166,7 @@ impl Encodable for Step {
|
||||
}
|
||||
}
|
||||
|
||||
/// (signature, (height, round, step, block_hash))
|
||||
/// (signature, (height, view, step, block_hash))
|
||||
impl Decodable for ConsensusMessage {
|
||||
fn decode<D>(decoder: &D) -> Result<Self, DecoderError> where D: Decoder {
|
||||
let rlp = decoder.as_rlp();
|
||||
@ -175,7 +195,7 @@ impl Encodable for ConsensusMessage {
|
||||
pub fn message_info_rlp(vote_step: &VoteStep, block_hash: Option<BlockHash>) -> Bytes {
|
||||
// TODO: figure out whats wrong with nested list encoding
|
||||
let mut s = RlpStream::new_list(5);
|
||||
s.append(&vote_step.height).append(&vote_step.round).append(&vote_step.step).append(&block_hash.unwrap_or_else(H256::zero));
|
||||
s.append(&vote_step.height).append(&vote_step.view).append(&vote_step.step).append(&block_hash.unwrap_or_else(H256::zero));
|
||||
s.out()
|
||||
}
|
||||
|
||||
@ -189,11 +209,11 @@ pub fn message_full_rlp(signature: &H520, vote_info: &Bytes) -> Bytes {
|
||||
mod tests {
|
||||
use util::*;
|
||||
use rlp::*;
|
||||
use super::super::Step;
|
||||
use super::*;
|
||||
use ethkey::Secret;
|
||||
use account_provider::AccountProvider;
|
||||
use header::Header;
|
||||
use ethkey::Secret;
|
||||
use super::super::Step;
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn encode_decode() {
|
||||
@ -201,7 +221,7 @@ mod tests {
|
||||
signature: H520::default(),
|
||||
vote_step: VoteStep {
|
||||
height: 10,
|
||||
round: 123,
|
||||
view: 123,
|
||||
step: Step::Precommit,
|
||||
},
|
||||
block_hash: Some("1".sha3())
|
||||
@ -214,7 +234,7 @@ mod tests {
|
||||
signature: H520::default(),
|
||||
vote_step: VoteStep {
|
||||
height: 1314,
|
||||
round: 0,
|
||||
view: 0,
|
||||
step: Step::Prevote,
|
||||
},
|
||||
block_hash: None
|
||||
@ -255,7 +275,7 @@ mod tests {
|
||||
signature: Default::default(),
|
||||
vote_step: VoteStep {
|
||||
height: 0,
|
||||
round: 0,
|
||||
view: 0,
|
||||
step: Step::Propose,
|
||||
},
|
||||
block_hash: Some(header.bare_hash())
|
||||
@ -275,4 +295,11 @@ mod tests {
|
||||
|
||||
assert_eq!(pro.precommit_hash(), pre.sha3());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn step_ordering() {
|
||||
assert!(VoteStep::new(10, 123, Step::Precommit) < VoteStep::new(11, 123, Step::Precommit));
|
||||
assert!(VoteStep::new(10, 123, Step::Propose) < VoteStep::new(11, 123, Step::Precommit));
|
||||
assert!(VoteStep::new(10, 122, Step::Propose) < VoteStep::new(11, 123, Step::Propose));
|
||||
}
|
||||
}
|
||||
|
@ -15,17 +15,15 @@
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/// Tendermint BFT consensus engine with round robin proof-of-authority.
|
||||
/// At each blockchain `Height` there can be multiple `Round`s of voting.
|
||||
/// Signatures always sign `Height`, `Round`, `Step` and `BlockHash` which is a block hash without seal.
|
||||
/// At each blockchain `Height` there can be multiple `View`s of voting.
|
||||
/// Signatures always sign `Height`, `View`, `Step` and `BlockHash` which is a block hash without seal.
|
||||
/// First a block with `Seal::Proposal` is issued by the designated proposer.
|
||||
/// Next the `Round` proceeds through `Prevote` and `Precommit` `Step`s.
|
||||
/// Block is issued when there is enough `Precommit` votes collected on a particular block at the end of a `Round`.
|
||||
/// Next the `View` proceeds through `Prevote` and `Precommit` `Step`s.
|
||||
/// Block is issued when there is enough `Precommit` votes collected on a particular block at the end of a `View`.
|
||||
/// Once enough votes have been gathered the proposer issues that block in the `Commit` step.
|
||||
|
||||
mod message;
|
||||
mod transition;
|
||||
mod params;
|
||||
mod vote_collector;
|
||||
|
||||
use std::sync::Weak;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering as AtomicOrdering};
|
||||
@ -35,7 +33,7 @@ use error::{Error, BlockError};
|
||||
use header::Header;
|
||||
use builtin::Builtin;
|
||||
use env_info::EnvInfo;
|
||||
use rlp::{UntrustedRlp, View};
|
||||
use rlp::{UntrustedRlp, View as RlpView};
|
||||
use ethkey::{recover, public_to_address, Signature};
|
||||
use account_provider::AccountProvider;
|
||||
use block::*;
|
||||
@ -46,10 +44,10 @@ use state::CleanupMode;
|
||||
use io::IoService;
|
||||
use super::signer::EngineSigner;
|
||||
use super::validator_set::{ValidatorSet, new_validator_set};
|
||||
use super::transition::TransitionHandler;
|
||||
use super::vote_collector::VoteCollector;
|
||||
use self::message::*;
|
||||
use self::transition::TransitionHandler;
|
||||
use self::params::TendermintParams;
|
||||
use self::vote_collector::VoteCollector;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
|
||||
pub enum Step {
|
||||
@ -69,7 +67,7 @@ impl Step {
|
||||
}
|
||||
|
||||
pub type Height = usize;
|
||||
pub type Round = usize;
|
||||
pub type View = usize;
|
||||
pub type BlockHash = H256;
|
||||
|
||||
/// Engine using `Tendermint` consensus algorithm, suitable for EVM chain.
|
||||
@ -82,17 +80,17 @@ pub struct Tendermint {
|
||||
block_reward: U256,
|
||||
/// Blockchain height.
|
||||
height: AtomicUsize,
|
||||
/// Consensus round.
|
||||
round: AtomicUsize,
|
||||
/// Consensus view.
|
||||
view: AtomicUsize,
|
||||
/// Consensus step.
|
||||
step: RwLock<Step>,
|
||||
/// Vote accumulator.
|
||||
votes: VoteCollector,
|
||||
votes: VoteCollector<ConsensusMessage>,
|
||||
/// Used to sign messages and proposals.
|
||||
signer: EngineSigner,
|
||||
/// Message for the last PoLC.
|
||||
lock_change: RwLock<Option<ConsensusMessage>>,
|
||||
/// Last lock round.
|
||||
/// Last lock view.
|
||||
last_lock: AtomicUsize,
|
||||
/// Bare hash of the proposed block, used for seal submission.
|
||||
proposal: RwLock<Option<H256>>,
|
||||
@ -112,16 +110,16 @@ impl Tendermint {
|
||||
step_service: IoService::<Step>::start()?,
|
||||
block_reward: our_params.block_reward,
|
||||
height: AtomicUsize::new(1),
|
||||
round: AtomicUsize::new(0),
|
||||
view: AtomicUsize::new(0),
|
||||
step: RwLock::new(Step::Propose),
|
||||
votes: VoteCollector::new(),
|
||||
votes: VoteCollector::default(),
|
||||
signer: Default::default(),
|
||||
lock_change: RwLock::new(None),
|
||||
last_lock: AtomicUsize::new(0),
|
||||
proposal: RwLock::new(None),
|
||||
validators: new_validator_set(our_params.validators),
|
||||
});
|
||||
let handler = TransitionHandler::new(Arc::downgrade(&engine), our_params.timeouts);
|
||||
let handler = TransitionHandler::new(Arc::downgrade(&engine) as Weak<Engine>, Box::new(our_params.timeouts));
|
||||
engine.step_service.register_handler(Arc::new(handler))?;
|
||||
Ok(engine)
|
||||
}
|
||||
@ -152,7 +150,7 @@ impl Tendermint {
|
||||
|
||||
fn generate_message(&self, block_hash: Option<BlockHash>) -> Option<Bytes> {
|
||||
let h = self.height.load(AtomicOrdering::SeqCst);
|
||||
let r = self.round.load(AtomicOrdering::SeqCst);
|
||||
let r = self.view.load(AtomicOrdering::SeqCst);
|
||||
let s = self.step.read();
|
||||
let vote_info = message_info_rlp(&VoteStep::new(h, r, *s), block_hash);
|
||||
match self.signer.sign(vote_info.sha3()).map(Into::into) {
|
||||
@ -181,7 +179,7 @@ impl Tendermint {
|
||||
|
||||
/// Broadcast all messages since last issued block to get the peers up to speed.
|
||||
fn broadcast_old_messages(&self) {
|
||||
for m in self.votes.get_up_to(self.height.load(AtomicOrdering::SeqCst)).into_iter() {
|
||||
for m in self.votes.get_up_to(&VoteStep::new(self.height.load(AtomicOrdering::SeqCst), self.view.load(AtomicOrdering::SeqCst), Step::Precommit)).into_iter() {
|
||||
self.broadcast_message(m);
|
||||
}
|
||||
}
|
||||
@ -191,7 +189,7 @@ impl Tendermint {
|
||||
debug!(target: "poa", "Received a Commit, transitioning to height {}.", new_height);
|
||||
self.last_lock.store(0, AtomicOrdering::SeqCst);
|
||||
self.height.store(new_height, AtomicOrdering::SeqCst);
|
||||
self.round.store(0, AtomicOrdering::SeqCst);
|
||||
self.view.store(0, AtomicOrdering::SeqCst);
|
||||
*self.lock_change.write() = None;
|
||||
}
|
||||
|
||||
@ -208,7 +206,7 @@ impl Tendermint {
|
||||
},
|
||||
Step::Prevote => {
|
||||
let block_hash = match *self.lock_change.read() {
|
||||
Some(ref m) if !self.should_unlock(m.vote_step.round) => m.block_hash,
|
||||
Some(ref m) if !self.should_unlock(m.vote_step.view) => m.block_hash,
|
||||
_ => self.proposal.read().clone(),
|
||||
};
|
||||
self.generate_and_broadcast_message(block_hash);
|
||||
@ -216,9 +214,9 @@ impl Tendermint {
|
||||
Step::Precommit => {
|
||||
trace!(target: "poa", "to_step: Precommit.");
|
||||
let block_hash = match *self.lock_change.read() {
|
||||
Some(ref m) if self.is_round(m) && m.block_hash.is_some() => {
|
||||
trace!(target: "poa", "Setting last lock: {}", m.vote_step.round);
|
||||
self.last_lock.store(m.vote_step.round, AtomicOrdering::SeqCst);
|
||||
Some(ref m) if self.is_view(m) && m.block_hash.is_some() => {
|
||||
trace!(target: "poa", "Setting last lock: {}", m.vote_step.view);
|
||||
self.last_lock.store(m.vote_step.view, AtomicOrdering::SeqCst);
|
||||
m.block_hash
|
||||
},
|
||||
_ => None,
|
||||
@ -228,15 +226,17 @@ impl Tendermint {
|
||||
Step::Commit => {
|
||||
trace!(target: "poa", "to_step: Commit.");
|
||||
// Commit the block using a complete signature set.
|
||||
let round = self.round.load(AtomicOrdering::SeqCst);
|
||||
let view = self.view.load(AtomicOrdering::SeqCst);
|
||||
let height = self.height.load(AtomicOrdering::SeqCst);
|
||||
if let Some(block_hash) = *self.proposal.read() {
|
||||
// Generate seal and remove old votes.
|
||||
if self.is_signer_proposer() {
|
||||
if let Some(seal) = self.votes.seal_signatures(height, round, &block_hash) {
|
||||
let proposal_step = VoteStep::new(height, view, Step::Propose);
|
||||
let precommit_step = VoteStep::new(proposal_step.height, proposal_step.view, Step::Precommit);
|
||||
if let Some(seal) = self.votes.seal_signatures(proposal_step, precommit_step, &block_hash) {
|
||||
trace!(target: "poa", "Collected seal: {:?}", seal);
|
||||
let seal = vec![
|
||||
::rlp::encode(&round).to_vec(),
|
||||
::rlp::encode(&view).to_vec(),
|
||||
::rlp::encode(&seal.proposal).to_vec(),
|
||||
::rlp::encode(&seal.votes).to_vec()
|
||||
];
|
||||
@ -259,16 +259,16 @@ impl Tendermint {
|
||||
n > self.validators.count() * 2/3
|
||||
}
|
||||
|
||||
/// Find the designated for the given round.
|
||||
fn round_proposer(&self, height: Height, round: Round) -> Address {
|
||||
let proposer_nonce = height + round;
|
||||
/// Find the designated for the given view.
|
||||
fn view_proposer(&self, height: Height, view: View) -> Address {
|
||||
let proposer_nonce = height + view;
|
||||
trace!(target: "poa", "Proposer nonce: {}", proposer_nonce);
|
||||
self.validators.get(proposer_nonce)
|
||||
}
|
||||
|
||||
/// Check if address is a proposer for given round.
|
||||
fn is_round_proposer(&self, height: Height, round: Round, address: &Address) -> Result<(), EngineError> {
|
||||
let proposer = self.round_proposer(height, round);
|
||||
/// Check if address is a proposer for given view.
|
||||
fn is_view_proposer(&self, height: Height, view: View, address: &Address) -> Result<(), EngineError> {
|
||||
let proposer = self.view_proposer(height, view);
|
||||
if proposer == *address {
|
||||
Ok(())
|
||||
} else {
|
||||
@ -278,7 +278,7 @@ impl Tendermint {
|
||||
|
||||
/// Check if current signer is the current proposer.
|
||||
fn is_signer_proposer(&self) -> bool {
|
||||
let proposer = self.round_proposer(self.height.load(AtomicOrdering::SeqCst), self.round.load(AtomicOrdering::SeqCst));
|
||||
let proposer = self.view_proposer(self.height.load(AtomicOrdering::SeqCst), self.view.load(AtomicOrdering::SeqCst));
|
||||
self.signer.is_address(&proposer)
|
||||
}
|
||||
|
||||
@ -286,29 +286,29 @@ impl Tendermint {
|
||||
message.vote_step.is_height(self.height.load(AtomicOrdering::SeqCst))
|
||||
}
|
||||
|
||||
fn is_round(&self, message: &ConsensusMessage) -> bool {
|
||||
message.vote_step.is_round(self.height.load(AtomicOrdering::SeqCst), self.round.load(AtomicOrdering::SeqCst))
|
||||
fn is_view(&self, message: &ConsensusMessage) -> bool {
|
||||
message.vote_step.is_view(self.height.load(AtomicOrdering::SeqCst), self.view.load(AtomicOrdering::SeqCst))
|
||||
}
|
||||
|
||||
fn increment_round(&self, n: Round) {
|
||||
trace!(target: "poa", "increment_round: New round.");
|
||||
self.round.fetch_add(n, AtomicOrdering::SeqCst);
|
||||
fn increment_view(&self, n: View) {
|
||||
trace!(target: "poa", "increment_view: New view.");
|
||||
self.view.fetch_add(n, AtomicOrdering::SeqCst);
|
||||
}
|
||||
|
||||
fn should_unlock(&self, lock_change_round: Round) -> bool {
|
||||
self.last_lock.load(AtomicOrdering::SeqCst) < lock_change_round
|
||||
&& lock_change_round < self.round.load(AtomicOrdering::SeqCst)
|
||||
fn should_unlock(&self, lock_change_view: View) -> bool {
|
||||
self.last_lock.load(AtomicOrdering::SeqCst) < lock_change_view
|
||||
&& lock_change_view < self.view.load(AtomicOrdering::SeqCst)
|
||||
}
|
||||
|
||||
|
||||
fn has_enough_any_votes(&self) -> bool {
|
||||
let step_votes = self.votes.count_step_votes(&VoteStep::new(self.height.load(AtomicOrdering::SeqCst), self.round.load(AtomicOrdering::SeqCst), *self.step.read()));
|
||||
let step_votes = self.votes.count_round_votes(&VoteStep::new(self.height.load(AtomicOrdering::SeqCst), self.view.load(AtomicOrdering::SeqCst), *self.step.read()));
|
||||
self.is_above_threshold(step_votes)
|
||||
}
|
||||
|
||||
fn has_enough_future_step_votes(&self, vote_step: &VoteStep) -> bool {
|
||||
if vote_step.round > self.round.load(AtomicOrdering::SeqCst) {
|
||||
let step_votes = self.votes.count_step_votes(vote_step);
|
||||
if vote_step.view > self.view.load(AtomicOrdering::SeqCst) {
|
||||
let step_votes = self.votes.count_round_votes(vote_step);
|
||||
self.is_above_threshold(step_votes)
|
||||
} else {
|
||||
false
|
||||
@ -339,21 +339,21 @@ impl Tendermint {
|
||||
let next_step = match *self.step.read() {
|
||||
Step::Precommit if self.has_enough_aligned_votes(message) => {
|
||||
if message.block_hash.is_none() {
|
||||
self.increment_round(1);
|
||||
self.increment_view(1);
|
||||
Some(Step::Propose)
|
||||
} else {
|
||||
Some(Step::Commit)
|
||||
}
|
||||
},
|
||||
Step::Precommit if self.has_enough_future_step_votes(&vote_step) => {
|
||||
self.increment_round(vote_step.round - self.round.load(AtomicOrdering::SeqCst));
|
||||
self.increment_view(vote_step.view - self.view.load(AtomicOrdering::SeqCst));
|
||||
Some(Step::Precommit)
|
||||
},
|
||||
// Avoid counting twice.
|
||||
Step::Prevote if lock_change => Some(Step::Precommit),
|
||||
Step::Prevote if self.has_enough_aligned_votes(message) => Some(Step::Precommit),
|
||||
Step::Prevote if self.has_enough_future_step_votes(&vote_step) => {
|
||||
self.increment_round(vote_step.round - self.round.load(AtomicOrdering::SeqCst));
|
||||
self.increment_view(vote_step.view - self.view.load(AtomicOrdering::SeqCst));
|
||||
Some(Step::Prevote)
|
||||
},
|
||||
_ => None,
|
||||
@ -370,7 +370,7 @@ impl Tendermint {
|
||||
impl Engine for Tendermint {
|
||||
fn name(&self) -> &str { "Tendermint" }
|
||||
fn version(&self) -> SemanticVersion { SemanticVersion::new(1, 0, 0) }
|
||||
/// (consensus round, proposal signature, authority signatures)
|
||||
/// (consensus view, proposal signature, authority signatures)
|
||||
fn seal_fields(&self) -> usize { 3 }
|
||||
|
||||
fn params(&self) -> &CommonParams { &self.params }
|
||||
@ -385,7 +385,7 @@ impl Engine for Tendermint {
|
||||
map![
|
||||
"signature".into() => message.signature.to_string(),
|
||||
"height".into() => message.vote_step.height.to_string(),
|
||||
"round".into() => message.vote_step.round.to_string(),
|
||||
"view".into() => message.vote_step.view.to_string(),
|
||||
"block_hash".into() => message.block_hash.as_ref().map(ToString::to_string).unwrap_or("".into())
|
||||
]
|
||||
}
|
||||
@ -395,8 +395,8 @@ impl Engine for Tendermint {
|
||||
}
|
||||
|
||||
fn populate_from_parent(&self, header: &mut Header, parent: &Header, gas_floor_target: U256, _gas_ceil_target: U256) {
|
||||
// Chain scoring: total weight is sqrt(U256::max_value())*height - round
|
||||
let new_difficulty = U256::from(U128::max_value()) + consensus_round(parent).expect("Header has been verified; qed").into() - self.round.load(AtomicOrdering::SeqCst).into();
|
||||
// Chain scoring: total weight is sqrt(U256::max_value())*height - view
|
||||
let new_difficulty = U256::from(U128::max_value()) + consensus_view(parent).expect("Header has been verified; qed").into() - self.view.load(AtomicOrdering::SeqCst).into();
|
||||
header.set_difficulty(new_difficulty);
|
||||
header.set_gas_limit({
|
||||
let gas_limit = parent.gas_limit().clone();
|
||||
@ -424,17 +424,17 @@ impl Engine for Tendermint {
|
||||
}
|
||||
|
||||
let height = header.number() as Height;
|
||||
let round = self.round.load(AtomicOrdering::SeqCst);
|
||||
let view = self.view.load(AtomicOrdering::SeqCst);
|
||||
let bh = Some(header.bare_hash());
|
||||
let vote_info = message_info_rlp(&VoteStep::new(height, round, Step::Propose), bh.clone());
|
||||
let vote_info = message_info_rlp(&VoteStep::new(height, view, Step::Propose), bh.clone());
|
||||
if let Ok(signature) = self.signer.sign(vote_info.sha3()).map(Into::into) {
|
||||
// Insert Propose vote.
|
||||
debug!(target: "poa", "Submitting proposal {} at height {} round {}.", header.bare_hash(), height, round);
|
||||
self.votes.vote(ConsensusMessage::new(signature, height, round, Step::Propose, bh), author);
|
||||
debug!(target: "poa", "Submitting proposal {} at height {} view {}.", header.bare_hash(), height, view);
|
||||
self.votes.vote(ConsensusMessage::new(signature, height, view, Step::Propose, bh), author);
|
||||
// Remember proposal for later seal submission.
|
||||
*self.proposal.write() = bh;
|
||||
Seal::Proposal(vec![
|
||||
::rlp::encode(&round).to_vec(),
|
||||
::rlp::encode(&view).to_vec(),
|
||||
::rlp::encode(&signature).to_vec(),
|
||||
::rlp::EMPTY_LIST_RLP.to_vec()
|
||||
])
|
||||
@ -535,7 +535,7 @@ impl Engine for Tendermint {
|
||||
found: signatures_len
|
||||
}))?;
|
||||
}
|
||||
self.is_round_proposer(proposal.vote_step.height, proposal.vote_step.round, &proposer)?;
|
||||
self.is_view_proposer(proposal.vote_step.height, proposal.vote_step.view, &proposer)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@ -583,7 +583,7 @@ impl Engine for Tendermint {
|
||||
}
|
||||
let proposer = proposal.verify().expect("block went through full verification; this Engine tries verify; qed");
|
||||
debug!(target: "poa", "Received a new proposal {:?} from {}.", proposal.vote_step, proposer);
|
||||
if self.is_round(&proposal) {
|
||||
if self.is_view(&proposal) {
|
||||
*self.proposal.write() = proposal.block_hash.clone();
|
||||
}
|
||||
self.votes.vote(proposal, &proposer);
|
||||
@ -597,7 +597,7 @@ impl Engine for Tendermint {
|
||||
trace!(target: "poa", "Propose timeout.");
|
||||
if self.proposal.read().is_none() {
|
||||
// Report the proposer if no proposal was received.
|
||||
let current_proposer = self.round_proposer(self.height.load(AtomicOrdering::SeqCst), self.round.load(AtomicOrdering::SeqCst));
|
||||
let current_proposer = self.view_proposer(self.height.load(AtomicOrdering::SeqCst), self.view.load(AtomicOrdering::SeqCst));
|
||||
self.validators.report_benign(¤t_proposer);
|
||||
}
|
||||
Step::Prevote
|
||||
@ -613,7 +613,7 @@ impl Engine for Tendermint {
|
||||
},
|
||||
Step::Precommit if self.has_enough_any_votes() => {
|
||||
trace!(target: "poa", "Precommit timeout.");
|
||||
self.increment_round(1);
|
||||
self.increment_view(1);
|
||||
Step::Propose
|
||||
},
|
||||
Step::Precommit => {
|
||||
@ -673,19 +673,19 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn vote<F>(engine: &Engine, signer: F, height: usize, round: usize, step: Step, block_hash: Option<H256>) -> Bytes where F: FnOnce(H256) -> Result<H520, ::account_provider::Error> {
|
||||
let mi = message_info_rlp(&VoteStep::new(height, round, step), block_hash);
|
||||
fn vote<F>(engine: &Engine, signer: F, height: usize, view: usize, step: Step, block_hash: Option<H256>) -> Bytes where F: FnOnce(H256) -> Result<H520, ::account_provider::Error> {
|
||||
let mi = message_info_rlp(&VoteStep::new(height, view, step), block_hash);
|
||||
let m = message_full_rlp(&signer(mi.sha3()).unwrap().into(), &mi);
|
||||
engine.handle_message(&m).unwrap();
|
||||
m
|
||||
}
|
||||
|
||||
fn proposal_seal(tap: &Arc<AccountProvider>, header: &Header, round: Round) -> Vec<Bytes> {
|
||||
fn proposal_seal(tap: &Arc<AccountProvider>, header: &Header, view: View) -> Vec<Bytes> {
|
||||
let author = header.author();
|
||||
let vote_info = message_info_rlp(&VoteStep::new(header.number() as Height, round, Step::Propose), Some(header.bare_hash()));
|
||||
let vote_info = message_info_rlp(&VoteStep::new(header.number() as Height, view, Step::Propose), Some(header.bare_hash()));
|
||||
let signature = tap.sign(*author, None, vote_info.sha3()).unwrap();
|
||||
vec![
|
||||
::rlp::encode(&round).to_vec(),
|
||||
::rlp::encode(&view).to_vec(),
|
||||
::rlp::encode(&H520::from(signature)).to_vec(),
|
||||
::rlp::EMPTY_LIST_RLP.to_vec()
|
||||
]
|
||||
|
@ -17,9 +17,10 @@
|
||||
//! Tendermint specific parameters.
|
||||
|
||||
use ethjson;
|
||||
use super::transition::TendermintTimeouts;
|
||||
use util::{U256, Uint};
|
||||
use time::Duration;
|
||||
use super::super::transition::Timeouts;
|
||||
use super::Step;
|
||||
|
||||
/// `Tendermint` params.
|
||||
#[derive(Debug)]
|
||||
@ -34,6 +35,41 @@ pub struct TendermintParams {
|
||||
pub block_reward: U256,
|
||||
}
|
||||
|
||||
/// Base timeout of each step in ms.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TendermintTimeouts {
|
||||
pub propose: Duration,
|
||||
pub prevote: Duration,
|
||||
pub precommit: Duration,
|
||||
pub commit: Duration,
|
||||
}
|
||||
|
||||
impl Default for TendermintTimeouts {
|
||||
fn default() -> Self {
|
||||
TendermintTimeouts {
|
||||
propose: Duration::milliseconds(1000),
|
||||
prevote: Duration::milliseconds(1000),
|
||||
precommit: Duration::milliseconds(1000),
|
||||
commit: Duration::milliseconds(1000),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Timeouts<Step> for TendermintTimeouts {
|
||||
fn initial(&self) -> Duration {
|
||||
self.propose
|
||||
}
|
||||
|
||||
fn timeout(&self, step: &Step) -> Duration {
|
||||
match *step {
|
||||
Step::Propose => self.propose,
|
||||
Step::Prevote => self.prevote,
|
||||
Step::Precommit => self.precommit,
|
||||
Step::Commit => self.commit,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn to_duration(ms: ethjson::uint::Uint) -> Duration {
|
||||
let ms: usize = ms.into();
|
||||
Duration::milliseconds(ms as i64)
|
||||
|
@ -1,102 +0,0 @@
|
||||
// Copyright 2015, 2016 Ethcore (UK) Ltd.
|
||||
// This file is part of Parity.
|
||||
|
||||
// Parity 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.
|
||||
|
||||
// Parity 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 Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Tendermint timeout handling.
|
||||
|
||||
use std::sync::Weak;
|
||||
use time::Duration;
|
||||
use io::{IoContext, IoHandler, TimerToken};
|
||||
use super::{Tendermint, Step};
|
||||
use engines::Engine;
|
||||
|
||||
pub struct TransitionHandler {
|
||||
engine: Weak<Tendermint>,
|
||||
timeouts: TendermintTimeouts,
|
||||
}
|
||||
|
||||
impl TransitionHandler {
|
||||
pub fn new(engine: Weak<Tendermint>, timeouts: TendermintTimeouts) -> Self {
|
||||
TransitionHandler {
|
||||
engine: engine,
|
||||
timeouts: timeouts,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Base timeout of each step in ms.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TendermintTimeouts {
|
||||
pub propose: Duration,
|
||||
pub prevote: Duration,
|
||||
pub precommit: Duration,
|
||||
pub commit: Duration,
|
||||
}
|
||||
|
||||
impl TendermintTimeouts {
|
||||
pub fn for_step(&self, step: Step) -> Duration {
|
||||
match step {
|
||||
Step::Propose => self.propose,
|
||||
Step::Prevote => self.prevote,
|
||||
Step::Precommit => self.precommit,
|
||||
Step::Commit => self.commit,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for TendermintTimeouts {
|
||||
fn default() -> Self {
|
||||
TendermintTimeouts {
|
||||
propose: Duration::milliseconds(1000),
|
||||
prevote: Duration::milliseconds(1000),
|
||||
precommit: Duration::milliseconds(1000),
|
||||
commit: Duration::milliseconds(1000),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Timer token representing the consensus step timeouts.
|
||||
pub const ENGINE_TIMEOUT_TOKEN: TimerToken = 23;
|
||||
|
||||
fn set_timeout(io: &IoContext<Step>, timeout: Duration) {
|
||||
io.register_timer_once(ENGINE_TIMEOUT_TOKEN, timeout.num_milliseconds() as u64)
|
||||
.unwrap_or_else(|e| warn!(target: "poa", "Failed to set consensus step timeout: {}.", e))
|
||||
}
|
||||
|
||||
impl IoHandler<Step> for TransitionHandler {
|
||||
fn initialize(&self, io: &IoContext<Step>) {
|
||||
set_timeout(io, self.timeouts.propose)
|
||||
}
|
||||
|
||||
fn timeout(&self, _io: &IoContext<Step>, timer: TimerToken) {
|
||||
if timer == ENGINE_TIMEOUT_TOKEN {
|
||||
if let Some(engine) = self.engine.upgrade() {
|
||||
engine.step();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn message(&self, io: &IoContext<Step>, next_step: &Step) {
|
||||
if let Err(io_err) = io.clear_timer(ENGINE_TIMEOUT_TOKEN) {
|
||||
warn!(target: "poa", "Could not remove consensus timer {}.", io_err)
|
||||
}
|
||||
match *next_step {
|
||||
Step::Propose => set_timeout(io, self.timeouts.propose),
|
||||
Step::Prevote => set_timeout(io, self.timeouts.prevote),
|
||||
Step::Precommit => set_timeout(io, self.timeouts.precommit),
|
||||
Step::Commit => set_timeout(io, self.timeouts.commit),
|
||||
};
|
||||
}
|
||||
}
|
@ -1,307 +0,0 @@
|
||||
// Copyright 2015, 2016 Ethcore (UK) Ltd.
|
||||
// This file is part of Parity.
|
||||
|
||||
// Parity 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.
|
||||
|
||||
// Parity 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 Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Collects votes on hashes at each height and round.
|
||||
|
||||
use util::*;
|
||||
use super::message::*;
|
||||
use super::{Height, Round, Step, BlockHash};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct VoteCollector {
|
||||
/// Storing all Proposals, Prevotes and Precommits.
|
||||
votes: RwLock<BTreeMap<VoteStep, StepCollector>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct StepCollector {
|
||||
voted: HashSet<Address>,
|
||||
pub block_votes: HashMap<Option<BlockHash>, HashMap<H520, Address>>,
|
||||
messages: HashSet<ConsensusMessage>,
|
||||
}
|
||||
|
||||
impl StepCollector {
|
||||
/// Returns Some(&Address) when validator is double voting.
|
||||
fn insert<'a>(&mut self, message: ConsensusMessage, address: &'a Address) -> Option<&'a Address> {
|
||||
// Do nothing when message was seen.
|
||||
if self.messages.insert(message.clone()) {
|
||||
if self.voted.insert(address.clone()) {
|
||||
self
|
||||
.block_votes
|
||||
.entry(message.block_hash)
|
||||
.or_insert_with(HashMap::new)
|
||||
.insert(message.signature, address.clone());
|
||||
} else {
|
||||
// Bad validator sent a different message.
|
||||
return Some(address);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Count all votes for the given block hash at this step.
|
||||
fn count_block(&self, block_hash: &Option<BlockHash>) -> usize {
|
||||
self.block_votes.get(block_hash).map_or(0, HashMap::len)
|
||||
}
|
||||
|
||||
/// Count all votes collected for the given step.
|
||||
fn count(&self) -> usize {
|
||||
self.block_votes.values().map(HashMap::len).sum()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SealSignatures {
|
||||
pub proposal: H520,
|
||||
pub votes: Vec<H520>,
|
||||
}
|
||||
|
||||
impl PartialEq for SealSignatures {
|
||||
fn eq(&self, other: &SealSignatures) -> bool {
|
||||
self.proposal == other.proposal
|
||||
&& self.votes.iter().collect::<HashSet<_>>() == other.votes.iter().collect::<HashSet<_>>()
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for SealSignatures {}
|
||||
|
||||
impl VoteCollector {
|
||||
pub fn new() -> Self {
|
||||
let mut collector = BTreeMap::new();
|
||||
// Insert dummy entry to fulfill invariant: "only messages newer than the oldest are inserted".
|
||||
collector.insert(VoteStep::new(0, 0, Step::Propose), Default::default());
|
||||
VoteCollector { votes: RwLock::new(collector) }
|
||||
}
|
||||
|
||||
/// Insert vote if it is newer than the oldest one.
|
||||
pub fn vote<'a>(&self, message: ConsensusMessage, voter: &'a Address) -> Option<&'a Address> {
|
||||
self
|
||||
.votes
|
||||
.write()
|
||||
.entry(message.vote_step.clone())
|
||||
.or_insert_with(Default::default)
|
||||
.insert(message, voter)
|
||||
}
|
||||
|
||||
/// Checks if the message should be ignored.
|
||||
pub fn is_old_or_known(&self, message: &ConsensusMessage) -> bool {
|
||||
self
|
||||
.votes
|
||||
.read()
|
||||
.get(&message.vote_step)
|
||||
.map_or(false, |c| {
|
||||
let is_known = c.messages.contains(message);
|
||||
if is_known { trace!(target: "poa", "Known message: {:?}.", message); }
|
||||
is_known
|
||||
})
|
||||
|| {
|
||||
let guard = self.votes.read();
|
||||
let is_old = guard.keys().next().map_or(true, |oldest| message.vote_step <= *oldest);
|
||||
if is_old { trace!(target: "poa", "Old message {:?}.", message); }
|
||||
is_old
|
||||
}
|
||||
}
|
||||
|
||||
/// Throws out messages older than message, leaves message as marker for the oldest.
|
||||
pub fn throw_out_old(&self, vote_step: &VoteStep) {
|
||||
let mut guard = self.votes.write();
|
||||
let new_collector = guard.split_off(vote_step);
|
||||
*guard = new_collector;
|
||||
}
|
||||
|
||||
/// Collects the signatures used to seal a block.
|
||||
pub fn seal_signatures(&self, height: Height, round: Round, block_hash: &H256) -> Option<SealSignatures> {
|
||||
let ref bh = Some(*block_hash);
|
||||
let precommit_step = VoteStep::new(height, round, Step::Precommit);
|
||||
let maybe_seal = {
|
||||
let guard = self.votes.read();
|
||||
guard
|
||||
.get(&VoteStep::new(height, round, Step::Propose))
|
||||
.and_then(|c| c.block_votes.get(bh))
|
||||
.and_then(|proposals| proposals.keys().next())
|
||||
.map(|proposal| SealSignatures {
|
||||
proposal: proposal.clone(),
|
||||
votes: guard
|
||||
.get(&precommit_step)
|
||||
.and_then(|c| c.block_votes.get(bh))
|
||||
.map(|precommits| precommits.keys().cloned().collect())
|
||||
.unwrap_or_else(Vec::new),
|
||||
})
|
||||
.and_then(|seal| if seal.votes.is_empty() { None } else { Some(seal) })
|
||||
};
|
||||
if maybe_seal.is_some() {
|
||||
// Remove messages that are no longer relevant.
|
||||
self.throw_out_old(&precommit_step);
|
||||
}
|
||||
maybe_seal
|
||||
}
|
||||
|
||||
/// Count votes which agree with the given message.
|
||||
pub fn count_aligned_votes(&self, message: &ConsensusMessage) -> usize {
|
||||
self
|
||||
.votes
|
||||
.read()
|
||||
.get(&message.vote_step)
|
||||
.map_or(0, |m| m.count_block(&message.block_hash))
|
||||
}
|
||||
|
||||
/// Count all votes collected for a given step.
|
||||
pub fn count_step_votes(&self, vote_step: &VoteStep) -> usize {
|
||||
self.votes.read().get(vote_step).map_or(0, StepCollector::count)
|
||||
}
|
||||
|
||||
/// Get all messages older than the height.
|
||||
pub fn get_up_to(&self, height: Height) -> Vec<Bytes> {
|
||||
let guard = self.votes.read();
|
||||
guard
|
||||
.iter()
|
||||
.filter(|&(s, _)| s.step.is_pre())
|
||||
.take_while(|&(s, _)| s.height <= height)
|
||||
.map(|(_, c)| c.messages.iter().map(|m| ::rlp::encode(m).to_vec()).collect::<Vec<_>>())
|
||||
.fold(Vec::new(), |mut acc, mut messages| { acc.append(&mut messages); acc })
|
||||
}
|
||||
|
||||
/// Retrieve address from which the message was sent from cache.
|
||||
pub fn get(&self, message: &ConsensusMessage) -> Option<Address> {
|
||||
let guard = self.votes.read();
|
||||
guard.get(&message.vote_step).and_then(|c| c.block_votes.get(&message.block_hash)).and_then(|origins| origins.get(&message.signature).cloned())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use util::*;
|
||||
use super::*;
|
||||
use super::super::{BlockHash, Step};
|
||||
use super::super::message::*;
|
||||
|
||||
fn random_vote(collector: &VoteCollector, signature: H520, vote_step: VoteStep, block_hash: Option<BlockHash>) -> bool {
|
||||
full_vote(collector, signature, vote_step, block_hash, &H160::random()).is_none()
|
||||
}
|
||||
|
||||
fn full_vote<'a>(collector: &VoteCollector, signature: H520, vote_step: VoteStep, block_hash: Option<BlockHash>, address: &'a Address) -> Option<&'a Address> {
|
||||
collector.vote(ConsensusMessage { signature: signature, vote_step: vote_step, block_hash: block_hash }, address)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seal_retrieval() {
|
||||
let collector = VoteCollector::new();
|
||||
let bh = Some("1".sha3());
|
||||
let h = 1;
|
||||
let r = 2;
|
||||
let mut signatures = Vec::new();
|
||||
for _ in 0..5 {
|
||||
signatures.push(H520::random());
|
||||
}
|
||||
let propose_step = VoteStep::new(h, r, Step::Propose);
|
||||
let prevote_step = VoteStep::new(h, r, Step::Prevote);
|
||||
let precommit_step = VoteStep::new(h, r, Step::Precommit);
|
||||
// Wrong height proposal.
|
||||
random_vote(&collector, signatures[4].clone(), VoteStep::new(h - 1, r, Step::Propose), bh.clone());
|
||||
// Good proposal
|
||||
random_vote(&collector, signatures[0].clone(), propose_step.clone(), bh.clone());
|
||||
// Wrong block proposal.
|
||||
random_vote(&collector, signatures[0].clone(), propose_step.clone(), Some("0".sha3()));
|
||||
// Wrong block precommit.
|
||||
random_vote(&collector, signatures[3].clone(), precommit_step.clone(), Some("0".sha3()));
|
||||
// Wrong round proposal.
|
||||
random_vote(&collector, signatures[0].clone(), VoteStep::new(h, r - 1, Step::Propose), bh.clone());
|
||||
// Prevote.
|
||||
random_vote(&collector, signatures[0].clone(), prevote_step.clone(), bh.clone());
|
||||
// Relevant precommit.
|
||||
random_vote(&collector, signatures[2].clone(), precommit_step.clone(), bh.clone());
|
||||
// Replcated vote.
|
||||
random_vote(&collector, signatures[2].clone(), precommit_step.clone(), bh.clone());
|
||||
// Wrong round precommit.
|
||||
random_vote(&collector, signatures[4].clone(), VoteStep::new(h, r + 1, Step::Precommit), bh.clone());
|
||||
// Wrong height precommit.
|
||||
random_vote(&collector, signatures[3].clone(), VoteStep::new(h + 1, r, Step::Precommit), bh.clone());
|
||||
// Relevant precommit.
|
||||
random_vote(&collector, signatures[1].clone(), precommit_step.clone(), bh.clone());
|
||||
// Wrong round precommit, same signature.
|
||||
random_vote(&collector, signatures[1].clone(), VoteStep::new(h, r + 1, Step::Precommit), bh.clone());
|
||||
// Wrong round precommit.
|
||||
random_vote(&collector, signatures[4].clone(), VoteStep::new(h, r - 1, Step::Precommit), bh.clone());
|
||||
let seal = SealSignatures {
|
||||
proposal: signatures[0],
|
||||
votes: signatures[1..3].to_vec()
|
||||
};
|
||||
assert_eq!(seal, collector.seal_signatures(h, r, &bh.unwrap()).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn count_votes() {
|
||||
let collector = VoteCollector::new();
|
||||
let prevote_step = VoteStep::new(3, 2, Step::Prevote);
|
||||
let precommit_step = VoteStep::new(3, 2, Step::Precommit);
|
||||
// good prevote
|
||||
random_vote(&collector, H520::random(), prevote_step.clone(), Some("0".sha3()));
|
||||
random_vote(&collector, H520::random(), VoteStep::new(3, 1, Step::Prevote), Some("0".sha3()));
|
||||
// good precommit
|
||||
random_vote(&collector, H520::random(), precommit_step.clone(), Some("0".sha3()));
|
||||
random_vote(&collector, H520::random(), VoteStep::new(3, 3, Step::Precommit), Some("0".sha3()));
|
||||
// good prevote
|
||||
random_vote(&collector, H520::random(), prevote_step.clone(), Some("1".sha3()));
|
||||
// good prevote
|
||||
let same_sig = H520::random();
|
||||
random_vote(&collector, same_sig.clone(), prevote_step.clone(), Some("1".sha3()));
|
||||
random_vote(&collector, same_sig, prevote_step.clone(), Some("1".sha3()));
|
||||
// good precommit
|
||||
random_vote(&collector, H520::random(), precommit_step.clone(), Some("1".sha3()));
|
||||
// good prevote
|
||||
random_vote(&collector, H520::random(), prevote_step.clone(), Some("0".sha3()));
|
||||
random_vote(&collector, H520::random(), VoteStep::new(2, 2, Step::Precommit), Some("2".sha3()));
|
||||
|
||||
assert_eq!(collector.count_step_votes(&prevote_step), 4);
|
||||
assert_eq!(collector.count_step_votes(&precommit_step), 2);
|
||||
|
||||
let message = ConsensusMessage {
|
||||
signature: H520::default(),
|
||||
vote_step: prevote_step,
|
||||
block_hash: Some("1".sha3())
|
||||
};
|
||||
assert_eq!(collector.count_aligned_votes(&message), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_old() {
|
||||
let collector = VoteCollector::new();
|
||||
let vote = |height, round, step, hash| {
|
||||
random_vote(&collector, H520::random(), VoteStep::new(height, round, step), hash);
|
||||
};
|
||||
vote(3, 2, Step::Prevote, Some("0".sha3()));
|
||||
vote(3, 1, Step::Prevote, Some("0".sha3()));
|
||||
vote(3, 3, Step::Precommit, Some("0".sha3()));
|
||||
vote(3, 2, Step::Prevote, Some("1".sha3()));
|
||||
vote(3, 2, Step::Prevote, Some("1".sha3()));
|
||||
vote(3, 2, Step::Prevote, Some("0".sha3()));
|
||||
vote(2, 2, Step::Precommit, Some("2".sha3()));
|
||||
|
||||
collector.throw_out_old(&VoteStep::new(3, 2, Step::Precommit));
|
||||
assert_eq!(collector.votes.read().len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malicious_authority() {
|
||||
let collector = VoteCollector::new();
|
||||
let vote_step = VoteStep::new(3, 2, Step::Prevote);
|
||||
// Vote is inserted fine.
|
||||
assert!(full_vote(&collector, H520::random(), vote_step.clone(), Some("0".sha3()), &Address::default()).is_none());
|
||||
// Returns the double voting address.
|
||||
full_vote(&collector, H520::random(), vote_step.clone(), Some("1".sha3()), &Address::default()).unwrap();
|
||||
assert_eq!(collector.count_step_votes(&vote_step), 1);
|
||||
}
|
||||
}
|
78
ethcore/src/engines/transition.rs
Normal file
78
ethcore/src/engines/transition.rs
Normal file
@ -0,0 +1,78 @@
|
||||
// Copyright 2015, 2016 Ethcore (UK) Ltd.
|
||||
// This file is part of Parity.
|
||||
|
||||
// Parity 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.
|
||||
|
||||
// Parity 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 Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Engine timeout transitioning calls `Engine.step()` on timeout.
|
||||
|
||||
use std::sync::Weak;
|
||||
use time::Duration;
|
||||
use io::{IoContext, IoHandler, TimerToken};
|
||||
use engines::Engine;
|
||||
|
||||
/// Timeouts lookup
|
||||
pub trait Timeouts<S: Sync + Send + Clone>: Send + Sync {
|
||||
/// Return the first timeout.
|
||||
fn initial(&self) -> Duration;
|
||||
|
||||
/// Get a timeout based on step.
|
||||
fn timeout(&self, step: &S) -> Duration;
|
||||
}
|
||||
|
||||
/// Timeout transition handling.
|
||||
pub struct TransitionHandler<S: Sync + Send + Clone> {
|
||||
engine: Weak<Engine>,
|
||||
timeouts: Box<Timeouts<S>>,
|
||||
}
|
||||
|
||||
impl <S> TransitionHandler<S> where S: Sync + Send + Clone {
|
||||
/// New step caller by timeouts.
|
||||
pub fn new(engine: Weak<Engine>, timeouts: Box<Timeouts<S>>) -> Self {
|
||||
TransitionHandler {
|
||||
engine: engine,
|
||||
timeouts: timeouts,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Timer token representing the consensus step timeouts.
|
||||
pub const ENGINE_TIMEOUT_TOKEN: TimerToken = 23;
|
||||
|
||||
fn set_timeout<S: Sync + Send + Clone>(io: &IoContext<S>, timeout: Duration) {
|
||||
io.register_timer_once(ENGINE_TIMEOUT_TOKEN, timeout.num_milliseconds() as u64)
|
||||
.unwrap_or_else(|e| warn!(target: "engine", "Failed to set consensus step timeout: {}.", e))
|
||||
}
|
||||
|
||||
impl <S> IoHandler<S> for TransitionHandler<S> where S: Sync + Send + Clone + 'static {
|
||||
fn initialize(&self, io: &IoContext<S>) {
|
||||
set_timeout(io, self.timeouts.initial());
|
||||
}
|
||||
|
||||
/// Call step after timeout.
|
||||
fn timeout(&self, _io: &IoContext<S>, timer: TimerToken) {
|
||||
if timer == ENGINE_TIMEOUT_TOKEN {
|
||||
if let Some(engine) = self.engine.upgrade() {
|
||||
engine.step();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Set a new timer on message.
|
||||
fn message(&self, io: &IoContext<S>, next: &S) {
|
||||
if let Err(io_err) = io.clear_timer(ENGINE_TIMEOUT_TOKEN) {
|
||||
warn!(target: "engine", "Could not remove consensus timer {}.", io_err)
|
||||
}
|
||||
set_timeout(io, self.timeouts.timeout(next));
|
||||
}
|
||||
}
|
345
ethcore/src/engines/vote_collector.rs
Normal file
345
ethcore/src/engines/vote_collector.rs
Normal file
@ -0,0 +1,345 @@
|
||||
// Copyright 2015, 2016 Ethcore (UK) Ltd.
|
||||
// This file is part of Parity.
|
||||
|
||||
// Parity 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.
|
||||
|
||||
// Parity 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 Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Collects votes on hashes at each Message::Round.
|
||||
|
||||
use std::fmt::Debug;
|
||||
use util::*;
|
||||
use rlp::Encodable;
|
||||
|
||||
pub trait Message: Clone + PartialEq + Eq + Hash + Encodable + Debug {
|
||||
type Round: Clone + PartialEq + Eq + Hash + Default + Debug + Ord;
|
||||
|
||||
fn signature(&self) -> H520;
|
||||
|
||||
fn block_hash(&self) -> Option<H256>;
|
||||
|
||||
fn round(&self) -> &Self::Round;
|
||||
|
||||
fn is_broadcastable(&self) -> bool;
|
||||
}
|
||||
|
||||
/// Storing all Proposals, Prevotes and Precommits.
|
||||
#[derive(Debug)]
|
||||
pub struct VoteCollector<M: Message> {
|
||||
votes: RwLock<BTreeMap<M::Round, StepCollector<M>>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct StepCollector<M: Message> {
|
||||
voted: HashSet<Address>,
|
||||
pub block_votes: HashMap<Option<H256>, HashMap<H520, Address>>,
|
||||
messages: HashSet<M>,
|
||||
}
|
||||
|
||||
impl <M: Message> StepCollector<M> {
|
||||
/// Returns Some(&Address) when validator is double voting.
|
||||
fn insert<'a>(&mut self, message: M, address: &'a Address) -> Option<&'a Address> {
|
||||
// Do nothing when message was seen.
|
||||
if self.messages.insert(message.clone()) {
|
||||
if self.voted.insert(address.clone()) {
|
||||
self
|
||||
.block_votes
|
||||
.entry(message.block_hash())
|
||||
.or_insert_with(HashMap::new)
|
||||
.insert(message.signature(), address.clone());
|
||||
} else {
|
||||
// Bad validator sent a different message.
|
||||
return Some(address);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Count all votes for the given block hash at this round.
|
||||
fn count_block(&self, block_hash: &Option<H256>) -> usize {
|
||||
self.block_votes.get(block_hash).map_or(0, HashMap::len)
|
||||
}
|
||||
|
||||
/// Count all votes collected for the given round.
|
||||
fn count(&self) -> usize {
|
||||
self.block_votes.values().map(HashMap::len).sum()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SealSignatures {
|
||||
pub proposal: H520,
|
||||
pub votes: Vec<H520>,
|
||||
}
|
||||
|
||||
impl PartialEq for SealSignatures {
|
||||
fn eq(&self, other: &SealSignatures) -> bool {
|
||||
self.proposal == other.proposal
|
||||
&& self.votes.iter().collect::<HashSet<_>>() == other.votes.iter().collect::<HashSet<_>>()
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for SealSignatures {}
|
||||
|
||||
impl <M: Message + Default> Default for VoteCollector<M> {
|
||||
fn default() -> Self {
|
||||
let mut collector = BTreeMap::new();
|
||||
// Insert dummy entry to fulfill invariant: "only messages newer than the oldest are inserted".
|
||||
collector.insert(Default::default(), Default::default());
|
||||
VoteCollector { votes: RwLock::new(collector) }
|
||||
}
|
||||
}
|
||||
|
||||
impl <M: Message + Default + Encodable + Debug> VoteCollector<M> {
|
||||
/// Insert vote if it is newer than the oldest one.
|
||||
pub fn vote<'a>(&self, message: M, voter: &'a Address) -> Option<&'a Address> {
|
||||
self
|
||||
.votes
|
||||
.write()
|
||||
.entry(message.round().clone())
|
||||
.or_insert_with(Default::default)
|
||||
.insert(message, voter)
|
||||
}
|
||||
|
||||
/// Checks if the message should be ignored.
|
||||
pub fn is_old_or_known(&self, message: &M) -> bool {
|
||||
self
|
||||
.votes
|
||||
.read()
|
||||
.get(&message.round())
|
||||
.map_or(false, |c| {
|
||||
let is_known = c.messages.contains(message);
|
||||
if is_known { trace!(target: "poa", "Known message: {:?}.", message); }
|
||||
is_known
|
||||
})
|
||||
|| {
|
||||
let guard = self.votes.read();
|
||||
let is_old = guard.keys().next().map_or(true, |oldest| message.round() <= oldest);
|
||||
if is_old { trace!(target: "poa", "Old message {:?}.", message); }
|
||||
is_old
|
||||
}
|
||||
}
|
||||
|
||||
/// Throws out messages older than message, leaves message as marker for the oldest.
|
||||
pub fn throw_out_old(&self, vote_round: &M::Round) {
|
||||
let mut guard = self.votes.write();
|
||||
let new_collector = guard.split_off(vote_round);
|
||||
*guard = new_collector;
|
||||
}
|
||||
|
||||
/// Collects the signatures used to seal a block.
|
||||
pub fn seal_signatures(&self, proposal_round: M::Round, commit_round: M::Round, block_hash: &H256) -> Option<SealSignatures> {
|
||||
let ref bh = Some(*block_hash);
|
||||
let maybe_seal = {
|
||||
let guard = self.votes.read();
|
||||
guard
|
||||
.get(&proposal_round)
|
||||
.and_then(|c| c.block_votes.get(bh))
|
||||
.and_then(|proposals| proposals.keys().next())
|
||||
.map(|proposal| SealSignatures {
|
||||
proposal: proposal.clone(),
|
||||
votes: guard
|
||||
.get(&commit_round)
|
||||
.and_then(|c| c.block_votes.get(bh))
|
||||
.map(|precommits| precommits.keys().cloned().collect())
|
||||
.unwrap_or_else(Vec::new),
|
||||
})
|
||||
.and_then(|seal| if seal.votes.is_empty() { None } else { Some(seal) })
|
||||
};
|
||||
if maybe_seal.is_some() {
|
||||
// Remove messages that are no longer relevant.
|
||||
self.throw_out_old(&commit_round);
|
||||
}
|
||||
maybe_seal
|
||||
}
|
||||
|
||||
/// Count votes which agree with the given message.
|
||||
pub fn count_aligned_votes(&self, message: &M) -> usize {
|
||||
self
|
||||
.votes
|
||||
.read()
|
||||
.get(&message.round())
|
||||
.map_or(0, |m| m.count_block(&message.block_hash()))
|
||||
}
|
||||
|
||||
/// Count all votes collected for a given round.
|
||||
pub fn count_round_votes(&self, vote_round: &M::Round) -> usize {
|
||||
self.votes.read().get(vote_round).map_or(0, StepCollector::count)
|
||||
}
|
||||
|
||||
/// Get all messages older than the round.
|
||||
pub fn get_up_to(&self, round: &M::Round) -> Vec<Bytes> {
|
||||
let guard = self.votes.read();
|
||||
guard
|
||||
.iter()
|
||||
.take_while(|&(r, _)| r <= round)
|
||||
.map(|(_, c)| c.messages.iter().filter(|m| m.is_broadcastable()).map(|m| ::rlp::encode(m).to_vec()).collect::<Vec<_>>())
|
||||
.fold(Vec::new(), |mut acc, mut messages| { acc.append(&mut messages); acc })
|
||||
}
|
||||
|
||||
/// Retrieve address from which the message was sent from cache.
|
||||
pub fn get(&self, message: &M) -> Option<Address> {
|
||||
let guard = self.votes.read();
|
||||
guard.get(&message.round()).and_then(|c| c.block_votes.get(&message.block_hash())).and_then(|origins| origins.get(&message.signature()).cloned())
|
||||
}
|
||||
|
||||
/// Count the number of total rounds kept track of.
|
||||
#[cfg(test)]
|
||||
pub fn len(&self) -> usize {
|
||||
self.votes.read().len()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use util::*;
|
||||
use rlp::*;
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Hash, Default)]
|
||||
struct TestMessage {
|
||||
step: TestStep,
|
||||
block_hash: Option<H256>,
|
||||
signature: H520,
|
||||
}
|
||||
|
||||
type TestStep = u64;
|
||||
|
||||
impl Message for TestMessage {
|
||||
type Round = TestStep;
|
||||
|
||||
fn signature(&self) -> H520 { self.signature }
|
||||
|
||||
fn block_hash(&self) -> Option<H256> { self.block_hash }
|
||||
|
||||
fn round(&self) -> &TestStep { &self.step }
|
||||
|
||||
fn is_broadcastable(&self) -> bool { true }
|
||||
}
|
||||
|
||||
impl Encodable for TestMessage {
|
||||
fn rlp_append(&self, s: &mut RlpStream) {
|
||||
s.begin_list(3)
|
||||
.append(&self.signature)
|
||||
.append(&self.step)
|
||||
.append(&self.block_hash.unwrap_or_else(H256::zero));
|
||||
}
|
||||
}
|
||||
|
||||
fn random_vote(collector: &VoteCollector<TestMessage>, signature: H520, step: TestStep, block_hash: Option<H256>) -> bool {
|
||||
full_vote(collector, signature, step, block_hash, &H160::random()).is_none()
|
||||
}
|
||||
|
||||
fn full_vote<'a>(collector: &VoteCollector<TestMessage>, signature: H520, step: TestStep, block_hash: Option<H256>, address: &'a Address) -> Option<&'a Address> {
|
||||
collector.vote(TestMessage { signature: signature, step: step, block_hash: block_hash }, address)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seal_retrieval() {
|
||||
let collector = VoteCollector::default();
|
||||
let bh = Some("1".sha3());
|
||||
let mut signatures = Vec::new();
|
||||
for _ in 0..5 {
|
||||
signatures.push(H520::random());
|
||||
}
|
||||
let propose_round = 3;
|
||||
let commit_round = 5;
|
||||
// Wrong round.
|
||||
random_vote(&collector, signatures[4].clone(), 1, bh.clone());
|
||||
// Good proposal
|
||||
random_vote(&collector, signatures[0].clone(), propose_round.clone(), bh.clone());
|
||||
// Wrong block proposal.
|
||||
random_vote(&collector, signatures[0].clone(), propose_round.clone(), Some("0".sha3()));
|
||||
// Wrong block commit.
|
||||
random_vote(&collector, signatures[3].clone(), commit_round.clone(), Some("0".sha3()));
|
||||
// Wrong round.
|
||||
random_vote(&collector, signatures[0].clone(), 6, bh.clone());
|
||||
// Wrong round.
|
||||
random_vote(&collector, signatures[0].clone(), 4, bh.clone());
|
||||
// Relevant commit.
|
||||
random_vote(&collector, signatures[2].clone(), commit_round.clone(), bh.clone());
|
||||
// Replicated vote.
|
||||
random_vote(&collector, signatures[2].clone(), commit_round.clone(), bh.clone());
|
||||
// Wrong round.
|
||||
random_vote(&collector, signatures[4].clone(), 6, bh.clone());
|
||||
// Relevant precommit.
|
||||
random_vote(&collector, signatures[1].clone(), commit_round.clone(), bh.clone());
|
||||
// Wrong round, same signature.
|
||||
random_vote(&collector, signatures[1].clone(), 7, bh.clone());
|
||||
let seal = SealSignatures {
|
||||
proposal: signatures[0],
|
||||
votes: signatures[1..3].to_vec()
|
||||
};
|
||||
assert_eq!(seal, collector.seal_signatures(propose_round, commit_round, &bh.unwrap()).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn count_votes() {
|
||||
let collector = VoteCollector::default();
|
||||
let round1 = 1;
|
||||
let round3 = 3;
|
||||
// good 1
|
||||
random_vote(&collector, H520::random(), round1, Some("0".sha3()));
|
||||
random_vote(&collector, H520::random(), 0, Some("0".sha3()));
|
||||
// good 3
|
||||
random_vote(&collector, H520::random(), round3, Some("0".sha3()));
|
||||
random_vote(&collector, H520::random(), 2, Some("0".sha3()));
|
||||
// good prevote
|
||||
random_vote(&collector, H520::random(), round1, Some("1".sha3()));
|
||||
// good prevote
|
||||
let same_sig = H520::random();
|
||||
random_vote(&collector, same_sig.clone(), round1, Some("1".sha3()));
|
||||
random_vote(&collector, same_sig, round1, Some("1".sha3()));
|
||||
// good precommit
|
||||
random_vote(&collector, H520::random(), round3, Some("1".sha3()));
|
||||
// good prevote
|
||||
random_vote(&collector, H520::random(), round1, Some("0".sha3()));
|
||||
random_vote(&collector, H520::random(), 4, Some("2".sha3()));
|
||||
|
||||
assert_eq!(collector.count_round_votes(&round1), 4);
|
||||
assert_eq!(collector.count_round_votes(&round3), 2);
|
||||
|
||||
let message = TestMessage {
|
||||
signature: H520::default(),
|
||||
step: round1,
|
||||
block_hash: Some("1".sha3())
|
||||
};
|
||||
assert_eq!(collector.count_aligned_votes(&message), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_old() {
|
||||
let collector = VoteCollector::default();
|
||||
let vote = |round, hash| {
|
||||
random_vote(&collector, H520::random(), round, hash);
|
||||
};
|
||||
vote(6, Some("0".sha3()));
|
||||
vote(3, Some("0".sha3()));
|
||||
vote(7, Some("0".sha3()));
|
||||
vote(8, Some("1".sha3()));
|
||||
vote(1, Some("1".sha3()));
|
||||
|
||||
collector.throw_out_old(&7);
|
||||
assert_eq!(collector.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malicious_authority() {
|
||||
let collector = VoteCollector::default();
|
||||
let round = 3;
|
||||
// Vote is inserted fine.
|
||||
assert!(full_vote(&collector, H520::random(), round, Some("0".sha3()), &Address::default()).is_none());
|
||||
// Returns the double voting address.
|
||||
full_vote(&collector, H520::random(), round, Some("1".sha3()), &Address::default()).unwrap();
|
||||
assert_eq!(collector.count_round_votes(&round), 1);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user