Skip to content
This repository has been archived by the owner on Nov 15, 2023. It is now read-only.

BEEFY: implement equivocations detection, reporting and slashing #13121

Merged
merged 47 commits into from
Feb 17, 2023
Merged
Show file tree
Hide file tree
Changes from 37 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
7d74264
client/beefy: simplify self_vote logic
acatangiu Dec 21, 2022
c366330
client/beefy: migrate to new state version
acatangiu Dec 21, 2022
a9f067e
client/beefy: detect equivocated votes
acatangiu Dec 22, 2022
4becc37
fix typos
acatangiu Jan 4, 2023
6db1632
sp-beefy: add equivocation primitives
acatangiu Jan 4, 2023
e8f50ac
client/beefy: refactor vote processing
acatangiu Jan 4, 2023
fc35fd5
fix version migration for new rounds struct
acatangiu Jan 5, 2023
800c1d9
client/beefy: track equivocations and create proofs
acatangiu Jan 5, 2023
bbc786a
client/beefy: adjust tests for new voting logic
acatangiu Jan 5, 2023
d6f6c18
sp-beefy: fix commitment ordering and equality
acatangiu Jan 5, 2023
4958928
client/beefy: simplify handle_vote() a bit
acatangiu Jan 5, 2023
acfcb1f
client/beefy: add simple equivocation test
acatangiu Jan 6, 2023
8c0d67b
client/beefy: submit equivocation proof - WIP
acatangiu Jan 9, 2023
7114074
frame/beefy: add equivocation report runtime api - part 1
acatangiu Jan 9, 2023
3219c2f
frame/beefy: report equivocation logic - part 2
acatangiu Jan 10, 2023
9278bf0
frame/beefy: add pluggable Equivocation handler - part 3
acatangiu Jan 10, 2023
db11585
frame/beefy: impl ValidateUnsigned for equivocations reporting
acatangiu Jan 10, 2023
57d6897
client/beefy: submit report equivocation unsigned extrinsic
acatangiu Jan 10, 2023
8a344cc
primitives/beefy: fix tests
acatangiu Jan 10, 2023
f6aa2e0
frame/beefy: add default weights
acatangiu Jan 10, 2023
5cf2560
frame/beefy: fix tests
acatangiu Jan 10, 2023
2c2c5d6
client/beefy: fix tests
acatangiu Jan 10, 2023
93ba33d
Merge branch 'master' of github.com:paritytech/substrate into beefy-e…
acatangiu Jan 16, 2023
9349350
frame/beefy-mmr: fix tests
acatangiu Jan 16, 2023
b5e2c21
frame/beefy: cross-check session index with equivocation report
acatangiu Jan 16, 2023
1eca5b2
sp-beefy: make test Keyring useable in pallet
acatangiu Jan 17, 2023
fccf5c7
frame/beefy: add basic equivocation test
acatangiu Jan 17, 2023
9d68c1d
frame/beefy: test verify equivocation results in slashing
acatangiu Jan 17, 2023
a08b319
frame/beefy: test report_equivocation_old_set
acatangiu Jan 17, 2023
64114b4
frame/beefy: add more equivocation tests
acatangiu Jan 18, 2023
5634805
sp-beefy: fix docs
acatangiu Jan 18, 2023
d8898a5
beefy: simplify equivocations and fix tests
acatangiu Jan 19, 2023
7388c55
client/beefy: address review comments
acatangiu Jan 19, 2023
1f52f4c
Merge remote-tracking branch 'origin/master' into beefy-equivocations
Jan 19, 2023
6ff53e5
frame/beefy: add ValidateUnsigned to test/mock runtime
acatangiu Jan 20, 2023
8f29cdb
Merge branch 'master' of github.com:paritytech/substrate into beefy-e…
acatangiu Jan 24, 2023
ab11674
client/beefy: fixes after merge master
acatangiu Jan 24, 2023
cb2bb8b
Merge branch 'master' of github.com:paritytech/substrate into beefy-e…
acatangiu Feb 6, 2023
711832b
fix missed merge damage
acatangiu Feb 6, 2023
7d62d22
Merge branch 'master' of github.com:paritytech/substrate into beefy-e…
acatangiu Feb 13, 2023
ab975f4
client/beefy: add test for reporting equivocations
acatangiu Feb 13, 2023
f6bfc81
sp-beefy: move test utils to their own file
acatangiu Feb 13, 2023
02e166e
client/beefy: add negative test for equivocation reports
acatangiu Feb 13, 2023
42ef521
sp-beefy: move back MmrRootProvider - used in polkadot-service
acatangiu Feb 13, 2023
282d18f
impl review suggestions
acatangiu Feb 16, 2023
900547f
Merge branch 'master' of github.com:paritytech/substrate into beefy-e…
acatangiu Feb 16, 2023
f070f63
client/beefy: add equivocation metrics
acatangiu Feb 16, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion client/beefy/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ sp-runtime = { version = "7.0.0", path = "../../primitives/runtime" }

[dev-dependencies]
serde = "1.0.136"
strum = { version = "0.24.1", features = ["derive"] }
tempfile = "3.1.0"
tokio = "1.22.0"
sc-block-builder = { version = "0.10.0-dev", path = "../block-builder" }
Expand Down
131 changes: 122 additions & 9 deletions client/beefy/src/aux_schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,25 +26,25 @@ use sp_blockchain::{Error as ClientError, Result as ClientResult};
use sp_runtime::traits::Block as BlockT;

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 = 1;
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);
AuxStore::insert_aux(backend, &[(VERSION_KEY, CURRENT_VERSION.encode().as_slice())], &[])
}

/// Write voter state.
pub(crate) fn write_voter_state<Block: BlockT, B: AuxStore>(
backend: &B,
state: &PersistedState<Block>,
pub(crate) fn write_voter_state<B: BlockT, BE: AuxStore>(
backend: &BE,
state: &PersistedState<B>,
) -> ClientResult<()> {
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)? {
None => Ok(None),
Some(t) => T::decode(&mut &t[..])
Expand All @@ -63,7 +63,8 @@ where

match version {
None => (),
Some(1) => return load_decode::<_, PersistedState<B>>(backend, WORKER_STATE),
Some(1) => return v1::migrate_from_version1::<B, _>(backend),
Some(2) => return load_decode::<_, PersistedState<B>>(backend, WORKER_STATE_KEY),
other =>
return Err(ClientError::Backend(format!("Unsupported BEEFY DB version: {:?}", other))),
}
Expand All @@ -72,6 +73,118 @@ where
Ok(None)
}

mod v1 {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since old aux-schema version has never been run in production I propose to actually drop this conversion and support for old version.

use super::*;
use crate::{round::RoundTracker, worker::PersistedState, Rounds};
use beefy_primitives::{
crypto::{Public, Signature},
Commitment, Payload, ValidatorSet,
};
use sp_runtime::traits::NumberFor;
use std::collections::{BTreeMap, VecDeque};

#[derive(Decode)]
struct V1RoundTracker {
self_vote: bool,
votes: BTreeMap<Public, Signature>,
}

impl Into<RoundTracker> for V1RoundTracker {
fn into(self) -> RoundTracker {
// make the compiler happy by using this deprecated field
let _ = self.self_vote;
RoundTracker::new(self.votes)
}
}

#[derive(Decode)]
struct V1Rounds<B: BlockT> {
rounds: BTreeMap<(Payload, NumberFor<B>), V1RoundTracker>,
session_start: NumberFor<B>,
validator_set: ValidatorSet<Public>,
mandatory_done: bool,
best_done: Option<NumberFor<B>>,
}

impl<B> Into<Rounds<B>> for V1Rounds<B>
where
B: BlockT,
{
fn into(self) -> Rounds<B> {
let validator_set_id = self.validator_set.id();
let rounds = self
.rounds
.into_iter()
.map(|((payload, block_number), v1_tracker)| {
(Commitment { payload, block_number, validator_set_id }, v1_tracker.into())
})
.collect();
Rounds::<B>::new_manual(
rounds,
BTreeMap::new(),
self.session_start,
self.validator_set,
self.mandatory_done,
self.best_done,
)
}
}

#[derive(Decode)]
pub(crate) struct V1VoterOracle<B: BlockT> {
sessions: VecDeque<V1Rounds<B>>,
min_block_delta: u32,
}

#[derive(Decode)]
pub(crate) struct V1PersistedState<B: BlockT> {
/// Best block we received a GRANDPA finality for.
best_grandpa_block_header: <B as BlockT>::Header,
/// Best block a BEEFY voting round has been concluded for.
best_beefy_block: NumberFor<B>,
/// Chooses which incoming votes to accept and which votes to generate.
/// Keeps track of voting seen for current and future rounds.
voting_oracle: V1VoterOracle<B>,
}

impl<B> TryInto<PersistedState<B>> for V1PersistedState<B>
where
B: BlockT,
{
type Error = ();
fn try_into(self) -> Result<PersistedState<B>, Self::Error> {
let Self { best_grandpa_block_header, best_beefy_block, voting_oracle } = self;
let V1VoterOracle { sessions, min_block_delta } = voting_oracle;
let sessions =
sessions.into_iter().map(<V1Rounds<B> as Into<Rounds<B>>>::into).collect();
PersistedState::checked_new(
best_grandpa_block_header,
best_beefy_block,
sessions,
min_block_delta,
)
.ok_or(())
}
}

pub(super) fn migrate_from_version1<B: BlockT, BE>(
backend: &BE,
) -> ClientResult<Option<PersistedState<B>>>
where
B: BlockT,
BE: Backend<B>,
{
write_current_version(backend)?;
if let Some(new_state) = load_decode::<_, V1PersistedState<B>>(backend, WORKER_STATE_KEY)?
.and_then(|old_state| old_state.try_into().ok())
{
write_voter_state(backend, &new_state)?;
return Ok(Some(new_state))
}
Ok(None)
}
}

#[cfg(test)]
pub(crate) mod tests {
use super::*;
Expand Down
7 changes: 4 additions & 3 deletions client/beefy/src/communication/gossip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ where

/// 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>) {
debug!(target: LOG_TARGET, "🥩 About to note gossip round #{}", round);
self.known_votes.write().insert(round);
Expand Down Expand Up @@ -242,9 +242,10 @@ mod tests {
use sc_network_test::Block;
use sp_keystore::{SyncCryptoStore, SyncCryptoStorePtr};

use crate::keystore::{tests::Keyring, BeefyKeystore};
use crate::keystore::BeefyKeystore;
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::*;
Expand Down
18 changes: 17 additions & 1 deletion client/beefy/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,30 @@

use std::fmt::Debug;

#[derive(Debug, thiserror::Error, PartialEq)]
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Backend: {0}")]
Backend(String),
#[error("Keystore error: {0}")]
Keystore(String),
#[error("Runtime api error: {0}")]
RuntimeApi(sp_api::ApiError),
#[error("Signature error: {0}")]
Signature(String),
#[error("Session uninitialized")]
UninitSession,
}

#[cfg(test)]
impl PartialEq for Error {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Error::Backend(s1), Error::Backend(s2)) => s1 == s2,
(Error::Keystore(s1), Error::Keystore(s2)) => s1 == s2,
(Error::RuntimeApi(_), Error::RuntimeApi(_)) => true,
(Error::Signature(s1), Error::Signature(s2)) => s1 == s2,
(Error::UninitSession, Error::UninitSession) => true,
_ => false,
}
}
}
5 changes: 3 additions & 2 deletions client/beefy/src/justification.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,13 @@ fn verify_with_validator_set<Block: BlockT>(
#[cfg(test)]
pub(crate) mod tests {
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 super::*;
use crate::{keystore::tests::Keyring, tests::make_beefy_ids};
use crate::tests::make_beefy_ids;

pub(crate) fn new_finality_proof(
block_num: NumberFor<Block>,
Expand Down
62 changes: 7 additions & 55 deletions client/beefy/src/keystore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
use sp_application_crypto::RuntimeAppPublic;
use sp_core::keccak_256;
use sp_keystore::{SyncCryptoStore, SyncCryptoStorePtr};
use sp_runtime::traits::Keccak256;

use log::warn;

Expand All @@ -30,6 +29,9 @@ use beefy_primitives::{

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
/// wrapper around [`sp_keystore::SyncCryptoStore`] and allows to customize
/// common cryptographic functionality.
Expand Down Expand Up @@ -104,7 +106,7 @@ impl BeefyKeystore {
///
/// Return `true` if the signature is authentic, `false` otherwise.
pub fn verify(public: &Public, sig: &Signature, message: &[u8]) -> bool {
BeefyAuthorityId::<Keccak256>::verify(public, sig, message)
BeefyAuthorityId::<BeefySignatureHasher>::verify(public, sig, message)
}
}

Expand All @@ -119,63 +121,13 @@ pub mod tests {
use std::sync::Arc;

use sc_keystore::LocalKeystore;
use sp_core::{ecdsa, keccak_256, Pair};
use sp_keystore::{SyncCryptoStore, SyncCryptoStorePtr};
use sp_core::{ecdsa, Pair};

use beefy_primitives::{crypto, KEY_TYPE};
use beefy_primitives::{crypto, keyring::Keyring};

use super::BeefyKeystore;
use super::*;
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 {
Arc::new(LocalKeystore::in_memory())
}
Expand Down
Loading