mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-07-12 16:55:46 +00:00
Slash Authorities for irrefutable misbehavior (#84)
* double-commit and prepare misbehavior * get misbehavior on completion * collect misbehavior on drop, not only on success * kill unused transaction_index field * add primitive misbehavior report type * add misbehavior report transaction * store prior session * fix set_items * basic checks for misbehavior reports * crate for substrate bft misbehavior checking * integrate misbehavior check crate * fix comment * new wasm binaries * fix hash in test * import misbehavior transactions into queue * fix test build * sign on digest and full proposal when proposing * detect proposal misbehavior * fix fallout * restore balance/bondage types
This commit is contained in:
committed by
GitHub
parent
de6e7e9136
commit
27c9e6de9a
@@ -14,13 +14,13 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Substrate. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Message accumulator for each round of BFT consensus.
|
||||
//! Vote accumulator for each round of BFT consensus.
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::hash::Hash;
|
||||
|
||||
use generic::{Message, LocalizedMessage};
|
||||
use generic::{Vote, LocalizedMessage, LocalizedProposal};
|
||||
|
||||
/// Justification for some state at a given round.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -122,6 +122,26 @@ struct VoteCounts {
|
||||
committed: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Proposal<Candidate, Digest, Signature> {
|
||||
proposal: Candidate,
|
||||
digest: Digest,
|
||||
digest_signature: Signature,
|
||||
}
|
||||
|
||||
/// Misbehavior which can occur.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Misbehavior<Digest, Signature> {
|
||||
/// Proposed out-of-turn.
|
||||
ProposeOutOfTurn(usize, Digest, Signature),
|
||||
/// Issued two conflicting proposals.
|
||||
DoublePropose(usize, (Digest, Signature), (Digest, Signature)),
|
||||
/// Issued two conflicting prepare messages.
|
||||
DoublePrepare(usize, (Digest, Signature), (Digest, Signature)),
|
||||
/// Issued two conflicting commit messages.
|
||||
DoubleCommit(usize, (Digest, Signature), (Digest, Signature)),
|
||||
}
|
||||
|
||||
/// Accumulates messages for a given round of BFT consensus.
|
||||
///
|
||||
/// This isn't tied to the "view" of a single authority. It
|
||||
@@ -132,13 +152,13 @@ pub struct Accumulator<Candidate, Digest, AuthorityId, Signature>
|
||||
where
|
||||
Candidate: Eq + Clone,
|
||||
Digest: Hash + Eq + Clone,
|
||||
AuthorityId: Hash + Eq,
|
||||
AuthorityId: Hash + Eq + Clone,
|
||||
Signature: Eq + Clone,
|
||||
{
|
||||
round_number: usize,
|
||||
threshold: usize,
|
||||
round_proposer: AuthorityId,
|
||||
proposal: Option<Candidate>,
|
||||
proposal: Option<Proposal<Candidate, Digest, Signature>>,
|
||||
prepares: HashMap<AuthorityId, (Digest, Signature)>,
|
||||
commits: HashMap<AuthorityId, (Digest, Signature)>,
|
||||
vote_counts: HashMap<Digest, VoteCounts>,
|
||||
@@ -150,7 +170,7 @@ impl<Candidate, Digest, AuthorityId, Signature> Accumulator<Candidate, Digest, A
|
||||
where
|
||||
Candidate: Eq + Clone,
|
||||
Digest: Hash + Eq + Clone,
|
||||
AuthorityId: Hash + Eq,
|
||||
AuthorityId: Hash + Eq + Clone,
|
||||
Signature: Eq + Clone,
|
||||
{
|
||||
/// Create a new state accumulator.
|
||||
@@ -179,7 +199,7 @@ impl<Candidate, Digest, AuthorityId, Signature> Accumulator<Candidate, Digest, A
|
||||
}
|
||||
|
||||
pub fn proposal(&self) -> Option<&Candidate> {
|
||||
self.proposal.as_ref()
|
||||
self.proposal.as_ref().map(|p| &p.proposal)
|
||||
}
|
||||
|
||||
/// Inspect the current consensus state.
|
||||
@@ -192,32 +212,61 @@ impl<Candidate, Digest, AuthorityId, Signature> Accumulator<Candidate, Digest, A
|
||||
pub fn import_message(
|
||||
&mut self,
|
||||
message: LocalizedMessage<Candidate, Digest, AuthorityId, Signature>,
|
||||
)
|
||||
{
|
||||
) -> Result<(), Misbehavior<Digest, Signature>> {
|
||||
// message from different round.
|
||||
if message.message.round_number() != self.round_number {
|
||||
return;
|
||||
if message.round_number() != self.round_number {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let (sender, signature) = (message.sender, message.signature);
|
||||
|
||||
match message.message {
|
||||
Message::Propose(_, p) => self.import_proposal(p, sender),
|
||||
Message::Prepare(_, d) => self.import_prepare(d, sender, signature),
|
||||
Message::Commit(_, d) => self.import_commit(d, sender, signature),
|
||||
Message::AdvanceRound(_) => self.import_advance_round(sender),
|
||||
match message {
|
||||
LocalizedMessage::Propose(proposal) => self.import_proposal(proposal),
|
||||
LocalizedMessage::Vote(vote) => {
|
||||
let (sender, signature) = (vote.sender, vote.signature);
|
||||
match vote.vote {
|
||||
Vote::Prepare(_, d) => self.import_prepare(d, sender, signature),
|
||||
Vote::Commit(_, d) => self.import_commit(d, sender, signature),
|
||||
Vote::AdvanceRound(_) => self.import_advance_round(sender),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn import_proposal(
|
||||
&mut self,
|
||||
proposal: Candidate,
|
||||
sender: AuthorityId,
|
||||
) {
|
||||
if sender != self.round_proposer || self.proposal.is_some() { return }
|
||||
proposal: LocalizedProposal<Candidate, Digest, AuthorityId, Signature>,
|
||||
) -> Result<(), Misbehavior<Digest, Signature>> {
|
||||
let sender = proposal.sender;
|
||||
|
||||
self.proposal = Some(proposal.clone());
|
||||
self.state = State::Proposed(proposal);
|
||||
if sender != self.round_proposer {
|
||||
return Err(Misbehavior::ProposeOutOfTurn(
|
||||
self.round_number,
|
||||
proposal.digest,
|
||||
proposal.digest_signature)
|
||||
);
|
||||
}
|
||||
|
||||
match self.proposal {
|
||||
Some(ref p) if &p.digest != &proposal.digest => {
|
||||
return Err(Misbehavior::DoublePropose(
|
||||
self.round_number,
|
||||
{
|
||||
let old = self.proposal.as_ref().expect("just checked to be Some; qed");
|
||||
(old.digest.clone(), old.digest_signature.clone())
|
||||
},
|
||||
(proposal.digest.clone(), proposal.digest_signature.clone())
|
||||
))
|
||||
}
|
||||
_ => {},
|
||||
}
|
||||
|
||||
self.proposal = Some(Proposal {
|
||||
proposal: proposal.proposal.clone(),
|
||||
digest: proposal.digest,
|
||||
digest_signature: proposal.digest_signature,
|
||||
});
|
||||
|
||||
self.state = State::Proposed(proposal.proposal);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn import_prepare(
|
||||
@@ -225,21 +274,32 @@ impl<Candidate, Digest, AuthorityId, Signature> Accumulator<Candidate, Digest, A
|
||||
digest: Digest,
|
||||
sender: AuthorityId,
|
||||
signature: Signature,
|
||||
) {
|
||||
) -> Result<(), Misbehavior<Digest, Signature>> {
|
||||
// ignore any subsequent prepares by the same sender.
|
||||
// TODO: if digest is different, that's misbehavior.
|
||||
let threshold_prepared = if let Entry::Vacant(vacant) = self.prepares.entry(sender) {
|
||||
vacant.insert((digest.clone(), signature));
|
||||
let count = self.vote_counts.entry(digest.clone()).or_insert_with(Default::default);
|
||||
count.prepared += 1;
|
||||
let threshold_prepared = match self.prepares.entry(sender.clone()) {
|
||||
Entry::Vacant(vacant) => {
|
||||
vacant.insert((digest.clone(), signature));
|
||||
let count = self.vote_counts.entry(digest.clone()).or_insert_with(Default::default);
|
||||
count.prepared += 1;
|
||||
|
||||
if count.prepared >= self.threshold {
|
||||
Some(digest)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
Entry::Occupied(occupied) => {
|
||||
// if digest is different, that's misbehavior.
|
||||
if occupied.get().0 != digest {
|
||||
return Err(Misbehavior::DoublePrepare(
|
||||
self.round_number,
|
||||
occupied.get().clone(),
|
||||
(digest, signature)
|
||||
));
|
||||
}
|
||||
|
||||
if count.prepared >= self.threshold {
|
||||
Some(digest)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// only allow transition to prepare from begin or proposed state.
|
||||
@@ -261,6 +321,8 @@ impl<Candidate, Digest, AuthorityId, Signature> Accumulator<Candidate, Digest, A
|
||||
signatures: signatures,
|
||||
}));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn import_commit(
|
||||
@@ -268,21 +330,32 @@ impl<Candidate, Digest, AuthorityId, Signature> Accumulator<Candidate, Digest, A
|
||||
digest: Digest,
|
||||
sender: AuthorityId,
|
||||
signature: Signature,
|
||||
) {
|
||||
) -> Result<(), Misbehavior<Digest, Signature>> {
|
||||
// ignore any subsequent commits by the same sender.
|
||||
// TODO: if digest is different, that's misbehavior.
|
||||
let threshold_committed = if let Entry::Vacant(vacant) = self.commits.entry(sender) {
|
||||
vacant.insert((digest.clone(), signature));
|
||||
let count = self.vote_counts.entry(digest.clone()).or_insert_with(Default::default);
|
||||
count.committed += 1;
|
||||
let threshold_committed = match self.commits.entry(sender.clone()) {
|
||||
Entry::Vacant(vacant) => {
|
||||
vacant.insert((digest.clone(), signature));
|
||||
let count = self.vote_counts.entry(digest.clone()).or_insert_with(Default::default);
|
||||
count.committed += 1;
|
||||
|
||||
if count.committed >= self.threshold {
|
||||
Some(digest)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
Entry::Occupied(occupied) => {
|
||||
// if digest is different, that's misbehavior.
|
||||
if occupied.get().0 != digest {
|
||||
return Err(Misbehavior::DoubleCommit(
|
||||
self.round_number,
|
||||
occupied.get().clone(),
|
||||
(digest, signature)
|
||||
));
|
||||
}
|
||||
|
||||
if count.committed >= self.threshold {
|
||||
Some(digest)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// transition to concluded state always valid.
|
||||
@@ -302,15 +375,17 @@ impl<Candidate, Digest, AuthorityId, Signature> Accumulator<Candidate, Digest, A
|
||||
signatures: signatures,
|
||||
}));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn import_advance_round(
|
||||
&mut self,
|
||||
sender: AuthorityId,
|
||||
) {
|
||||
) -> Result<(), Misbehavior<Digest, Signature>> {
|
||||
self.advance_round.insert(sender);
|
||||
|
||||
if self.advance_round.len() < self.threshold { return }
|
||||
if self.advance_round.len() < self.threshold { return Ok(()) }
|
||||
|
||||
// allow transition to new round only if we haven't produced a justification
|
||||
// yet.
|
||||
@@ -319,13 +394,16 @@ impl<Candidate, Digest, AuthorityId, Signature> Accumulator<Candidate, Digest, A
|
||||
State::Prepared(j) => State::Advanced(Some(j)),
|
||||
State::Advanced(j) => State::Advanced(j),
|
||||
State::Begin | State::Proposed(_) => State::Advanced(None),
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use generic::{LocalizedMessage, LocalizedProposal, LocalizedVote};
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
pub struct Candidate(usize);
|
||||
@@ -333,7 +411,7 @@ mod tests {
|
||||
#[derive(Hash, PartialEq, Eq, Clone, Debug)]
|
||||
pub struct Digest(usize);
|
||||
|
||||
#[derive(Hash, PartialEq, Eq, Debug)]
|
||||
#[derive(Hash, PartialEq, Eq, Debug, Clone)]
|
||||
pub struct AuthorityId(usize);
|
||||
|
||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||
@@ -375,19 +453,27 @@ mod tests {
|
||||
let mut accumulator = Accumulator::<_, Digest, _, _>::new(1, 7, AuthorityId(8));
|
||||
assert_eq!(accumulator.state(), &State::Begin);
|
||||
|
||||
accumulator.import_message(LocalizedMessage {
|
||||
let res = accumulator.import_message(LocalizedMessage::Propose(LocalizedProposal {
|
||||
sender: AuthorityId(5),
|
||||
signature: Signature(999, 5),
|
||||
message: Message::Propose(1, Candidate(999)),
|
||||
});
|
||||
full_signature: Signature(999, 5),
|
||||
digest_signature: Signature(999, 5),
|
||||
proposal: Candidate(999),
|
||||
digest: Digest(999),
|
||||
round_number: 1,
|
||||
}));
|
||||
|
||||
assert!(res.is_err());
|
||||
|
||||
assert_eq!(accumulator.state(), &State::Begin);
|
||||
|
||||
accumulator.import_message(LocalizedMessage {
|
||||
accumulator.import_message(LocalizedMessage::Propose(LocalizedProposal {
|
||||
sender: AuthorityId(8),
|
||||
signature: Signature(999, 8),
|
||||
message: Message::Propose(1, Candidate(999)),
|
||||
});
|
||||
full_signature: Signature(999, 8),
|
||||
digest_signature: Signature(999, 8),
|
||||
proposal: Candidate(999),
|
||||
digest: Digest(999),
|
||||
round_number: 1,
|
||||
})).unwrap();
|
||||
|
||||
assert_eq!(accumulator.state(), &State::Proposed(Candidate(999)));
|
||||
}
|
||||
@@ -397,29 +483,32 @@ mod tests {
|
||||
let mut accumulator = Accumulator::new(1, 7, AuthorityId(8));
|
||||
assert_eq!(accumulator.state(), &State::Begin);
|
||||
|
||||
accumulator.import_message(LocalizedMessage {
|
||||
accumulator.import_message(LocalizedMessage::Propose(LocalizedProposal {
|
||||
sender: AuthorityId(8),
|
||||
signature: Signature(999, 8),
|
||||
message: Message::Propose(1, Candidate(999)),
|
||||
});
|
||||
full_signature: Signature(999, 8),
|
||||
digest_signature: Signature(999, 8),
|
||||
round_number: 1,
|
||||
proposal: Candidate(999),
|
||||
digest: Digest(999),
|
||||
})).unwrap();
|
||||
|
||||
assert_eq!(accumulator.state(), &State::Proposed(Candidate(999)));
|
||||
|
||||
for i in 0..6 {
|
||||
accumulator.import_message(LocalizedMessage {
|
||||
accumulator.import_message(LocalizedVote {
|
||||
sender: AuthorityId(i),
|
||||
signature: Signature(999, i),
|
||||
message: Message::Prepare(1, Digest(999)),
|
||||
});
|
||||
vote: Vote::Prepare(1, Digest(999)),
|
||||
}.into()).unwrap();
|
||||
|
||||
assert_eq!(accumulator.state(), &State::Proposed(Candidate(999)));
|
||||
}
|
||||
|
||||
accumulator.import_message(LocalizedMessage {
|
||||
accumulator.import_message(LocalizedVote {
|
||||
sender: AuthorityId(7),
|
||||
signature: Signature(999, 7),
|
||||
message: Message::Prepare(1, Digest(999)),
|
||||
});
|
||||
vote: Vote::Prepare(1, Digest(999)),
|
||||
}.into()).unwrap();
|
||||
|
||||
match accumulator.state() {
|
||||
&State::Prepared(ref j) => assert_eq!(j.digest, Digest(999)),
|
||||
@@ -432,29 +521,32 @@ mod tests {
|
||||
let mut accumulator = Accumulator::new(1, 7, AuthorityId(8));
|
||||
assert_eq!(accumulator.state(), &State::Begin);
|
||||
|
||||
accumulator.import_message(LocalizedMessage {
|
||||
accumulator.import_message(LocalizedMessage::Propose(LocalizedProposal {
|
||||
sender: AuthorityId(8),
|
||||
signature: Signature(999, 8),
|
||||
message: Message::Propose(1, Candidate(999)),
|
||||
});
|
||||
full_signature: Signature(999, 8),
|
||||
digest_signature: Signature(999, 8),
|
||||
round_number: 1,
|
||||
proposal: Candidate(999),
|
||||
digest: Digest(999),
|
||||
})).unwrap();
|
||||
|
||||
assert_eq!(accumulator.state(), &State::Proposed(Candidate(999)));
|
||||
|
||||
for i in 0..6 {
|
||||
accumulator.import_message(LocalizedMessage {
|
||||
accumulator.import_message(LocalizedVote {
|
||||
sender: AuthorityId(i),
|
||||
signature: Signature(999, i),
|
||||
message: Message::Prepare(1, Digest(999)),
|
||||
});
|
||||
vote: Vote::Prepare(1, Digest(999)),
|
||||
}.into()).unwrap();
|
||||
|
||||
assert_eq!(accumulator.state(), &State::Proposed(Candidate(999)));
|
||||
}
|
||||
|
||||
accumulator.import_message(LocalizedMessage {
|
||||
accumulator.import_message(LocalizedVote {
|
||||
sender: AuthorityId(7),
|
||||
signature: Signature(999, 7),
|
||||
message: Message::Prepare(1, Digest(999)),
|
||||
});
|
||||
vote: Vote::Prepare(1, Digest(999)),
|
||||
}.into()).unwrap();
|
||||
|
||||
match accumulator.state() {
|
||||
&State::Prepared(ref j) => assert_eq!(j.digest, Digest(999)),
|
||||
@@ -462,11 +554,11 @@ mod tests {
|
||||
}
|
||||
|
||||
for i in 0..6 {
|
||||
accumulator.import_message(LocalizedMessage {
|
||||
accumulator.import_message(LocalizedVote {
|
||||
sender: AuthorityId(i),
|
||||
signature: Signature(999, i),
|
||||
message: Message::Commit(1, Digest(999)),
|
||||
});
|
||||
vote: Vote::Commit(1, Digest(999)),
|
||||
}.into()).unwrap();
|
||||
|
||||
match accumulator.state() {
|
||||
&State::Prepared(_) => {},
|
||||
@@ -474,11 +566,11 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
accumulator.import_message(LocalizedMessage {
|
||||
accumulator.import_message(LocalizedVote {
|
||||
sender: AuthorityId(7),
|
||||
signature: Signature(999, 7),
|
||||
message: Message::Commit(1, Digest(999)),
|
||||
});
|
||||
vote: Vote::Commit(1, Digest(999)),
|
||||
}.into()).unwrap();
|
||||
|
||||
match accumulator.state() {
|
||||
&State::Committed(ref j) => assert_eq!(j.digest, Digest(999)),
|
||||
@@ -491,20 +583,23 @@ mod tests {
|
||||
let mut accumulator = Accumulator::new(1, 7, AuthorityId(8));
|
||||
assert_eq!(accumulator.state(), &State::Begin);
|
||||
|
||||
accumulator.import_message(LocalizedMessage {
|
||||
accumulator.import_message(LocalizedMessage::Propose(LocalizedProposal {
|
||||
sender: AuthorityId(8),
|
||||
signature: Signature(999, 8),
|
||||
message: Message::Propose(1, Candidate(999)),
|
||||
});
|
||||
full_signature: Signature(999, 8),
|
||||
digest_signature: Signature(999, 8),
|
||||
round_number: 1,
|
||||
proposal: Candidate(999),
|
||||
digest: Digest(999),
|
||||
})).unwrap();
|
||||
|
||||
assert_eq!(accumulator.state(), &State::Proposed(Candidate(999)));
|
||||
|
||||
for i in 0..7 {
|
||||
accumulator.import_message(LocalizedMessage {
|
||||
accumulator.import_message(LocalizedVote {
|
||||
sender: AuthorityId(i),
|
||||
signature: Signature(999, i),
|
||||
message: Message::Prepare(1, Digest(999)),
|
||||
});
|
||||
vote: Vote::Prepare(1, Digest(999)),
|
||||
}.into()).unwrap();
|
||||
}
|
||||
|
||||
match accumulator.state() {
|
||||
@@ -513,11 +608,11 @@ mod tests {
|
||||
}
|
||||
|
||||
for i in 0..6 {
|
||||
accumulator.import_message(LocalizedMessage {
|
||||
accumulator.import_message(LocalizedVote {
|
||||
sender: AuthorityId(i),
|
||||
signature: Signature(999, i),
|
||||
message: Message::AdvanceRound(1),
|
||||
});
|
||||
vote: Vote::AdvanceRound(1),
|
||||
}.into()).unwrap();
|
||||
|
||||
match accumulator.state() {
|
||||
&State::Prepared(_) => {},
|
||||
@@ -525,11 +620,11 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
accumulator.import_message(LocalizedMessage {
|
||||
accumulator.import_message(LocalizedVote {
|
||||
sender: AuthorityId(7),
|
||||
signature: Signature(999, 7),
|
||||
message: Message::AdvanceRound(1),
|
||||
});
|
||||
vote: Vote::AdvanceRound(1),
|
||||
}.into()).unwrap();
|
||||
|
||||
match accumulator.state() {
|
||||
&State::Advanced(Some(_)) => {},
|
||||
@@ -543,11 +638,11 @@ mod tests {
|
||||
assert_eq!(accumulator.state(), &State::Begin);
|
||||
|
||||
for i in 0..7 {
|
||||
accumulator.import_message(LocalizedMessage {
|
||||
accumulator.import_message(LocalizedVote {
|
||||
sender: AuthorityId(i),
|
||||
signature: Signature(999, i),
|
||||
message: Message::Prepare(1, Digest(999)),
|
||||
});
|
||||
vote: Vote::Prepare(1, Digest(999)),
|
||||
}.into()).unwrap();
|
||||
}
|
||||
|
||||
match accumulator.state() {
|
||||
@@ -556,11 +651,11 @@ mod tests {
|
||||
}
|
||||
|
||||
for i in 0..7 {
|
||||
accumulator.import_message(LocalizedMessage {
|
||||
accumulator.import_message(LocalizedVote {
|
||||
sender: AuthorityId(i),
|
||||
signature: Signature(999, i),
|
||||
message: Message::Commit(1, Digest(999)),
|
||||
});
|
||||
vote: Vote::Commit(1, Digest(999)),
|
||||
}.into()).unwrap();
|
||||
}
|
||||
|
||||
match accumulator.state() {
|
||||
@@ -575,11 +670,11 @@ mod tests {
|
||||
assert_eq!(accumulator.state(), &State::Begin);
|
||||
|
||||
for i in 0..7 {
|
||||
accumulator.import_message(LocalizedMessage {
|
||||
accumulator.import_message(LocalizedVote {
|
||||
sender: AuthorityId(i),
|
||||
signature: Signature(1, i),
|
||||
message: Message::AdvanceRound(1),
|
||||
});
|
||||
vote: Vote::AdvanceRound(1),
|
||||
}.into()).unwrap();
|
||||
}
|
||||
|
||||
match accumulator.state() {
|
||||
@@ -594,11 +689,11 @@ mod tests {
|
||||
assert_eq!(accumulator.state(), &State::Begin);
|
||||
|
||||
for i in 0..7 {
|
||||
accumulator.import_message(LocalizedMessage {
|
||||
accumulator.import_message(LocalizedVote {
|
||||
sender: AuthorityId(i),
|
||||
signature: Signature(999, i),
|
||||
message: Message::Commit(1, Digest(999)),
|
||||
});
|
||||
vote: Vote::Commit(1, Digest(999)),
|
||||
}.into()).unwrap();
|
||||
}
|
||||
|
||||
match accumulator.state() {
|
||||
@@ -606,4 +701,76 @@ mod tests {
|
||||
s => panic!("wrong state: {:?}", s),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn double_prepare_is_misbehavior() {
|
||||
let mut accumulator = Accumulator::<Candidate, _, _, _>::new(1, 7, AuthorityId(8));
|
||||
assert_eq!(accumulator.state(), &State::Begin);
|
||||
|
||||
for i in 0..7 {
|
||||
accumulator.import_message(LocalizedVote {
|
||||
sender: AuthorityId(i),
|
||||
signature: Signature(999, i),
|
||||
vote: Vote::Prepare(1, Digest(999)),
|
||||
}.into()).unwrap();
|
||||
|
||||
let res = accumulator.import_message(LocalizedVote {
|
||||
sender: AuthorityId(i),
|
||||
signature: Signature(123, i),
|
||||
vote: Vote::Prepare(1, Digest(123)),
|
||||
}.into());
|
||||
|
||||
assert!(res.is_err());
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn double_commit_is_misbehavior() {
|
||||
let mut accumulator = Accumulator::<Candidate, _, _, _>::new(1, 7, AuthorityId(8));
|
||||
assert_eq!(accumulator.state(), &State::Begin);
|
||||
|
||||
for i in 0..7 {
|
||||
accumulator.import_message(LocalizedVote {
|
||||
sender: AuthorityId(i),
|
||||
signature: Signature(999, i),
|
||||
vote: Vote::Commit(1, Digest(999)),
|
||||
}.into()).unwrap();
|
||||
|
||||
let res = accumulator.import_message(LocalizedVote {
|
||||
sender: AuthorityId(i),
|
||||
signature: Signature(123, i),
|
||||
vote: Vote::Commit(1, Digest(123)),
|
||||
}.into());
|
||||
|
||||
assert!(res.is_err());
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn double_propose_is_misbehavior() {
|
||||
let mut accumulator = Accumulator::<Candidate, _, _, _>::new(1, 7, AuthorityId(8));
|
||||
assert_eq!(accumulator.state(), &State::Begin);
|
||||
|
||||
accumulator.import_message(LocalizedMessage::Propose(LocalizedProposal {
|
||||
sender: AuthorityId(8),
|
||||
full_signature: Signature(999, 8),
|
||||
digest_signature: Signature(999, 8),
|
||||
round_number: 1,
|
||||
proposal: Candidate(999),
|
||||
digest: Digest(999),
|
||||
})).unwrap();
|
||||
|
||||
let res = accumulator.import_message(LocalizedMessage::Propose(LocalizedProposal {
|
||||
sender: AuthorityId(8),
|
||||
full_signature: Signature(500, 8),
|
||||
digest_signature: Signature(500, 8),
|
||||
round_number: 1,
|
||||
proposal: Candidate(500),
|
||||
digest: Digest(500),
|
||||
}));
|
||||
|
||||
assert!(res.is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
//! Very general implementation.
|
||||
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::collections::hash_map;
|
||||
use std::fmt::Debug;
|
||||
use std::hash::Hash;
|
||||
|
||||
@@ -25,19 +26,16 @@ use futures::{future, Future, Stream, Sink, Poll, Async, AsyncSink};
|
||||
|
||||
use self::accumulator::State;
|
||||
|
||||
pub use self::accumulator::{Accumulator, Justification, PrepareJustification, UncheckedJustification};
|
||||
pub use self::accumulator::{Accumulator, Justification, PrepareJustification, UncheckedJustification, Misbehavior};
|
||||
|
||||
mod accumulator;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
/// Messages over the proposal.
|
||||
/// Each message carries an associated round number.
|
||||
/// Votes during a round.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Message<C, D> {
|
||||
/// Send a full proposal.
|
||||
Propose(usize, C),
|
||||
pub enum Vote<D> {
|
||||
/// Prepare to vote for proposal with digest D.
|
||||
Prepare(usize, D),
|
||||
/// Commit to proposal with digest D..
|
||||
@@ -46,29 +44,94 @@ pub enum Message<C, D> {
|
||||
AdvanceRound(usize),
|
||||
}
|
||||
|
||||
impl<C, D> Message<C, D> {
|
||||
impl<D> Vote<D> {
|
||||
/// Extract the round number.
|
||||
pub fn round_number(&self) -> usize {
|
||||
match *self {
|
||||
Message::Propose(round, _) => round,
|
||||
Message::Prepare(round, _) => round,
|
||||
Message::Commit(round, _) => round,
|
||||
Message::AdvanceRound(round) => round,
|
||||
Vote::Prepare(round, _) => round,
|
||||
Vote::Commit(round, _) => round,
|
||||
Vote::AdvanceRound(round) => round,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A localized message, including the sender.
|
||||
/// Messages over the proposal.
|
||||
/// Each message carries an associated round number.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Message<C, D> {
|
||||
/// A proposal itself.
|
||||
Propose(usize, C),
|
||||
/// A vote of some kind, localized to a round number.
|
||||
Vote(Vote<D>),
|
||||
}
|
||||
|
||||
impl<C, D> From<Vote<D>> for Message<C, D> {
|
||||
fn from(vote: Vote<D>) -> Self {
|
||||
Message::Vote(vote)
|
||||
}
|
||||
}
|
||||
|
||||
/// A localized proposal message. Contains two signed pieces of data.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LocalizedMessage<C, D, V, S> {
|
||||
/// The message received.
|
||||
pub message: Message<C, D>,
|
||||
pub struct LocalizedProposal<C, D, V, S> {
|
||||
/// The round number.
|
||||
pub round_number: usize,
|
||||
/// The proposal sent.
|
||||
pub proposal: C,
|
||||
/// The digest of the proposal.
|
||||
pub digest: D,
|
||||
/// The sender of the proposal
|
||||
pub sender: V,
|
||||
/// The signature on the message (propose, round number, digest)
|
||||
pub digest_signature: S,
|
||||
/// The signature on the message (propose, round number, proposal)
|
||||
pub full_signature: S,
|
||||
}
|
||||
|
||||
/// A localized vote message, including the sender.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LocalizedVote<D, V, S> {
|
||||
/// The message sent.
|
||||
pub vote: Vote<D>,
|
||||
/// The sender of the message
|
||||
pub sender: V,
|
||||
/// The signature of the message.
|
||||
pub signature: S,
|
||||
}
|
||||
|
||||
/// A localized message.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum LocalizedMessage<C, D, V, S> {
|
||||
/// A proposal.
|
||||
Propose(LocalizedProposal<C, D, V, S>),
|
||||
/// A vote.
|
||||
Vote(LocalizedVote<D, V, S>),
|
||||
}
|
||||
|
||||
impl<C, D, V, S> LocalizedMessage<C, D, V, S> {
|
||||
/// Extract the sender.
|
||||
pub fn sender(&self) -> &V {
|
||||
match *self {
|
||||
LocalizedMessage::Propose(ref proposal) => &proposal.sender,
|
||||
LocalizedMessage::Vote(ref vote) => &vote.sender,
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract the round number.
|
||||
pub fn round_number(&self) -> usize {
|
||||
match *self {
|
||||
LocalizedMessage::Propose(ref proposal) => proposal.round_number,
|
||||
LocalizedMessage::Vote(ref vote) => vote.vote.round_number(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<C, D, V, S> From<LocalizedVote<D, V, S>> for LocalizedMessage<C, D, V, S> {
|
||||
fn from(vote: LocalizedVote<D, V, S>) -> Self {
|
||||
LocalizedMessage::Vote(vote)
|
||||
}
|
||||
}
|
||||
|
||||
/// Context necessary for agreement.
|
||||
///
|
||||
/// Provides necessary types for protocol messages, and functions necessary for a
|
||||
@@ -101,6 +164,8 @@ pub trait Context {
|
||||
fn candidate_digest(&self, candidate: &Self::Candidate) -> Self::Digest;
|
||||
|
||||
/// Sign a message using the local authority ID.
|
||||
/// In the case of a proposal message, it should sign on the hash and
|
||||
/// the bytes of the proposal.
|
||||
fn sign_local(&self, message: Message<Self::Candidate, Self::Digest>)
|
||||
-> LocalizedMessage<Self::Candidate, Self::Digest, Self::AuthorityId, Self::Signature>;
|
||||
|
||||
@@ -258,6 +323,7 @@ struct Strategy<C: Context> {
|
||||
current_accumulator: Accumulator<C::Candidate, C::Digest, C::AuthorityId, C::Signature>,
|
||||
future_accumulator: Accumulator<C::Candidate, C::Digest, C::AuthorityId, C::Signature>,
|
||||
local_id: C::AuthorityId,
|
||||
misbehavior: HashMap<C::AuthorityId, Misbehavior<C::Digest, C::Signature>>,
|
||||
}
|
||||
|
||||
impl<C: Context> Strategy<C> {
|
||||
@@ -289,6 +355,7 @@ impl<C: Context> Strategy<C> {
|
||||
notable_candidates: HashMap::new(),
|
||||
round_timeout: timeout.fuse(),
|
||||
local_id: context.local_id(),
|
||||
misbehavior: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,12 +363,19 @@ impl<C: Context> Strategy<C> {
|
||||
&mut self,
|
||||
msg: LocalizedMessage<C::Candidate, C::Digest, C::AuthorityId, C::Signature>
|
||||
) {
|
||||
let round_number = msg.message.round_number();
|
||||
let round_number = msg.round_number();
|
||||
|
||||
if round_number == self.current_accumulator.round_number() {
|
||||
self.current_accumulator.import_message(msg);
|
||||
let sender = msg.sender().clone();
|
||||
let misbehavior = if round_number == self.current_accumulator.round_number() {
|
||||
self.current_accumulator.import_message(msg)
|
||||
} else if round_number == self.future_accumulator.round_number() {
|
||||
self.future_accumulator.import_message(msg);
|
||||
self.future_accumulator.import_message(msg)
|
||||
} else {
|
||||
Ok(())
|
||||
};
|
||||
|
||||
if let Err(misbehavior) = misbehavior {
|
||||
self.misbehavior.insert(sender, misbehavior);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -526,10 +600,10 @@ impl<C: Context> Strategy<C> {
|
||||
}
|
||||
|
||||
if let Some(digest) = prepare_for {
|
||||
let message = Message::Prepare(
|
||||
let message = Vote::Prepare(
|
||||
self.current_accumulator.round_number(),
|
||||
digest
|
||||
);
|
||||
).into();
|
||||
|
||||
self.import_and_send_message(message, context, sending);
|
||||
self.local_state = LocalState::Prepared;
|
||||
@@ -559,10 +633,10 @@ impl<C: Context> Strategy<C> {
|
||||
}
|
||||
|
||||
if let Some(digest) = commit_for {
|
||||
let message = Message::Commit(
|
||||
let message = Vote::Commit(
|
||||
self.current_accumulator.round_number(),
|
||||
digest
|
||||
);
|
||||
).into();
|
||||
|
||||
self.import_and_send_message(message, context, sending);
|
||||
self.local_state = LocalState::Committed;
|
||||
@@ -588,9 +662,9 @@ impl<C: Context> Strategy<C> {
|
||||
}
|
||||
|
||||
if attempt_advance {
|
||||
let message = Message::AdvanceRound(
|
||||
let message = Vote::AdvanceRound(
|
||||
self.current_accumulator.round_number(),
|
||||
);
|
||||
).into();
|
||||
|
||||
self.import_and_send_message(message, context, sending);
|
||||
self.local_state = LocalState::VoteAdvance;
|
||||
@@ -715,6 +789,18 @@ impl<C, I, O> Future for Agreement<C, I, O>
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: Context, I, O> Agreement<C, I, O> {
|
||||
/// Get a reference to the underlying context.
|
||||
pub fn context(&self) -> &C {
|
||||
&self.context
|
||||
}
|
||||
|
||||
/// Drain the misbehavior vector.
|
||||
pub fn drain_misbehavior(&mut self) -> hash_map::Drain<C::AuthorityId, Misbehavior<C::Digest, C::Signature>> {
|
||||
self.strategy.misbehavior.drain()
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempt to reach BFT agreement on a candidate.
|
||||
///
|
||||
/// `nodes` is the number of nodes in the system.
|
||||
|
||||
@@ -191,10 +191,21 @@ impl Context for TestContext {
|
||||
-> LocalizedMessage<Candidate, Digest, AuthorityId, Signature>
|
||||
{
|
||||
let signature = Signature(message.clone(), self.local_id.clone());
|
||||
LocalizedMessage {
|
||||
message,
|
||||
signature,
|
||||
sender: self.local_id.clone()
|
||||
|
||||
match message {
|
||||
Message::Propose(r, proposal) => LocalizedMessage::Propose(LocalizedProposal {
|
||||
round_number: r,
|
||||
digest: Digest(proposal.0),
|
||||
proposal,
|
||||
digest_signature: signature.clone(),
|
||||
full_signature: signature,
|
||||
sender: self.local_id.clone(),
|
||||
}),
|
||||
Message::Vote(vote) => LocalizedMessage::Vote(LocalizedVote {
|
||||
vote,
|
||||
signature,
|
||||
sender: self.local_id.clone(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -333,7 +344,7 @@ fn threshold_plus_one_locked_on_proposal_only_one_with_candidate() {
|
||||
round_number: locked_round,
|
||||
digest: locked_digest.clone(),
|
||||
signatures: (0..7)
|
||||
.map(|i| Signature(Message::Prepare(locked_round, locked_digest.clone()), AuthorityId(i)))
|
||||
.map(|i| Signature(Message::Vote(Vote::Prepare(locked_round, locked_digest.clone())), AuthorityId(i)))
|
||||
.collect()
|
||||
}.check(7, |_, _, s| Some(s.1.clone())).unwrap();
|
||||
|
||||
|
||||
@@ -100,6 +100,9 @@ pub type Committed = generic::Committed<Block, HeaderHash, LocalizedSignature>;
|
||||
/// Communication between BFT participants.
|
||||
pub type Communication = generic::Communication<Block, HeaderHash, AuthorityId, LocalizedSignature>;
|
||||
|
||||
/// Misbehavior observed from BFT participants.
|
||||
pub type Misbehavior = generic::Misbehavior<HeaderHash, LocalizedSignature>;
|
||||
|
||||
/// Proposer factory. Can be used to create a proposer instance.
|
||||
pub trait ProposerFactory {
|
||||
/// The proposer type this creates.
|
||||
@@ -129,6 +132,8 @@ pub trait Proposer {
|
||||
/// Evaluate proposal. True means valid.
|
||||
// TODO: change this to a future.
|
||||
fn evaluate(&self, proposal: &Block) -> Self::Evaluate;
|
||||
/// Import witnessed misbehavior.
|
||||
fn import_misbehavior(&self, misbehavior: Vec<(AuthorityId, Misbehavior)>);
|
||||
}
|
||||
|
||||
/// Block import trait.
|
||||
@@ -269,6 +274,14 @@ impl<P: Proposer, I: BlockImport> Future for BftFuture<P, I> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: Proposer, I> Drop for BftFuture<P, I> {
|
||||
fn drop(&mut self) {
|
||||
// TODO: have a trait member to pass misbehavior reports into.
|
||||
let misbehavior = self.inner.drain_misbehavior().collect::<Vec<_>>();
|
||||
self.inner.context().proposer.import_misbehavior(misbehavior);
|
||||
}
|
||||
}
|
||||
|
||||
struct AgreementHandle {
|
||||
cancel: Arc<AtomicBool>,
|
||||
task: Option<oneshot::Receiver<task::Task>>,
|
||||
@@ -317,7 +330,6 @@ impl<P, E, I> BftService<P, E, I>
|
||||
|
||||
let authorities = self.client.authorities(&BlockId::Hash(hash))?;
|
||||
|
||||
// TODO: check key is one of the authorities.
|
||||
let n = authorities.len();
|
||||
let max_faulty = max_faulty_of(n);
|
||||
|
||||
@@ -426,28 +438,49 @@ pub fn check_prepare_justification(authorities: &[AuthorityId], parent: HeaderHa
|
||||
|
||||
/// Sign a BFT message with the given key.
|
||||
pub fn sign_message(message: Message, key: &ed25519::Pair, parent_hash: HeaderHash) -> LocalizedMessage {
|
||||
let action = match message.clone() {
|
||||
::generic::Message::Propose(r, p) => PrimitiveAction::Propose(r as u32, p),
|
||||
::generic::Message::Prepare(r, h) => PrimitiveAction::Prepare(r as u32, h),
|
||||
::generic::Message::Commit(r, h) => PrimitiveAction::Commit(r as u32, h),
|
||||
::generic::Message::AdvanceRound(r) => PrimitiveAction::AdvanceRound(r as u32),
|
||||
let signer = key.public();
|
||||
|
||||
let sign_action = |action| {
|
||||
let primitive = PrimitiveMessage {
|
||||
parent: parent_hash,
|
||||
action,
|
||||
};
|
||||
|
||||
let to_sign = Slicable::encode(&primitive);
|
||||
LocalizedSignature {
|
||||
signer: signer.clone(),
|
||||
signature: key.sign(&to_sign),
|
||||
}
|
||||
};
|
||||
|
||||
let primitive = PrimitiveMessage {
|
||||
parent: parent_hash,
|
||||
action,
|
||||
};
|
||||
match message {
|
||||
::generic::Message::Propose(r, proposal) => {
|
||||
let header_hash = proposal.header.hash();
|
||||
let action_header = PrimitiveAction::ProposeHeader(r as u32, header_hash.clone());
|
||||
let action_propose = PrimitiveAction::Propose(r as u32, proposal.clone());
|
||||
|
||||
let to_sign = Slicable::encode(&primitive);
|
||||
let signature = LocalizedSignature {
|
||||
signer: key.public(),
|
||||
signature: key.sign(&to_sign),
|
||||
};
|
||||
::generic::LocalizedMessage::Propose(::generic::LocalizedProposal {
|
||||
round_number: r,
|
||||
proposal,
|
||||
digest: header_hash,
|
||||
sender: signer.0,
|
||||
digest_signature: sign_action(action_header),
|
||||
full_signature: sign_action(action_propose),
|
||||
})
|
||||
}
|
||||
::generic::Message::Vote(vote) => {
|
||||
let action = match vote {
|
||||
::generic::Vote::Prepare(r, h) => PrimitiveAction::Prepare(r as u32, h),
|
||||
::generic::Vote::Commit(r, h) => PrimitiveAction::Commit(r as u32, h),
|
||||
::generic::Vote::AdvanceRound(r) => PrimitiveAction::AdvanceRound(r as u32),
|
||||
};
|
||||
|
||||
LocalizedMessage {
|
||||
message,
|
||||
signature,
|
||||
sender: key.public().0
|
||||
::generic::LocalizedMessage::Vote(::generic::LocalizedVote {
|
||||
vote: vote,
|
||||
sender: signer.0,
|
||||
signature: sign_action(action),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -506,6 +539,8 @@ mod tests {
|
||||
fn evaluate(&self, proposal: &Block) -> Result<bool, Error> {
|
||||
Ok(proposal.header.number == self.0)
|
||||
}
|
||||
|
||||
fn import_misbehavior(&self, _misbehavior: Vec<(AuthorityId, Misbehavior)>) {}
|
||||
}
|
||||
|
||||
fn make_service(client: FakeClient, handle: Handle)
|
||||
@@ -522,6 +557,13 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn sign_vote(vote: ::generic::Vote<HeaderHash>, key: &ed25519::Pair, parent_hash: HeaderHash) -> LocalizedSignature {
|
||||
match sign_message(vote.into(), key, parent_hash) {
|
||||
::generic::LocalizedMessage::Vote(vote) => vote.signature,
|
||||
_ => panic!("signing vote leads to signed vote"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn future_gets_preempted() {
|
||||
let client = FakeClient {
|
||||
@@ -591,7 +633,7 @@ mod tests {
|
||||
digest: hash,
|
||||
round_number: 1,
|
||||
signatures: authorities_keys.iter().take(3).map(|key| {
|
||||
sign_message(generic::Message::Commit(1, hash), key, parent_hash).signature
|
||||
sign_vote(generic::Vote::Commit(1, hash).into(), key, parent_hash)
|
||||
}).collect(),
|
||||
};
|
||||
|
||||
@@ -601,7 +643,7 @@ mod tests {
|
||||
digest: hash,
|
||||
round_number: 0, // wrong round number (vs. the signatures)
|
||||
signatures: authorities_keys.iter().take(3).map(|key| {
|
||||
sign_message(generic::Message::Commit(1, hash), key, parent_hash).signature
|
||||
sign_vote(generic::Vote::Commit(1, hash).into(), key, parent_hash)
|
||||
}).collect(),
|
||||
};
|
||||
|
||||
@@ -612,7 +654,7 @@ mod tests {
|
||||
digest: hash,
|
||||
round_number: 1,
|
||||
signatures: authorities_keys.iter().take(2).map(|key| {
|
||||
sign_message(generic::Message::Commit(1, hash), key, parent_hash).signature
|
||||
sign_vote(generic::Vote::Commit(1, hash).into(), key, parent_hash)
|
||||
}).collect(),
|
||||
};
|
||||
|
||||
@@ -623,7 +665,7 @@ mod tests {
|
||||
digest: [0xfe; 32].into(),
|
||||
round_number: 1,
|
||||
signatures: authorities_keys.iter().take(3).map(|key| {
|
||||
sign_message(generic::Message::Commit(1, hash), key, parent_hash).signature
|
||||
sign_vote(generic::Vote::Commit(1, hash).into(), key, parent_hash)
|
||||
}).collect(),
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user