BEEFY: client support for detecting equivocated votes (#13285)

* client/beefy: detect equivocated votes

* client/beefy: make sure to persist state after voting

* client/beefy: drop never-used aux-schema v2 migration

* impl review suggestion

---------

Signed-off-by: Adrian Catangiu <adrian@parity.io>
This commit is contained in:
Adrian Catangiu
2023-02-06 14:42:54 +02:00
committed by GitHub
parent c000d18111
commit 8851bbb0f0
13 changed files with 368 additions and 350 deletions
+2 -1
View File
@@ -512,7 +512,6 @@ dependencies = [
"sp-mmr-primitives", "sp-mmr-primitives",
"sp-runtime", "sp-runtime",
"sp-tracing", "sp-tracing",
"strum",
"substrate-prometheus-endpoint", "substrate-prometheus-endpoint",
"substrate-test-runtime-client", "substrate-test-runtime-client",
"tempfile", "tempfile",
@@ -9734,6 +9733,7 @@ name = "sp-beefy"
version = "4.0.0-dev" version = "4.0.0-dev"
dependencies = [ dependencies = [
"array-bytes", "array-bytes",
"lazy_static",
"parity-scale-codec", "parity-scale-codec",
"scale-info", "scale-info",
"serde", "serde",
@@ -9745,6 +9745,7 @@ dependencies = [
"sp-mmr-primitives", "sp-mmr-primitives",
"sp-runtime", "sp-runtime",
"sp-std", "sp-std",
"strum",
] ]
[[package]] [[package]]
-1
View File
@@ -39,7 +39,6 @@ sp-runtime = { version = "7.0.0", path = "../../primitives/runtime" }
[dev-dependencies] [dev-dependencies]
serde = "1.0.136" serde = "1.0.136"
strum = { version = "0.24.1", features = ["derive"] }
tempfile = "3.1.0" tempfile = "3.1.0"
tokio = "1.22.0" tokio = "1.22.0"
sc-block-builder = { version = "0.10.0-dev", path = "../block-builder" } sc-block-builder = { version = "0.10.0-dev", path = "../block-builder" }
+8 -8
View File
@@ -26,25 +26,25 @@ use sp_blockchain::{Error as ClientError, Result as ClientResult};
use sp_runtime::traits::Block as BlockT; use sp_runtime::traits::Block as BlockT;
const VERSION_KEY: &[u8] = b"beefy_auxschema_version"; const VERSION_KEY: &[u8] = b"beefy_auxschema_version";
const WORKER_STATE: &[u8] = b"beefy_voter_state"; const WORKER_STATE_KEY: &[u8] = b"beefy_voter_state";
const CURRENT_VERSION: u32 = 2; const CURRENT_VERSION: u32 = 2;
pub(crate) fn write_current_version<B: AuxStore>(backend: &B) -> ClientResult<()> { pub(crate) fn write_current_version<BE: AuxStore>(backend: &BE) -> ClientResult<()> {
info!(target: LOG_TARGET, "🥩 write aux schema version {:?}", CURRENT_VERSION); info!(target: LOG_TARGET, "🥩 write aux schema version {:?}", CURRENT_VERSION);
AuxStore::insert_aux(backend, &[(VERSION_KEY, CURRENT_VERSION.encode().as_slice())], &[]) AuxStore::insert_aux(backend, &[(VERSION_KEY, CURRENT_VERSION.encode().as_slice())], &[])
} }
/// Write voter state. /// Write voter state.
pub(crate) fn write_voter_state<Block: BlockT, B: AuxStore>( pub(crate) fn write_voter_state<B: BlockT, BE: AuxStore>(
backend: &B, backend: &BE,
state: &PersistedState<Block>, state: &PersistedState<B>,
) -> ClientResult<()> { ) -> ClientResult<()> {
trace!(target: LOG_TARGET, "🥩 persisting {:?}", state); trace!(target: LOG_TARGET, "🥩 persisting {:?}", state);
backend.insert_aux(&[(WORKER_STATE, state.encode().as_slice())], &[]) AuxStore::insert_aux(backend, &[(WORKER_STATE_KEY, state.encode().as_slice())], &[])
} }
fn load_decode<B: AuxStore, T: Decode>(backend: &B, key: &[u8]) -> ClientResult<Option<T>> { fn load_decode<BE: AuxStore, T: Decode>(backend: &BE, key: &[u8]) -> ClientResult<Option<T>> {
match backend.get_aux(key)? { match backend.get_aux(key)? {
None => Ok(None), None => Ok(None),
Some(t) => T::decode(&mut &t[..]) Some(t) => T::decode(&mut &t[..])
@@ -64,7 +64,7 @@ where
match version { match version {
None => (), None => (),
Some(1) => (), // version 1 is totally obsolete and should be simply ignored Some(1) => (), // version 1 is totally obsolete and should be simply ignored
Some(2) => return load_decode::<_, PersistedState<B>>(backend, WORKER_STATE), Some(2) => return load_decode::<_, PersistedState<B>>(backend, WORKER_STATE_KEY),
other => other =>
return Err(ClientError::Backend(format!("Unsupported BEEFY DB version: {:?}", other))), return Err(ClientError::Backend(format!("Unsupported BEEFY DB version: {:?}", other))),
} }
@@ -120,7 +120,7 @@ where
/// Note a voting round. /// Note a voting round.
/// ///
/// Noting round will start a live `round`. /// Noting round will track gossiped votes for `round`.
pub(crate) fn note_round(&self, round: NumberFor<B>) { pub(crate) fn note_round(&self, round: NumberFor<B>) {
debug!(target: LOG_TARGET, "🥩 About to note gossip round #{}", round); debug!(target: LOG_TARGET, "🥩 About to note gossip round #{}", round);
self.known_votes.write().insert(round); self.known_votes.write().insert(round);
@@ -242,9 +242,10 @@ mod tests {
use sc_network_test::Block; use sc_network_test::Block;
use sp_keystore::{SyncCryptoStore, SyncCryptoStorePtr}; use sp_keystore::{SyncCryptoStore, SyncCryptoStorePtr};
use crate::keystore::{tests::Keyring, BeefyKeystore}; use crate::keystore::BeefyKeystore;
use beefy_primitives::{ use beefy_primitives::{
crypto::Signature, known_payloads, Commitment, MmrRootHash, Payload, VoteMessage, KEY_TYPE, crypto::Signature, keyring::Keyring, known_payloads, Commitment, MmrRootHash, Payload,
VoteMessage, KEY_TYPE,
}; };
use super::*; use super::*;
+3 -2
View File
@@ -81,12 +81,13 @@ fn verify_with_validator_set<Block: BlockT>(
#[cfg(test)] #[cfg(test)]
pub(crate) mod tests { pub(crate) mod tests {
use beefy_primitives::{ use beefy_primitives::{
known_payloads, Commitment, Payload, SignedCommitment, VersionedFinalityProof, keyring::Keyring, known_payloads, Commitment, Payload, SignedCommitment,
VersionedFinalityProof,
}; };
use substrate_test_runtime_client::runtime::Block; use substrate_test_runtime_client::runtime::Block;
use super::*; use super::*;
use crate::{keystore::tests::Keyring, tests::make_beefy_ids}; use crate::tests::make_beefy_ids;
pub(crate) fn new_finality_proof( pub(crate) fn new_finality_proof(
block_num: NumberFor<Block>, block_num: NumberFor<Block>,
+7 -55
View File
@@ -19,7 +19,6 @@
use sp_application_crypto::RuntimeAppPublic; use sp_application_crypto::RuntimeAppPublic;
use sp_core::keccak_256; use sp_core::keccak_256;
use sp_keystore::{SyncCryptoStore, SyncCryptoStorePtr}; use sp_keystore::{SyncCryptoStore, SyncCryptoStorePtr};
use sp_runtime::traits::Keccak256;
use log::warn; use log::warn;
@@ -30,6 +29,9 @@ use beefy_primitives::{
use crate::{error, LOG_TARGET}; use crate::{error, LOG_TARGET};
/// Hasher used for BEEFY signatures.
pub(crate) type BeefySignatureHasher = sp_runtime::traits::Keccak256;
/// A BEEFY specific keystore implemented as a `Newtype`. This is basically a /// A BEEFY specific keystore implemented as a `Newtype`. This is basically a
/// wrapper around [`sp_keystore::SyncCryptoStore`] and allows to customize /// wrapper around [`sp_keystore::SyncCryptoStore`] and allows to customize
/// common cryptographic functionality. /// common cryptographic functionality.
@@ -104,7 +106,7 @@ impl BeefyKeystore {
/// ///
/// Return `true` if the signature is authentic, `false` otherwise. /// Return `true` if the signature is authentic, `false` otherwise.
pub fn verify(public: &Public, sig: &Signature, message: &[u8]) -> bool { pub fn verify(public: &Public, sig: &Signature, message: &[u8]) -> bool {
BeefyAuthorityId::<Keccak256>::verify(public, sig, message) BeefyAuthorityId::<BeefySignatureHasher>::verify(public, sig, message)
} }
} }
@@ -119,63 +121,13 @@ pub mod tests {
use std::sync::Arc; use std::sync::Arc;
use sc_keystore::LocalKeystore; use sc_keystore::LocalKeystore;
use sp_core::{ecdsa, keccak_256, Pair}; use sp_core::{ecdsa, Pair};
use sp_keystore::{SyncCryptoStore, SyncCryptoStorePtr};
use beefy_primitives::{crypto, KEY_TYPE}; use beefy_primitives::{crypto, keyring::Keyring};
use super::BeefyKeystore; use super::*;
use crate::error::Error; use crate::error::Error;
/// Set of test accounts using [`beefy_primitives::crypto`] types.
#[allow(missing_docs)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, strum::Display, strum::EnumIter)]
pub(crate) enum Keyring {
Alice,
Bob,
Charlie,
Dave,
Eve,
Ferdie,
One,
Two,
}
impl Keyring {
/// Sign `msg`.
pub fn sign(self, msg: &[u8]) -> crypto::Signature {
let msg = keccak_256(msg);
ecdsa::Pair::from(self).sign_prehashed(&msg).into()
}
/// Return key pair.
pub fn pair(self) -> crypto::Pair {
ecdsa::Pair::from_string(self.to_seed().as_str(), None).unwrap().into()
}
/// Return public key.
pub fn public(self) -> crypto::Public {
self.pair().public()
}
/// Return seed string.
pub fn to_seed(self) -> String {
format!("//{}", self)
}
}
impl From<Keyring> for crypto::Pair {
fn from(k: Keyring) -> Self {
k.pair()
}
}
impl From<Keyring> for ecdsa::Pair {
fn from(k: Keyring) -> Self {
k.pair().into()
}
}
fn keystore() -> SyncCryptoStorePtr { fn keystore() -> SyncCryptoStorePtr {
Arc::new(LocalKeystore::in_memory()) Arc::new(LocalKeystore::in_memory())
} }
+6
View File
@@ -30,6 +30,8 @@ pub(crate) struct Metrics {
pub beefy_round_concluded: Gauge<U64>, pub beefy_round_concluded: Gauge<U64>,
/// Best block finalized by BEEFY /// Best block finalized by BEEFY
pub beefy_best_block: Gauge<U64>, pub beefy_best_block: Gauge<U64>,
/// Best block BEEFY voted on
pub beefy_best_voted: Gauge<U64>,
/// Next block BEEFY should vote on /// Next block BEEFY should vote on
pub beefy_should_vote_on: Gauge<U64>, pub beefy_should_vote_on: Gauge<U64>,
/// Number of sessions with lagging signed commitment on mandatory block /// Number of sessions with lagging signed commitment on mandatory block
@@ -61,6 +63,10 @@ impl Metrics {
Gauge::new("substrate_beefy_best_block", "Best block finalized by BEEFY")?, Gauge::new("substrate_beefy_best_block", "Best block finalized by BEEFY")?,
registry, registry,
)?, )?,
beefy_best_voted: register(
Gauge::new("substrate_beefy_best_voted", "Best block voted on by BEEFY")?,
registry,
)?,
beefy_should_vote_on: register( beefy_should_vote_on: register(
Gauge::new("substrate_beefy_should_vote_on", "Next block, BEEFY should vote on")?, Gauge::new("substrate_beefy_should_vote_on", "Next block, BEEFY should vote on")?,
registry, registry,
+200 -201
View File
@@ -19,39 +19,33 @@
use crate::LOG_TARGET; use crate::LOG_TARGET;
use beefy_primitives::{ use beefy_primitives::{
crypto::{Public, Signature}, crypto::{AuthorityId, Public, Signature},
ValidatorSet, ValidatorSetId, Commitment, SignedCommitment, ValidatorSet, ValidatorSetId, VoteMessage,
}; };
use codec::{Decode, Encode}; use codec::{Decode, Encode};
use log::{debug, trace}; use log::debug;
use sp_runtime::traits::{Block, NumberFor}; use sp_runtime::traits::{Block, NumberFor};
use std::{collections::BTreeMap, hash::Hash}; use std::collections::BTreeMap;
/// Tracks for each round which validators have voted/signed and /// Tracks for each round which validators have voted/signed and
/// whether the local `self` validator has voted/signed. /// whether the local `self` validator has voted/signed.
/// ///
/// Does not do any validation on votes or signatures, layers above need to handle that (gossip). /// Does not do any validation on votes or signatures, layers above need to handle that (gossip).
#[derive(Debug, Decode, Default, Encode, PartialEq)] #[derive(Debug, Decode, Default, Encode, PartialEq)]
struct RoundTracker { pub(crate) struct RoundTracker {
self_vote: bool,
votes: BTreeMap<Public, Signature>, votes: BTreeMap<Public, Signature>,
} }
impl RoundTracker { impl RoundTracker {
fn add_vote(&mut self, vote: (Public, Signature), self_vote: bool) -> bool { fn add_vote(&mut self, vote: (Public, Signature)) -> bool {
if self.votes.contains_key(&vote.0) { if self.votes.contains_key(&vote.0) {
return false return false
} }
self.self_vote = self.self_vote || self_vote;
self.votes.insert(vote.0, vote.1); self.votes.insert(vote.0, vote.1);
true true
} }
fn has_self_vote(&self) -> bool {
self.self_vote
}
fn is_done(&self, threshold: usize) -> bool { fn is_done(&self, threshold: usize) -> bool {
self.votes.len() >= threshold self.votes.len() >= threshold
} }
@@ -63,27 +57,37 @@ pub fn threshold(authorities: usize) -> usize {
authorities - faulty authorities - faulty
} }
#[derive(Debug, PartialEq)]
pub enum VoteImportResult<B: Block> {
Ok,
RoundConcluded(SignedCommitment<NumberFor<B>, Signature>),
Equivocation, /* TODO: (EquivocationProof<NumberFor<B>, Public, Signature>) */
Invalid,
Stale,
}
/// Keeps track of all voting rounds (block numbers) within a session. /// Keeps track of all voting rounds (block numbers) within a session.
/// Only round numbers > `best_done` are of interest, all others are considered stale. /// Only round numbers > `best_done` are of interest, all others are considered stale.
/// ///
/// Does not do any validation on votes or signatures, layers above need to handle that (gossip). /// Does not do any validation on votes or signatures, layers above need to handle that (gossip).
#[derive(Debug, Decode, Encode, PartialEq)] #[derive(Debug, Decode, Encode, PartialEq)]
pub(crate) struct Rounds<Payload, B: Block> { pub(crate) struct Rounds<B: Block> {
rounds: BTreeMap<(Payload, NumberFor<B>), RoundTracker>, rounds: BTreeMap<Commitment<NumberFor<B>>, RoundTracker>,
previous_votes: BTreeMap<(Public, NumberFor<B>), VoteMessage<NumberFor<B>, Public, Signature>>,
session_start: NumberFor<B>, session_start: NumberFor<B>,
validator_set: ValidatorSet<Public>, validator_set: ValidatorSet<Public>,
mandatory_done: bool, mandatory_done: bool,
best_done: Option<NumberFor<B>>, best_done: Option<NumberFor<B>>,
} }
impl<P, B> Rounds<P, B> impl<B> Rounds<B>
where where
P: Ord + Hash + Clone,
B: Block, B: Block,
{ {
pub(crate) fn new(session_start: NumberFor<B>, validator_set: ValidatorSet<Public>) -> Self { pub(crate) fn new(session_start: NumberFor<B>, validator_set: ValidatorSet<Public>) -> Self {
Rounds { Rounds {
rounds: BTreeMap::new(), rounds: BTreeMap::new(),
previous_votes: BTreeMap::new(),
session_start, session_start,
validator_set, validator_set,
mandatory_done: false, mandatory_done: false,
@@ -111,60 +115,77 @@ where
self.mandatory_done self.mandatory_done
} }
pub(crate) fn should_self_vote(&self, round: &(P, NumberFor<B>)) -> bool {
Some(round.1) > self.best_done &&
self.rounds.get(round).map(|tracker| !tracker.has_self_vote()).unwrap_or(true)
}
pub(crate) fn add_vote( pub(crate) fn add_vote(
&mut self, &mut self,
round: &(P, NumberFor<B>), vote: VoteMessage<NumberFor<B>, AuthorityId, Signature>,
vote: (Public, Signature), ) -> VoteImportResult<B> {
self_vote: bool, let num = vote.commitment.block_number;
) -> bool { let vote_key = (vote.id.clone(), num);
let num = round.1;
if num < self.session_start || Some(num) <= self.best_done { if num < self.session_start || Some(num) <= self.best_done {
debug!(target: LOG_TARGET, "🥩 received vote for old stale round {:?}, ignoring", num); debug!(target: LOG_TARGET, "🥩 received vote for old stale round {:?}, ignoring", num);
false return VoteImportResult::Stale
} else if !self.validators().iter().any(|id| vote.0 == *id) { } else if vote.commitment.validator_set_id != self.validator_set_id() {
debug!(
target: LOG_TARGET,
"🥩 expected set_id {:?}, ignoring vote {:?}.",
self.validator_set_id(),
vote,
);
return VoteImportResult::Invalid
} else if !self.validators().iter().any(|id| &vote.id == id) {
debug!( debug!(
target: LOG_TARGET, target: LOG_TARGET,
"🥩 received vote {:?} from validator that is not in the validator set, ignoring", "🥩 received vote {:?} from validator that is not in the validator set, ignoring",
vote vote
); );
false return VoteImportResult::Invalid
} else {
self.rounds.entry(round.clone()).or_default().add_vote(vote, self_vote)
}
} }
pub(crate) fn should_conclude( if let Some(previous_vote) = self.previous_votes.get(&vote_key) {
// is the same public key voting for a different payload?
if previous_vote.commitment.payload != vote.commitment.payload {
debug!(
target: LOG_TARGET,
"🥩 detected equivocated vote: 1st: {:?}, 2nd: {:?}", previous_vote, vote
);
// TODO: build `EquivocationProof` and return it here.
return VoteImportResult::Equivocation
}
} else {
// this is the first vote sent by `id` for `num`, all good
self.previous_votes.insert(vote_key, vote.clone());
}
// add valid vote
let round = self.rounds.entry(vote.commitment.clone()).or_default();
if round.add_vote((vote.id, vote.signature)) &&
round.is_done(threshold(self.validator_set.len()))
{
if let Some(round) = self.rounds.remove_entry(&vote.commitment) {
return VoteImportResult::RoundConcluded(self.signed_commitment(round))
}
}
VoteImportResult::Ok
}
fn signed_commitment(
&mut self, &mut self,
round: &(P, NumberFor<B>), round: (Commitment<NumberFor<B>>, RoundTracker),
) -> Option<Vec<Option<Signature>>> { ) -> SignedCommitment<NumberFor<B>, Signature> {
let done = self let votes = round.1.votes;
.rounds let signatures = self
.get(round) .validators()
.map(|tracker| tracker.is_done(threshold(self.validator_set.len())))
.unwrap_or(false);
trace!(target: LOG_TARGET, "🥩 Round #{} done: {}", round.1, done);
if done {
let signatures = self.rounds.remove(round)?.votes;
Some(
self.validators()
.iter() .iter()
.map(|authority_id| signatures.get(authority_id).cloned()) .map(|authority_id| votes.get(authority_id).cloned())
.collect(), .collect();
) SignedCommitment { commitment: round.0, signatures }
} else {
None
}
} }
pub(crate) fn conclude(&mut self, round_num: NumberFor<B>) { pub(crate) fn conclude(&mut self, round_num: NumberFor<B>) {
// Remove this and older (now stale) rounds. // Remove this and older (now stale) rounds.
self.rounds.retain(|&(_, number), _| number > round_num); self.rounds.retain(|commitment, _| commitment.block_number > round_num);
self.previous_votes.retain(|&(_, number), _| number > round_num);
self.mandatory_done = self.mandatory_done || round_num == self.session_start; self.mandatory_done = self.mandatory_done || round_num == self.session_start;
self.best_done = self.best_done.max(Some(round_num)); self.best_done = self.best_done.max(Some(round_num));
debug!(target: LOG_TARGET, "🥩 Concluded round #{}", round_num); debug!(target: LOG_TARGET, "🥩 Concluded round #{}", round_num);
@@ -174,16 +195,17 @@ where
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use sc_network_test::Block; use sc_network_test::Block;
use sp_core::H256;
use beefy_primitives::{crypto::Public, ValidatorSet}; use beefy_primitives::{
crypto::Public, keyring::Keyring, known_payloads::MMR_ROOT_ID, Commitment, Payload,
SignedCommitment, ValidatorSet, VoteMessage,
};
use super::{threshold, Block as BlockT, Hash, RoundTracker, Rounds}; use super::{threshold, Block as BlockT, RoundTracker, Rounds};
use crate::keystore::tests::Keyring; use crate::round::VoteImportResult;
impl<P, B> Rounds<P, B> impl<B> Rounds<B>
where where
P: Ord + Hash + Clone,
B: BlockT, B: BlockT,
{ {
pub(crate) fn test_set_mandatory_done(&mut self, done: bool) { pub(crate) fn test_set_mandatory_done(&mut self, done: bool) {
@@ -197,26 +219,18 @@ mod tests {
let bob_vote = (Keyring::Bob.public(), Keyring::Bob.sign(b"I am committed")); let bob_vote = (Keyring::Bob.public(), Keyring::Bob.sign(b"I am committed"));
let threshold = 2; let threshold = 2;
// self vote not added yet
assert!(!rt.has_self_vote());
// adding new vote allowed // adding new vote allowed
assert!(rt.add_vote(bob_vote.clone(), false)); assert!(rt.add_vote(bob_vote.clone()));
// adding existing vote not allowed // adding existing vote not allowed
assert!(!rt.add_vote(bob_vote, false)); assert!(!rt.add_vote(bob_vote));
// self vote still not added yet
assert!(!rt.has_self_vote());
// vote is not done // vote is not done
assert!(!rt.is_done(threshold)); assert!(!rt.is_done(threshold));
let alice_vote = (Keyring::Alice.public(), Keyring::Alice.sign(b"I am committed")); let alice_vote = (Keyring::Alice.public(), Keyring::Alice.sign(b"I am committed"));
// adding new vote (self vote this time) allowed // adding new vote (self vote this time) allowed
assert!(rt.add_vote(alice_vote, true)); assert!(rt.add_vote(alice_vote));
// self vote registered
assert!(rt.has_self_vote());
// vote is now done // vote is now done
assert!(rt.is_done(threshold)); assert!(rt.is_done(threshold));
} }
@@ -242,7 +256,7 @@ mod tests {
.unwrap(); .unwrap();
let session_start = 1u64.into(); let session_start = 1u64.into();
let rounds = Rounds::<H256, Block>::new(session_start, validators); let rounds = Rounds::<Block>::new(session_start, validators);
assert_eq!(42, rounds.validator_set_id()); assert_eq!(42, rounds.validator_set_id());
assert_eq!(1, rounds.session_start()); assert_eq!(1, rounds.session_start());
@@ -266,65 +280,56 @@ mod tests {
Default::default(), Default::default(),
) )
.unwrap(); .unwrap();
let round = (H256::from_low_u64_le(1), 1); let validator_set_id = validators.id();
let session_start = 1u64.into(); let session_start = 1u64.into();
let mut rounds = Rounds::<H256, Block>::new(session_start, validators); let mut rounds = Rounds::<Block>::new(session_start, validators);
// no self vote yet, should self vote
assert!(rounds.should_self_vote(&round));
let payload = Payload::from_single_entry(MMR_ROOT_ID, vec![]);
let block_number = 1;
let commitment = Commitment { block_number, payload, validator_set_id };
let mut vote = VoteMessage {
id: Keyring::Alice.public(),
commitment: commitment.clone(),
signature: Keyring::Alice.sign(b"I am committed"),
};
// add 1st good vote // add 1st good vote
assert!(rounds.add_vote( assert_eq!(rounds.add_vote(vote.clone()), VoteImportResult::Ok);
&round,
(Keyring::Alice.public(), Keyring::Alice.sign(b"I am committed")),
true
));
// round not concluded
assert!(rounds.should_conclude(&round).is_none());
// self vote already present, should not self vote
assert!(!rounds.should_self_vote(&round));
// double voting not allowed // double voting (same vote), ok, no effect
assert!(!rounds.add_vote( assert_eq!(rounds.add_vote(vote.clone()), VoteImportResult::Ok);
&round,
(Keyring::Alice.public(), Keyring::Alice.sign(b"I am committed")),
true
));
vote.id = Keyring::Dave.public();
vote.signature = Keyring::Dave.sign(b"I am committed");
// invalid vote (Dave is not a validator) // invalid vote (Dave is not a validator)
assert!(!rounds.add_vote( assert_eq!(rounds.add_vote(vote.clone()), VoteImportResult::Invalid);
&round,
(Keyring::Dave.public(), Keyring::Dave.sign(b"I am committed")),
false
));
assert!(rounds.should_conclude(&round).is_none());
vote.id = Keyring::Bob.public();
vote.signature = Keyring::Bob.sign(b"I am committed");
// add 2nd good vote // add 2nd good vote
assert!(rounds.add_vote( assert_eq!(rounds.add_vote(vote.clone()), VoteImportResult::Ok);
&round,
(Keyring::Bob.public(), Keyring::Bob.sign(b"I am committed")),
false
));
// round not concluded
assert!(rounds.should_conclude(&round).is_none());
// add 3rd good vote vote.id = Keyring::Charlie.public();
assert!(rounds.add_vote( vote.signature = Keyring::Charlie.sign(b"I am committed");
&round, // add 3rd good vote -> round concluded -> signatures present
(Keyring::Charlie.public(), Keyring::Charlie.sign(b"I am committed")), assert_eq!(
false rounds.add_vote(vote.clone()),
)); VoteImportResult::RoundConcluded(SignedCommitment {
// round concluded commitment,
assert!(rounds.should_conclude(&round).is_some()); signatures: vec![
rounds.conclude(round.1); Some(Keyring::Alice.sign(b"I am committed")),
Some(Keyring::Bob.sign(b"I am committed")),
Some(Keyring::Charlie.sign(b"I am committed")),
None,
]
})
);
rounds.conclude(block_number);
vote.id = Keyring::Eve.public();
vote.signature = Keyring::Eve.sign(b"I am committed");
// Eve is a validator, but round was concluded, adding vote disallowed // Eve is a validator, but round was concluded, adding vote disallowed
assert!(!rounds.add_vote( assert_eq!(rounds.add_vote(vote), VoteImportResult::Stale);
&round,
(Keyring::Eve.public(), Keyring::Eve.sign(b"I am committed")),
false
));
} }
#[test] #[test]
@@ -336,30 +341,39 @@ mod tests {
42, 42,
) )
.unwrap(); .unwrap();
let alice = (Keyring::Alice.public(), Keyring::Alice.sign(b"I am committed")); let validator_set_id = validators.id();
// active rounds starts at block 10
let session_start = 10u64.into(); let session_start = 10u64.into();
let mut rounds = Rounds::<H256, Block>::new(session_start, validators); let mut rounds = Rounds::<Block>::new(session_start, validators);
let mut vote = (H256::from_low_u64_le(1), 9); // vote on round 9
let block_number = 9;
let payload = Payload::from_single_entry(MMR_ROOT_ID, vec![]);
let commitment = Commitment { block_number, payload, validator_set_id };
let mut vote = VoteMessage {
id: Keyring::Alice.public(),
commitment,
signature: Keyring::Alice.sign(b"I am committed"),
};
// add vote for previous session, should fail // add vote for previous session, should fail
assert!(!rounds.add_vote(&vote, alice.clone(), true)); assert_eq!(rounds.add_vote(vote.clone()), VoteImportResult::Stale);
// no votes present // no votes present
assert!(rounds.rounds.is_empty()); assert!(rounds.rounds.is_empty());
// simulate 11 was concluded // simulate 11 was concluded
rounds.best_done = Some(11); rounds.best_done = Some(11);
// add votes for current session, but already concluded rounds, should fail // add votes for current session, but already concluded rounds, should fail
vote.1 = 10; vote.commitment.block_number = 10;
assert!(!rounds.add_vote(&vote, alice.clone(), true)); assert_eq!(rounds.add_vote(vote.clone()), VoteImportResult::Stale);
vote.1 = 11; vote.commitment.block_number = 11;
assert!(!rounds.add_vote(&vote, alice.clone(), true)); assert_eq!(rounds.add_vote(vote.clone()), VoteImportResult::Stale);
// no votes present // no votes present
assert!(rounds.rounds.is_empty()); assert!(rounds.rounds.is_empty());
// add good vote // add vote for active round 12
vote.1 = 12; vote.commitment.block_number = 12;
assert!(rounds.add_vote(&vote, alice, true)); assert_eq!(rounds.add_vote(vote), VoteImportResult::Ok);
// good vote present // good vote present
assert_eq!(rounds.rounds.len(), 1); assert_eq!(rounds.rounds.len(), 1);
} }
@@ -369,88 +383,73 @@ mod tests {
sp_tracing::try_init_simple(); sp_tracing::try_init_simple();
let validators = ValidatorSet::<Public>::new( let validators = ValidatorSet::<Public>::new(
vec![ vec![Keyring::Alice.public(), Keyring::Bob.public(), Keyring::Charlie.public()],
Keyring::Alice.public(),
Keyring::Bob.public(),
Keyring::Charlie.public(),
Keyring::Dave.public(),
],
Default::default(), Default::default(),
) )
.unwrap(); .unwrap();
let validator_set_id = validators.id();
let session_start = 1u64.into(); let session_start = 1u64.into();
let mut rounds = Rounds::<H256, Block>::new(session_start, validators); let mut rounds = Rounds::<Block>::new(session_start, validators);
// round 1 let payload = Payload::from_single_entry(MMR_ROOT_ID, vec![]);
assert!(rounds.add_vote( let commitment = Commitment { block_number: 1, payload, validator_set_id };
&(H256::from_low_u64_le(1), 1), let mut alice_vote = VoteMessage {
(Keyring::Alice.public(), Keyring::Alice.sign(b"I am committed")), id: Keyring::Alice.public(),
true, commitment: commitment.clone(),
)); signature: Keyring::Alice.sign(b"I am committed"),
assert!(rounds.add_vote( };
&(H256::from_low_u64_le(1), 1), let mut bob_vote = VoteMessage {
(Keyring::Bob.public(), Keyring::Bob.sign(b"I am committed")), id: Keyring::Bob.public(),
false, commitment: commitment.clone(),
)); signature: Keyring::Bob.sign(b"I am committed"),
assert!(rounds.add_vote( };
&(H256::from_low_u64_le(1), 1), let mut charlie_vote = VoteMessage {
(Keyring::Charlie.public(), Keyring::Charlie.sign(b"I am committed")), id: Keyring::Charlie.public(),
false, commitment,
)); signature: Keyring::Charlie.sign(b"I am committed"),
};
let expected_signatures = vec![
Some(Keyring::Alice.sign(b"I am committed")),
Some(Keyring::Bob.sign(b"I am committed")),
Some(Keyring::Charlie.sign(b"I am committed")),
];
// round 2 // round 1 - only 2 out of 3 vote
assert!(rounds.add_vote( assert_eq!(rounds.add_vote(alice_vote.clone()), VoteImportResult::Ok);
&(H256::from_low_u64_le(2), 2), assert_eq!(rounds.add_vote(charlie_vote.clone()), VoteImportResult::Ok);
(Keyring::Alice.public(), Keyring::Alice.sign(b"I am again committed")), // should be 1 active round
true,
));
assert!(rounds.add_vote(
&(H256::from_low_u64_le(2), 2),
(Keyring::Bob.public(), Keyring::Bob.sign(b"I am again committed")),
false,
));
assert!(rounds.add_vote(
&(H256::from_low_u64_le(2), 2),
(Keyring::Charlie.public(), Keyring::Charlie.sign(b"I am again committed")),
false,
));
// round 3
assert!(rounds.add_vote(
&(H256::from_low_u64_le(3), 3),
(Keyring::Alice.public(), Keyring::Alice.sign(b"I am still committed")),
true,
));
assert!(rounds.add_vote(
&(H256::from_low_u64_le(3), 3),
(Keyring::Bob.public(), Keyring::Bob.sign(b"I am still committed")),
false,
));
assert!(rounds.add_vote(
&(H256::from_low_u64_le(3), 3),
(Keyring::Charlie.public(), Keyring::Charlie.sign(b"I am still committed")),
false,
));
assert_eq!(3, rounds.rounds.len());
// conclude unknown round
assert!(rounds.should_conclude(&(H256::from_low_u64_le(5), 5)).is_none());
assert_eq!(3, rounds.rounds.len());
// conclude round 2
let signatures = rounds.should_conclude(&(H256::from_low_u64_le(2), 2)).unwrap();
rounds.conclude(2);
assert_eq!(1, rounds.rounds.len()); assert_eq!(1, rounds.rounds.len());
// round 2 - only Charlie votes
charlie_vote.commitment.block_number = 2;
assert_eq!(rounds.add_vote(charlie_vote.clone()), VoteImportResult::Ok);
// should be 2 active rounds
assert_eq!(2, rounds.rounds.len());
// round 3 - all validators vote -> round is concluded
alice_vote.commitment.block_number = 3;
bob_vote.commitment.block_number = 3;
charlie_vote.commitment.block_number = 3;
assert_eq!(rounds.add_vote(alice_vote.clone()), VoteImportResult::Ok);
assert_eq!(rounds.add_vote(bob_vote.clone()), VoteImportResult::Ok);
assert_eq!( assert_eq!(
signatures, rounds.add_vote(charlie_vote.clone()),
vec![ VoteImportResult::RoundConcluded(SignedCommitment {
Some(Keyring::Alice.sign(b"I am again committed")), commitment: charlie_vote.commitment,
Some(Keyring::Bob.sign(b"I am again committed")), signatures: expected_signatures
Some(Keyring::Charlie.sign(b"I am again committed")), })
None
]
); );
// should be only 2 active since this one auto-concluded
assert_eq!(2, rounds.rounds.len());
// conclude round 2
rounds.conclude(2);
// should be no more active rounds since 2 was officially concluded and round "1" is stale
assert!(rounds.rounds.is_empty());
// conclude round 3
rounds.conclude(3);
assert!(rounds.previous_votes.is_empty());
} }
} }
+1 -1
View File
@@ -26,12 +26,12 @@ use crate::{
}, },
gossip_protocol_name, gossip_protocol_name,
justification::*, justification::*,
keystore::tests::Keyring as BeefyKeyring,
load_or_init_voter_state, wait_for_runtime_pallet, BeefyRPCLinks, BeefyVoterLinks, KnownPeers, load_or_init_voter_state, wait_for_runtime_pallet, BeefyRPCLinks, BeefyVoterLinks, KnownPeers,
PersistedState, PersistedState,
}; };
use beefy_primitives::{ use beefy_primitives::{
crypto::{AuthorityId, Signature}, crypto::{AuthorityId, Signature},
keyring::Keyring as BeefyKeyring,
known_payloads, known_payloads,
mmr::MmrRootProvider, mmr::MmrRootProvider,
BeefyApi, Commitment, ConsensusLog, MmrRootHash, Payload, SignedCommitment, ValidatorSet, BeefyApi, Commitment, ConsensusLog, MmrRootHash, Payload, SignedCommitment, ValidatorSet,
+55 -76
View File
@@ -26,13 +26,13 @@ use crate::{
keystore::BeefyKeystore, keystore::BeefyKeystore,
metric_inc, metric_set, metric_inc, metric_set,
metrics::Metrics, metrics::Metrics,
round::Rounds, round::{Rounds, VoteImportResult},
BeefyVoterLinks, LOG_TARGET, BeefyVoterLinks, LOG_TARGET,
}; };
use beefy_primitives::{ use beefy_primitives::{
crypto::{AuthorityId, Signature}, crypto::{AuthorityId, Signature},
Commitment, ConsensusLog, Payload, PayloadProvider, SignedCommitment, ValidatorSet, Commitment, ConsensusLog, PayloadProvider, ValidatorSet, VersionedFinalityProof, VoteMessage,
VersionedFinalityProof, VoteMessage, BEEFY_ENGINE_ID, BEEFY_ENGINE_ID,
}; };
use codec::{Codec, Decode, Encode}; use codec::{Codec, Decode, Encode};
use futures::{stream::Fuse, FutureExt, StreamExt}; use futures::{stream::Fuse, FutureExt, StreamExt};
@@ -55,6 +55,7 @@ use std::{
marker::PhantomData, marker::PhantomData,
sync::Arc, sync::Arc,
}; };
/// Bound for the number of buffered future voting rounds. /// Bound for the number of buffered future voting rounds.
const MAX_BUFFERED_VOTE_ROUNDS: usize = 600; const MAX_BUFFERED_VOTE_ROUNDS: usize = 600;
/// Bound for the number of buffered votes per round number. /// Bound for the number of buffered votes per round number.
@@ -83,17 +84,14 @@ pub(crate) struct VoterOracle<B: Block> {
/// 3. lagging behind GRANDPA: queue has [1, N] elements, where all `mandatory_done == false`. /// 3. lagging behind GRANDPA: queue has [1, N] elements, where all `mandatory_done == false`.
/// In this state, everytime a session gets its mandatory block BEEFY finalized, it's /// In this state, everytime a session gets its mandatory block BEEFY finalized, it's
/// popped off the queue, eventually getting to state `2. up-to-date`. /// popped off the queue, eventually getting to state `2. up-to-date`.
sessions: VecDeque<Rounds<Payload, B>>, sessions: VecDeque<Rounds<B>>,
/// Min delta in block numbers between two blocks, BEEFY should vote on. /// Min delta in block numbers between two blocks, BEEFY should vote on.
min_block_delta: u32, min_block_delta: u32,
} }
impl<B: Block> VoterOracle<B> { impl<B: Block> VoterOracle<B> {
/// Verify provided `sessions` satisfies requirements, then build `VoterOracle`. /// Verify provided `sessions` satisfies requirements, then build `VoterOracle`.
pub fn checked_new( pub fn checked_new(sessions: VecDeque<Rounds<B>>, min_block_delta: u32) -> Option<Self> {
sessions: VecDeque<Rounds<Payload, B>>,
min_block_delta: u32,
) -> Option<Self> {
let mut prev_start = Zero::zero(); let mut prev_start = Zero::zero();
let mut prev_validator_id = None; let mut prev_validator_id = None;
// verifies the // verifies the
@@ -136,13 +134,13 @@ impl<B: Block> VoterOracle<B> {
// Return reference to rounds pertaining to first session in the queue. // Return reference to rounds pertaining to first session in the queue.
// Voting will always happen at the head of the queue. // Voting will always happen at the head of the queue.
fn active_rounds(&self) -> Option<&Rounds<Payload, B>> { fn active_rounds(&self) -> Option<&Rounds<B>> {
self.sessions.front() self.sessions.front()
} }
// Return mutable reference to rounds pertaining to first session in the queue. // Return mutable reference to rounds pertaining to first session in the queue.
// Voting will always happen at the head of the queue. // Voting will always happen at the head of the queue.
fn active_rounds_mut(&mut self) -> Option<&mut Rounds<Payload, B>> { fn active_rounds_mut(&mut self) -> Option<&mut Rounds<B>> {
self.sessions.front_mut() self.sessions.front_mut()
} }
@@ -157,7 +155,7 @@ impl<B: Block> VoterOracle<B> {
} }
/// Add new observed session to the Oracle. /// Add new observed session to the Oracle.
pub fn add_session(&mut self, rounds: Rounds<Payload, B>) { pub fn add_session(&mut self, rounds: Rounds<B>) {
self.sessions.push_back(rounds); self.sessions.push_back(rounds);
// Once we add a new session we can drop/prune previous session if it's been finalized. // Once we add a new session we can drop/prune previous session if it's been finalized.
self.try_prune(); self.try_prune();
@@ -264,6 +262,8 @@ pub(crate) struct PersistedState<B: Block> {
best_grandpa_block_header: <B as Block>::Header, best_grandpa_block_header: <B as Block>::Header,
/// Best block a BEEFY voting round has been concluded for. /// Best block a BEEFY voting round has been concluded for.
best_beefy_block: NumberFor<B>, best_beefy_block: NumberFor<B>,
/// Best block we voted on.
best_voted: NumberFor<B>,
/// Chooses which incoming votes to accept and which votes to generate. /// Chooses which incoming votes to accept and which votes to generate.
/// Keeps track of voting seen for current and future rounds. /// Keeps track of voting seen for current and future rounds.
voting_oracle: VoterOracle<B>, voting_oracle: VoterOracle<B>,
@@ -273,12 +273,13 @@ impl<B: Block> PersistedState<B> {
pub fn checked_new( pub fn checked_new(
grandpa_header: <B as Block>::Header, grandpa_header: <B as Block>::Header,
best_beefy: NumberFor<B>, best_beefy: NumberFor<B>,
sessions: VecDeque<Rounds<Payload, B>>, sessions: VecDeque<Rounds<B>>,
min_block_delta: u32, min_block_delta: u32,
) -> Option<Self> { ) -> Option<Self> {
VoterOracle::checked_new(sessions, min_block_delta).map(|voting_oracle| PersistedState { VoterOracle::checked_new(sessions, min_block_delta).map(|voting_oracle| PersistedState {
best_grandpa_block_header: grandpa_header, best_grandpa_block_header: grandpa_header,
best_beefy_block: best_beefy, best_beefy_block: best_beefy,
best_voted: Zero::zero(),
voting_oracle, voting_oracle,
}) })
} }
@@ -381,7 +382,7 @@ where
&self.persisted_state.voting_oracle &self.persisted_state.voting_oracle
} }
fn active_rounds(&mut self) -> Option<&Rounds<Payload, B>> { fn active_rounds(&mut self) -> Option<&Rounds<B>> {
self.persisted_state.voting_oracle.active_rounds() self.persisted_state.voting_oracle.active_rounds()
} }
@@ -486,12 +487,9 @@ where
) -> Result<(), Error> { ) -> Result<(), Error> {
let block_num = vote.commitment.block_number; let block_num = vote.commitment.block_number;
let best_grandpa = self.best_grandpa_block(); let best_grandpa = self.best_grandpa_block();
self.gossip_validator.note_round(block_num);
match self.voting_oracle().triage_round(block_num, best_grandpa)? { match self.voting_oracle().triage_round(block_num, best_grandpa)? {
RoundAction::Process => self.handle_vote( RoundAction::Process => self.handle_vote(vote)?,
(vote.commitment.payload, vote.commitment.block_number),
(vote.id, vote.signature),
false,
)?,
RoundAction::Enqueue => { RoundAction::Enqueue => {
debug!(target: LOG_TARGET, "🥩 Buffer vote for round: {:?}.", block_num); debug!(target: LOG_TARGET, "🥩 Buffer vote for round: {:?}.", block_num);
if self.pending_votes.len() < MAX_BUFFERED_VOTE_ROUNDS { if self.pending_votes.len() < MAX_BUFFERED_VOTE_ROUNDS {
@@ -546,57 +544,47 @@ where
fn handle_vote( fn handle_vote(
&mut self, &mut self,
round: (Payload, NumberFor<B>), vote: VoteMessage<NumberFor<B>, AuthorityId, Signature>,
vote: (AuthorityId, Signature),
self_vote: bool,
) -> Result<(), Error> { ) -> Result<(), Error> {
self.gossip_validator.note_round(round.1);
let rounds = self let rounds = self
.persisted_state .persisted_state
.voting_oracle .voting_oracle
.active_rounds_mut() .active_rounds_mut()
.ok_or(Error::UninitSession)?; .ok_or(Error::UninitSession)?;
if rounds.add_vote(&round, vote, self_vote) { let block_number = vote.commitment.block_number;
if let Some(signatures) = rounds.should_conclude(&round) { match rounds.add_vote(vote) {
self.gossip_validator.conclude_round(round.1); VoteImportResult::RoundConcluded(signed_commitment) => {
self.gossip_validator.conclude_round(block_number);
let block_num = round.1; metric_set!(self, beefy_round_concluded, block_number);
let commitment = Commitment {
payload: round.0,
block_number: block_num,
validator_set_id: rounds.validator_set_id(),
};
let finality_proof =
VersionedFinalityProof::V1(SignedCommitment { commitment, signatures });
metric_set!(self, beefy_round_concluded, block_num);
let finality_proof = VersionedFinalityProof::V1(signed_commitment);
info!( info!(
target: LOG_TARGET, target: LOG_TARGET,
"🥩 Round #{} concluded, finality_proof: {:?}.", round.1, finality_proof "🥩 Round #{} concluded, finality_proof: {:?}.", block_number, finality_proof
); );
// We created the `finality_proof` and know to be valid. // We created the `finality_proof` and know to be valid.
// New state is persisted after finalization. // New state is persisted after finalization.
self.finalize(finality_proof)?; self.finalize(finality_proof)?;
} else { },
let mandatory_round = self VoteImportResult::Ok => {
// Persist state after handling mandatory block vote.
if self
.voting_oracle() .voting_oracle()
.mandatory_pending() .mandatory_pending()
.map(|p| p.0 == round.1) .map(|(mandatory_num, _)| mandatory_num == block_number)
.unwrap_or(false); .unwrap_or(false)
// Persist state after handling self vote to avoid double voting in case {
// of voter restarts.
// Also persist state after handling mandatory block vote.
if self_vote || mandatory_round {
crate::aux_schema::write_voter_state(&*self.backend, &self.persisted_state) crate::aux_schema::write_voter_state(&*self.backend, &self.persisted_state)
.map_err(|e| Error::Backend(e.to_string()))?; .map_err(|e| Error::Backend(e.to_string()))?;
} }
} },
} VoteImportResult::Equivocation => {
// TODO: report returned `EquivocationProof` to chain through `pallet-beefy`.
()
},
VoteImportResult::Invalid | VoteImportResult::Stale => (),
};
Ok(()) Ok(())
} }
@@ -695,11 +683,7 @@ where
for (num, votes) in votes_to_handle.into_iter() { for (num, votes) in votes_to_handle.into_iter() {
debug!(target: LOG_TARGET, "🥩 Handle buffered votes for: {:?}.", num); debug!(target: LOG_TARGET, "🥩 Handle buffered votes for: {:?}.", num);
for v in votes.into_iter() { for v in votes.into_iter() {
if let Err(err) = self.handle_vote( if let Err(err) = self.handle_vote(v) {
(v.commitment.payload, v.commitment.block_number),
(v.id, v.signature),
false,
) {
error!(target: LOG_TARGET, "🥩 Error handling buffered vote: {}", err); error!(target: LOG_TARGET, "🥩 Error handling buffered vote: {}", err);
}; };
} }
@@ -716,8 +700,10 @@ where
.voting_target(self.best_beefy_block(), self.best_grandpa_block()) .voting_target(self.best_beefy_block(), self.best_grandpa_block())
{ {
metric_set!(self, beefy_should_vote_on, target); metric_set!(self, beefy_should_vote_on, target);
if target > self.persisted_state.best_voted {
self.do_vote(target)?; self.do_vote(target)?;
} }
}
Ok(()) Ok(())
} }
@@ -766,13 +752,6 @@ where
.voting_oracle .voting_oracle
.active_rounds_mut() .active_rounds_mut()
.ok_or(Error::UninitSession)?; .ok_or(Error::UninitSession)?;
if !rounds.should_self_vote(&(payload.clone(), target_number)) {
debug!(
target: LOG_TARGET,
"🥩 Don't double vote for block number: {:?}", target_number
);
return Ok(())
}
let (validators, validator_set_id) = (rounds.validators(), rounds.validator_set_id()); let (validators, validator_set_id) = (rounds.validators(), rounds.validator_set_id());
let authority_id = if let Some(id) = self.key_store.authority_id(validators) { let authority_id = if let Some(id) = self.key_store.authority_id(validators) {
@@ -812,17 +791,17 @@ where
debug!(target: LOG_TARGET, "🥩 Sent vote message: {:?}", message); debug!(target: LOG_TARGET, "🥩 Sent vote message: {:?}", message);
if let Err(err) = self.handle_vote( if let Err(err) = self.handle_vote(message) {
(message.commitment.payload, message.commitment.block_number),
(message.id, message.signature),
true,
) {
error!(target: LOG_TARGET, "🥩 Error handling self vote: {}", err); error!(target: LOG_TARGET, "🥩 Error handling self vote: {}", err);
} }
self.gossip_engine.gossip_message(topic::<B>(), encoded_message, false); self.gossip_engine.gossip_message(topic::<B>(), encoded_message, false);
Ok(()) // Persist state after vote to avoid double voting in case of voter restarts.
self.persisted_state.best_voted = target_number;
metric_set!(self, beefy_best_voted, target_number);
crate::aux_schema::write_voter_state(&*self.backend, &self.persisted_state)
.map_err(|e| Error::Backend(e.to_string()))
} }
fn process_new_state(&mut self) { fn process_new_state(&mut self) {
@@ -848,8 +827,7 @@ where
/// Main loop for BEEFY worker. /// Main loop for BEEFY worker.
/// ///
/// Wait for BEEFY runtime pallet to be available, then start the main async loop /// Run the main async loop which is driven by finality notifications and gossiped votes.
/// which is driven by finality notifications and gossiped votes.
pub(crate) async fn run( pub(crate) async fn run(
mut self, mut self,
mut block_import_justif: Fuse<NotificationReceiver<BeefyVersionedFinalityProof<B>>>, mut block_import_justif: Fuse<NotificationReceiver<BeefyVersionedFinalityProof<B>>>,
@@ -992,14 +970,15 @@ pub(crate) mod tests {
use super::*; use super::*;
use crate::{ use crate::{
communication::notification::{BeefyBestBlockStream, BeefyVersionedFinalityProofStream}, communication::notification::{BeefyBestBlockStream, BeefyVersionedFinalityProofStream},
keystore::tests::Keyring,
tests::{ tests::{
create_beefy_keystore, get_beefy_streams, make_beefy_ids, BeefyPeer, BeefyTestNet, create_beefy_keystore, get_beefy_streams, make_beefy_ids, BeefyPeer, BeefyTestNet,
TestApi, TestApi,
}, },
BeefyRPCLinks, KnownPeers, BeefyRPCLinks, KnownPeers,
}; };
use beefy_primitives::{known_payloads, mmr::MmrRootProvider}; use beefy_primitives::{
keyring::Keyring, known_payloads, mmr::MmrRootProvider, Payload, SignedCommitment,
};
use futures::{future::poll_fn, task::Poll}; use futures::{future::poll_fn, task::Poll};
use parking_lot::Mutex; use parking_lot::Mutex;
use sc_client_api::{Backend as BackendT, HeaderBackend}; use sc_client_api::{Backend as BackendT, HeaderBackend};
@@ -1007,7 +986,7 @@ pub(crate) mod tests {
use sc_network_test::TestNetFactory; use sc_network_test::TestNetFactory;
use sp_api::HeaderT; use sp_api::HeaderT;
use sp_blockchain::Backend as BlockchainBackendT; use sp_blockchain::Backend as BlockchainBackendT;
use sp_runtime::traits::{One, Zero}; use sp_runtime::traits::One;
use substrate_test_runtime_client::{ use substrate_test_runtime_client::{
runtime::{Block, Digest, DigestItem, Header, H256}, runtime::{Block, Digest, DigestItem, Header, H256},
Backend, Backend,
@@ -1018,7 +997,7 @@ pub(crate) mod tests {
&self.voting_oracle &self.voting_oracle
} }
pub fn active_round(&self) -> Option<&Rounds<Payload, B>> { pub fn active_round(&self) -> Option<&Rounds<B>> {
self.voting_oracle.active_rounds() self.voting_oracle.active_rounds()
} }
@@ -1032,7 +1011,7 @@ pub(crate) mod tests {
} }
impl<B: super::Block> VoterOracle<B> { impl<B: super::Block> VoterOracle<B> {
pub fn sessions(&self) -> &VecDeque<Rounds<Payload, B>> { pub fn sessions(&self) -> &VecDeque<Rounds<B>> {
&self.sessions &self.sessions
} }
} }
@@ -1090,7 +1069,7 @@ pub(crate) mod tests {
min_block_delta, min_block_delta,
) )
.unwrap(); .unwrap();
let payload_provider = MmrRootProvider::new(api); let payload_provider = MmrRootProvider::new(api.clone());
let worker_params = crate::worker::WorkerParams { let worker_params = crate::worker::WorkerParams {
backend, backend,
payload_provider, payload_provider,
+2
View File
@@ -22,6 +22,8 @@ sp-io = { version = "7.0.0", default-features = false, path = "../io" }
sp-mmr-primitives = { version = "4.0.0-dev", default-features = false, path = "../merkle-mountain-range" } sp-mmr-primitives = { version = "4.0.0-dev", default-features = false, path = "../merkle-mountain-range" }
sp-runtime = { version = "7.0.0", default-features = false, path = "../runtime" } sp-runtime = { version = "7.0.0", default-features = false, path = "../runtime" }
sp-std = { version = "5.0.0", default-features = false, path = "../std" } sp-std = { version = "5.0.0", default-features = false, path = "../std" }
strum = { version = "0.24.1", features = ["derive"] }
lazy_static = "1.4.0"
[dev-dependencies] [dev-dependencies]
array-bytes = "4.1" array-bytes = "4.1"
@@ -77,6 +77,7 @@ where
self.validator_set_id self.validator_set_id
.cmp(&other.validator_set_id) .cmp(&other.validator_set_id)
.then_with(|| self.block_number.cmp(&other.block_number)) .then_with(|| self.block_number.cmp(&other.block_number))
.then_with(|| self.payload.cmp(&other.payload))
} }
} }
+78 -1
View File
@@ -173,7 +173,7 @@ pub enum ConsensusLog<AuthorityId: Codec> {
/// ///
/// A vote message is a direct vote created by a BEEFY node on every voting round /// A vote message is a direct vote created by a BEEFY node on every voting round
/// and is gossiped to its peers. /// and is gossiped to its peers.
#[derive(Debug, Decode, Encode, TypeInfo)] #[derive(Clone, Debug, Decode, Encode, PartialEq, TypeInfo)]
pub struct VoteMessage<Number, Id, Signature> { pub struct VoteMessage<Number, Id, Signature> {
/// Commit to information extracted from a finalized block /// Commit to information extracted from a finalized block
pub commitment: Commitment<Number>, pub commitment: Commitment<Number>,
@@ -209,6 +209,83 @@ sp_api::decl_runtime_apis! {
} }
} }
#[cfg(feature = "std")]
/// Test accounts using [`crate::crypto`] types.
pub mod keyring {
use super::*;
use sp_core::{ecdsa, keccak_256, Pair};
use std::collections::HashMap;
use strum::IntoEnumIterator;
/// Set of test accounts using [`crate::crypto`] types.
#[allow(missing_docs)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, strum::Display, strum::EnumIter)]
pub enum Keyring {
Alice,
Bob,
Charlie,
Dave,
Eve,
Ferdie,
One,
Two,
}
impl Keyring {
/// Sign `msg`.
pub fn sign(self, msg: &[u8]) -> crypto::Signature {
// todo: use custom signature hashing type
let msg = keccak_256(msg);
ecdsa::Pair::from(self).sign_prehashed(&msg).into()
}
/// Return key pair.
pub fn pair(self) -> crypto::Pair {
ecdsa::Pair::from_string(self.to_seed().as_str(), None).unwrap().into()
}
/// Return public key.
pub fn public(self) -> crypto::Public {
self.pair().public()
}
/// Return seed string.
pub fn to_seed(self) -> String {
format!("//{}", self)
}
/// Get Keyring from public key.
pub fn from_public(who: &crypto::Public) -> Option<Keyring> {
Self::iter().find(|&k| &crypto::Public::from(k) == who)
}
}
lazy_static::lazy_static! {
static ref PRIVATE_KEYS: HashMap<Keyring, crypto::Pair> =
Keyring::iter().map(|i| (i, i.pair())).collect();
static ref PUBLIC_KEYS: HashMap<Keyring, crypto::Public> =
PRIVATE_KEYS.iter().map(|(&name, pair)| (name, pair.public())).collect();
}
impl From<Keyring> for crypto::Pair {
fn from(k: Keyring) -> Self {
k.pair()
}
}
impl From<Keyring> for ecdsa::Pair {
fn from(k: Keyring) -> Self {
k.pair().into()
}
}
impl From<Keyring> for crypto::Public {
fn from(k: Keyring) -> Self {
(*PUBLIC_KEYS).get(&k).cloned().unwrap()
}
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;