Add equivocation detector crate and implement clients (#2348) (#2353)

* Split FinalitySyncPipeline and SourceClient

* Move some logic to finality_base

* Add empty equivocation detection clients

* Add equivocation reporting logic to the source client

* Use convenience trait for SubstrateFinalitySyncPipeline

* Define JustificationVerificationContext for GRANDPA

* Equivocation source client: finality_verification_context()

* Equivocation source client: synced_headers_finality_info()

* reuse HeaderFinalityInfo

* Define EquivocationsFinder

* Fix spellcheck

* Address review comments

* Avoid equivocations lookup errors
This commit is contained in:
Serban Iorga
2023-08-16 08:20:09 +03:00
committed by Bastian Köcher
parent 9bfad80664
commit 48cae06a77
35 changed files with 931 additions and 375 deletions
+2 -2
View File
@@ -884,7 +884,7 @@ impl_runtime_apis! {
}
fn synced_headers_grandpa_info(
) -> Vec<bp_header_chain::HeaderGrandpaInfo<bp_rialto::Header>> {
) -> Vec<bp_header_chain::StoredHeaderGrandpaInfo<bp_rialto::Header>> {
BridgeRialtoGrandpa::synced_headers_grandpa_info()
}
}
@@ -895,7 +895,7 @@ impl_runtime_apis! {
}
fn synced_headers_grandpa_info(
) -> Vec<bp_header_chain::HeaderGrandpaInfo<bp_westend::Header>> {
) -> Vec<bp_header_chain::StoredHeaderGrandpaInfo<bp_westend::Header>> {
BridgeWestendGrandpa::synced_headers_grandpa_info()
}
}
@@ -748,7 +748,7 @@ impl_runtime_apis! {
}
fn synced_headers_grandpa_info(
) -> Vec<bp_header_chain::HeaderGrandpaInfo<bp_millau::Header>> {
) -> Vec<bp_header_chain::StoredHeaderGrandpaInfo<bp_millau::Header>> {
BridgeMillauGrandpa::synced_headers_grandpa_info()
}
}
+1 -1
View File
@@ -696,7 +696,7 @@ impl_runtime_apis! {
}
fn synced_headers_grandpa_info(
) -> Vec<bp_header_chain::HeaderGrandpaInfo<bp_millau::Header>> {
) -> Vec<bp_header_chain::StoredHeaderGrandpaInfo<bp_millau::Header>> {
BridgeMillauGrandpa::synced_headers_grandpa_info()
}
}
+25 -23
View File
@@ -40,10 +40,10 @@ pub use storage_types::StoredAuthoritySet;
use bp_header_chain::{
justification::GrandpaJustification, AuthoritySet, ChainWithGrandpa, GrandpaConsensusLogReader,
HeaderChain, HeaderGrandpaInfo, InitializationData, StoredHeaderData, StoredHeaderDataBuilder,
HeaderChain, InitializationData, StoredHeaderData, StoredHeaderDataBuilder,
StoredHeaderGrandpaInfo,
};
use bp_runtime::{BlockNumberOf, HashOf, HasherOf, HeaderId, HeaderOf, OwnedBridgeModule};
use finality_grandpa::voter_set::VoterSet;
use frame_support::{dispatch::PostDispatchInfo, ensure, DefaultNoBound};
use sp_runtime::{
traits::{Header as HeaderT, Zero},
@@ -241,9 +241,9 @@ pub mod pallet {
Self::deposit_event(Event::UpdatedBestFinalizedHeader {
number,
hash,
grandpa_info: HeaderGrandpaInfo {
justification,
authority_set: maybe_new_authority_set,
grandpa_info: StoredHeaderGrandpaInfo {
finality_proof: justification,
new_verification_context: maybe_new_authority_set,
},
});
@@ -411,7 +411,7 @@ pub mod pallet {
number: BridgedBlockNumber<T, I>,
hash: BridgedBlockHash<T, I>,
/// The Grandpa info associated to the new best finalized header.
grandpa_info: HeaderGrandpaInfo<BridgedHeader<T, I>>,
grandpa_info: StoredHeaderGrandpaInfo<BridgedHeader<T, I>>,
},
}
@@ -505,14 +505,9 @@ pub mod pallet {
) -> Result<(), sp_runtime::DispatchError> {
use bp_header_chain::justification::verify_justification;
let voter_set =
VoterSet::new(authority_set.authorities).ok_or(<Error<T, I>>::InvalidAuthoritySet)?;
let set_id = authority_set.set_id;
Ok(verify_justification::<BridgedHeader<T, I>>(
(hash, number),
set_id,
&voter_set,
&authority_set.try_into().map_err(|_| <Error<T, I>>::InvalidAuthoritySet)?,
justification,
)
.map_err(|e| {
@@ -617,7 +612,7 @@ where
<T as frame_system::Config>::RuntimeEvent: TryInto<Event<T, I>>,
{
/// Get the GRANDPA justifications accepted in the current block.
pub fn synced_headers_grandpa_info() -> Vec<HeaderGrandpaInfo<BridgedHeader<T, I>>> {
pub fn synced_headers_grandpa_info() -> Vec<StoredHeaderGrandpaInfo<BridgedHeader<T, I>>> {
frame_system::Pallet::<T>::read_events_no_consensus()
.filter_map(|event| {
if let Event::<T, I>::UpdatedBestFinalizedHeader { grandpa_info, .. } =
@@ -934,9 +929,9 @@ mod tests {
event: TestEvent::Grandpa(Event::UpdatedBestFinalizedHeader {
number: *header.number(),
hash: header.hash(),
grandpa_info: HeaderGrandpaInfo {
justification: justification.clone(),
authority_set: None,
grandpa_info: StoredHeaderGrandpaInfo {
finality_proof: justification.clone(),
new_verification_context: None,
},
}),
topics: vec![],
@@ -944,7 +939,10 @@ mod tests {
);
assert_eq!(
Pallet::<TestRuntime>::synced_headers_grandpa_info(),
vec![HeaderGrandpaInfo { justification, authority_set: None }]
vec![StoredHeaderGrandpaInfo {
finality_proof: justification,
new_verification_context: None
}]
);
})
}
@@ -1075,9 +1073,11 @@ mod tests {
event: TestEvent::Grandpa(Event::UpdatedBestFinalizedHeader {
number: *header.number(),
hash: header.hash(),
grandpa_info: HeaderGrandpaInfo {
justification: justification.clone(),
authority_set: Some(<CurrentAuthoritySet<TestRuntime>>::get().into()),
grandpa_info: StoredHeaderGrandpaInfo {
finality_proof: justification.clone(),
new_verification_context: Some(
<CurrentAuthoritySet<TestRuntime>>::get().into()
),
},
}),
topics: vec![],
@@ -1085,9 +1085,11 @@ mod tests {
);
assert_eq!(
Pallet::<TestRuntime>::synced_headers_grandpa_info(),
vec![HeaderGrandpaInfo {
justification,
authority_set: Some(<CurrentAuthoritySet<TestRuntime>>::get().into()),
vec![StoredHeaderGrandpaInfo {
finality_proof: justification,
new_verification_context: Some(
<CurrentAuthoritySet<TestRuntime>>::get().into()
),
}]
);
})
+13 -13
View File
@@ -697,7 +697,7 @@ pub(crate) mod tests {
use bp_test_utils::prepare_parachain_heads_proof;
use codec::Encode;
use bp_header_chain::{justification::GrandpaJustification, HeaderGrandpaInfo};
use bp_header_chain::{justification::GrandpaJustification, StoredHeaderGrandpaInfo};
use bp_parachains::{
BestParaHeadHash, BridgeParachainCall, ImportedParaHeadsKeyProvider, ParasInfoKeyProvider,
};
@@ -1036,9 +1036,9 @@ pub(crate) mod tests {
pallet_bridge_grandpa::Event::UpdatedBestFinalizedHeader {
number: 1,
hash: relay_1_hash,
grandpa_info: HeaderGrandpaInfo {
justification,
authority_set: None,
grandpa_info: StoredHeaderGrandpaInfo {
finality_proof: justification,
new_verification_context: None,
},
}
),
@@ -1177,9 +1177,9 @@ pub(crate) mod tests {
pallet_bridge_grandpa::Event::UpdatedBestFinalizedHeader {
number: 1,
hash: relay_1_hash,
grandpa_info: HeaderGrandpaInfo {
justification,
authority_set: None,
grandpa_info: StoredHeaderGrandpaInfo {
finality_proof: justification,
new_verification_context: None,
}
}
),
@@ -1230,9 +1230,9 @@ pub(crate) mod tests {
pallet_bridge_grandpa::Event::UpdatedBestFinalizedHeader {
number: 1,
hash: relay_1_hash,
grandpa_info: HeaderGrandpaInfo {
justification: justification.clone(),
authority_set: None,
grandpa_info: StoredHeaderGrandpaInfo {
finality_proof: justification.clone(),
new_verification_context: None,
}
}
),
@@ -1271,9 +1271,9 @@ pub(crate) mod tests {
pallet_bridge_grandpa::Event::UpdatedBestFinalizedHeader {
number: 1,
hash: relay_1_hash,
grandpa_info: HeaderGrandpaInfo {
justification,
authority_set: None,
grandpa_info: StoredHeaderGrandpaInfo {
finality_proof: justification,
new_verification_context: None,
}
}
),
@@ -23,10 +23,11 @@ mod verification;
use crate::ChainWithGrandpa;
pub use verification::{
equivocation::{EquivocationsCollector, Error as EquivocationsCollectorError},
equivocation::{EquivocationsCollector, GrandpaEquivocationsFinder},
optimizer::verify_and_optimize_justification,
strict::verify_justification,
AncestryChain, Error as JustificationVerificationError, PrecommitError,
AncestryChain, Error as JustificationVerificationError, JustificationVerificationContext,
PrecommitError,
};
use bp_runtime::{BlockNumberOf, Chain, HashOf, HeaderId};
@@ -16,33 +16,26 @@
//! Logic for extracting equivocations from multiple GRANDPA Finality Proofs.
use crate::justification::{
verification::{
Error as JustificationVerificationError, JustificationVerifier, PrecommitError,
SignedPrecommit,
use crate::{
justification::{
verification::{
Error as JustificationVerificationError, IterationFlow,
JustificationVerificationContext, JustificationVerifier, PrecommitError,
SignedPrecommit,
},
GrandpaJustification,
},
GrandpaJustification,
ChainWithGrandpa, FindEquivocations,
};
use crate::justification::verification::IterationFlow;
use finality_grandpa::voter_set::VoterSet;
use frame_support::RuntimeDebug;
use sp_consensus_grandpa::{AuthorityId, AuthoritySignature, EquivocationProof, Precommit, SetId};
use bp_runtime::{BlockNumberOf, HashOf, HeaderOf};
use sp_consensus_grandpa::{AuthorityId, AuthoritySignature, EquivocationProof, Precommit};
use sp_runtime::traits::Header as HeaderT;
use sp_std::{
collections::{btree_map::BTreeMap, btree_set::BTreeSet},
prelude::*,
};
/// Justification verification error.
#[derive(Eq, RuntimeDebug, PartialEq)]
pub enum Error {
/// Justification is targeting unexpected round.
InvalidRound,
/// Justification verification error.
JustificationVerification(JustificationVerificationError),
}
enum AuthorityVotes<Header: HeaderT> {
SingleVote(SignedPrecommit<Header>),
Equivocation(
@@ -53,8 +46,7 @@ enum AuthorityVotes<Header: HeaderT> {
/// Structure that can extract equivocations from multiple GRANDPA justifications.
pub struct EquivocationsCollector<'a, Header: HeaderT> {
round: u64,
authorities_set_id: SetId,
authorities_set: &'a VoterSet<AuthorityId>,
context: &'a JustificationVerificationContext,
votes: BTreeMap<AuthorityId, AuthorityVotes<Header>>,
}
@@ -62,38 +54,34 @@ pub struct EquivocationsCollector<'a, Header: HeaderT> {
impl<'a, Header: HeaderT> EquivocationsCollector<'a, Header> {
/// Create a new instance of `EquivocationsCollector`.
pub fn new(
authorities_set_id: SetId,
authorities_set: &'a VoterSet<AuthorityId>,
context: &'a JustificationVerificationContext,
base_justification: &GrandpaJustification<Header>,
) -> Result<Self, Error> {
let mut checker = Self {
round: base_justification.round,
authorities_set_id,
authorities_set,
votes: BTreeMap::new(),
};
) -> Result<Self, JustificationVerificationError> {
let mut checker = Self { round: base_justification.round, context, votes: BTreeMap::new() };
checker.verify_justification(
(base_justification.commit.target_hash, base_justification.commit.target_number),
checker.context,
base_justification,
)?;
checker.parse_justification(base_justification)?;
Ok(checker)
}
/// Parse an additional justification for equivocations.
pub fn parse_justification(
&mut self,
justification: &GrandpaJustification<Header>,
) -> Result<(), Error> {
// The justification should target the same round as the base justification.
if self.round != justification.round {
return Err(Error::InvalidRound)
/// Parse additional justifications for equivocations.
pub fn parse_justifications(&mut self, justifications: &[GrandpaJustification<Header>]) {
let round = self.round;
for justification in
justifications.iter().filter(|justification| round == justification.round)
{
// We ignore the Errors received here since we don't care if the proofs are valid.
// We only care about collecting equivocations.
let _ = self.verify_justification(
(justification.commit.target_hash, justification.commit.target_number),
self.context,
justification,
);
}
self.verify_justification(
(justification.commit.target_hash, justification.commit.target_number),
self.authorities_set_id,
self.authorities_set,
justification,
)
.map_err(Error::JustificationVerification)
}
/// Extract the equivocation proofs that have been collected.
@@ -102,7 +90,7 @@ impl<'a, Header: HeaderT> EquivocationsCollector<'a, Header> {
for (_authority, vote) in self.votes {
if let AuthorityVotes::Equivocation(equivocation) = vote {
equivocations.push(EquivocationProof::new(
self.authorities_set_id,
self.context.authority_set_id,
sp_consensus_grandpa::Equivocation::Precommit(equivocation),
));
}
@@ -177,3 +165,29 @@ impl<'a, Header: HeaderT> JustificationVerifier<Header> for EquivocationsCollect
Ok(())
}
}
/// Helper struct for finding equivocations in GRANDPA proofs.
pub struct GrandpaEquivocationsFinder<C>(sp_std::marker::PhantomData<C>);
impl<C: ChainWithGrandpa>
FindEquivocations<
GrandpaJustification<HeaderOf<C>>,
JustificationVerificationContext,
EquivocationProof<HashOf<C>, BlockNumberOf<C>>,
> for GrandpaEquivocationsFinder<C>
{
type Error = JustificationVerificationError;
fn find_equivocations(
verification_context: &JustificationVerificationContext,
synced_proof: &GrandpaJustification<HeaderOf<C>>,
source_proofs: &[GrandpaJustification<HeaderOf<C>>],
) -> Result<Vec<EquivocationProof<HashOf<C>, BlockNumberOf<C>>>, Self::Error> {
let mut equivocations_collector =
EquivocationsCollector::new(verification_context, synced_proof)?;
equivocations_collector.parse_justifications(source_proofs);
Ok(equivocations_collector.into_equivocation_proofs())
}
}
@@ -20,7 +20,7 @@ pub mod equivocation;
pub mod optimizer;
pub mod strict;
use crate::justification::GrandpaJustification;
use crate::{justification::GrandpaJustification, AuthoritySet};
use bp_runtime::HeaderId;
use finality_grandpa::voter_set::VoterSet;
@@ -114,6 +114,8 @@ impl<Header: HeaderT> AncestryChain<Header> {
/// Justification verification error.
#[derive(Eq, RuntimeDebug, PartialEq)]
pub enum Error {
/// Could not convert `AuthorityList` to `VoterSet`.
InvalidAuthorityList,
/// Justification is finalizing unexpected header.
InvalidJustificationTarget,
/// Error validating a precommit
@@ -141,6 +143,24 @@ pub enum PrecommitError {
UnrelatedAncestryVote,
}
/// The context needed for validating GRANDPA finality proofs.
pub struct JustificationVerificationContext {
/// The authority set used to verify the justification.
pub voter_set: VoterSet<AuthorityId>,
/// The ID of the authority set used to verify the justification.
pub authority_set_id: SetId,
}
impl TryFrom<AuthoritySet> for JustificationVerificationContext {
type Error = Error;
fn try_from(authority_set: AuthoritySet) -> Result<Self, Self::Error> {
let voter_set =
VoterSet::new(authority_set.authorities).ok_or(Error::InvalidAuthorityList)?;
Ok(JustificationVerificationContext { voter_set, authority_set_id: authority_set.set_id })
}
}
enum IterationFlow {
Run,
Skip,
@@ -185,8 +205,7 @@ trait JustificationVerifier<Header: HeaderT> {
fn verify_justification(
&mut self,
finalized_target: (Header::Hash, Header::Number),
authorities_set_id: SetId,
authorities_set: &VoterSet<AuthorityId>,
context: &JustificationVerificationContext,
justification: &GrandpaJustification<Header>,
) -> Result<(), Error> {
// ensure that it is justification for the expected header
@@ -196,7 +215,7 @@ trait JustificationVerifier<Header: HeaderT> {
return Err(Error::InvalidJustificationTarget)
}
let threshold = authorities_set.threshold().get();
let threshold = context.voter_set.threshold().get();
let mut chain = AncestryChain::new(justification);
let mut signature_buffer = Vec::new();
let mut cumulative_weight = 0u64;
@@ -211,7 +230,7 @@ trait JustificationVerifier<Header: HeaderT> {
}
// authority must be in the set
let authority_info = match authorities_set.get(&signed.id) {
let authority_info = match context.voter_set.get(&signed.id) {
Some(authority_info) => {
// The implementer may want to do extra checks here.
// For example to see if the authority has already voted in the same round.
@@ -248,7 +267,7 @@ trait JustificationVerifier<Header: HeaderT> {
&signed.id,
&signed.signature,
justification.round,
authorities_set_id,
context.authority_set_id,
&mut signature_buffer,
) {
self.process_invalid_signature_vote(precommit_idx).map_err(Error::Precommit)?;
@@ -21,9 +21,10 @@ use crate::justification::{
GrandpaJustification,
};
use crate::justification::verification::{IterationFlow, SignedPrecommit};
use finality_grandpa::voter_set::VoterSet;
use sp_consensus_grandpa::{AuthorityId, SetId};
use crate::justification::verification::{
IterationFlow, JustificationVerificationContext, SignedPrecommit,
};
use sp_consensus_grandpa::AuthorityId;
use sp_runtime::traits::Header as HeaderT;
use sp_std::{collections::btree_set::BTreeSet, prelude::*};
@@ -111,8 +112,7 @@ impl<Header: HeaderT> JustificationVerifier<Header> for JustificationOptimizer<H
/// Verify and optimize given justification by removing unknown and duplicate votes.
pub fn verify_and_optimize_justification<Header: HeaderT>(
finalized_target: (Header::Hash, Header::Number),
authorities_set_id: SetId,
authorities_set: &VoterSet<AuthorityId>,
context: &JustificationVerificationContext,
justification: &mut GrandpaJustification<Header>,
) -> Result<(), Error> {
let mut optimizer = JustificationOptimizer {
@@ -120,12 +120,7 @@ pub fn verify_and_optimize_justification<Header: HeaderT>(
extra_precommits: vec![],
redundant_votes_ancestries: Default::default(),
};
optimizer.verify_justification(
finalized_target,
authorities_set_id,
authorities_set,
justification,
)?;
optimizer.verify_justification(finalized_target, context, justification)?;
optimizer.optimize(justification);
Ok(())
@@ -21,9 +21,10 @@ use crate::justification::{
GrandpaJustification,
};
use crate::justification::verification::{IterationFlow, SignedPrecommit};
use finality_grandpa::voter_set::VoterSet;
use sp_consensus_grandpa::{AuthorityId, SetId};
use crate::justification::verification::{
IterationFlow, JustificationVerificationContext, SignedPrecommit,
};
use sp_consensus_grandpa::AuthorityId;
use sp_runtime::traits::Header as HeaderT;
use sp_std::collections::btree_set::BTreeSet;
@@ -92,15 +93,9 @@ impl<Header: HeaderT> JustificationVerifier<Header> for StrictJustificationVerif
/// Verify that justification, that is generated by given authority set, finalizes given header.
pub fn verify_justification<Header: HeaderT>(
finalized_target: (Header::Hash, Header::Number),
authorities_set_id: SetId,
authorities_set: &VoterSet<AuthorityId>,
context: &JustificationVerificationContext,
justification: &GrandpaJustification<Header>,
) -> Result<(), Error> {
let mut verifier = StrictJustificationVerifier { votes: BTreeSet::new() };
verifier.verify_justification(
finalized_target,
authorities_set_id,
authorities_set,
justification,
)
verifier.verify_justification(finalized_target, context, justification)
}
+55 -16
View File
@@ -19,6 +19,9 @@
#![cfg_attr(not(feature = "std"), no_std)]
use crate::justification::{
GrandpaJustification, JustificationVerificationContext, JustificationVerificationError,
};
use bp_runtime::{
BasicOperatingMode, Chain, HashOf, HasherOf, HeaderOf, RawStorageProof, StorageProofChecker,
StorageProofError, UnderlyingChainProvider,
@@ -30,7 +33,7 @@ use scale_info::TypeInfo;
use serde::{Deserialize, Serialize};
use sp_consensus_grandpa::{AuthorityList, ConsensusLog, SetId, GRANDPA_ENGINE_ID};
use sp_runtime::{traits::Header as HeaderT, Digest, RuntimeDebug};
use sp_std::boxed::Box;
use sp_std::{boxed::Box, vec::Vec};
pub mod justification;
pub mod storage_keys;
@@ -172,13 +175,48 @@ impl<Number: Codec> ConsensusLogReader for GrandpaConsensusLogReader<Number> {
}
}
/// The Grandpa-related info associated to a header.
/// The finality-related info associated to a header.
#[derive(Encode, Decode, Debug, PartialEq, Clone, TypeInfo)]
pub struct HeaderGrandpaInfo<Header: HeaderT> {
/// The header justification
pub justification: justification::GrandpaJustification<Header>,
/// The authority set introduced by the header.
pub authority_set: Option<AuthoritySet>,
pub struct HeaderFinalityInfo<FinalityProof, FinalityVerificationContext> {
/// The header finality proof.
pub finality_proof: FinalityProof,
/// The new verification context introduced by the header.
pub new_verification_context: Option<FinalityVerificationContext>,
}
/// Grandpa-related info associated to a header. This info can be saved to events.
pub type StoredHeaderGrandpaInfo<Header> =
HeaderFinalityInfo<GrandpaJustification<Header>, AuthoritySet>;
/// Processed Grandpa-related info associated to a header.
pub type HeaderGrandpaInfo<Header> =
HeaderFinalityInfo<GrandpaJustification<Header>, JustificationVerificationContext>;
impl<Header: HeaderT> TryFrom<StoredHeaderGrandpaInfo<Header>> for HeaderGrandpaInfo<Header> {
type Error = JustificationVerificationError;
fn try_from(grandpa_info: StoredHeaderGrandpaInfo<Header>) -> Result<Self, Self::Error> {
Ok(Self {
finality_proof: grandpa_info.finality_proof,
new_verification_context: match grandpa_info.new_verification_context {
Some(authority_set) => Some(authority_set.try_into()?),
None => None,
},
})
}
}
/// Helper trait for finding equivocations in finality proofs.
pub trait FindEquivocations<FinalityProof, FinalityVerificationContext, EquivocationProof> {
/// The type returned when encountering an error while looking for equivocations.
type Error;
/// Find equivocations.
fn find_equivocations(
verification_context: &FinalityVerificationContext,
synced_proof: &FinalityProof,
source_proofs: &[FinalityProof],
) -> Result<Vec<EquivocationProof>, Self::Error>;
}
/// A minimized version of `pallet-bridge-grandpa::Call` that can be used without a runtime.
@@ -244,16 +282,17 @@ pub trait ChainWithGrandpa: Chain {
const AVERAGE_HEADER_SIZE_IN_JUSTIFICATION: u32;
}
/// A trait that provides the type of the underlying `ChainWithGrandpa`.
pub trait UnderlyingChainWithGrandpaProvider: UnderlyingChainProvider {
/// Underlying `ChainWithGrandpa` type.
type ChainWithGrandpa: ChainWithGrandpa;
}
impl<T> UnderlyingChainWithGrandpaProvider for T
impl<T> ChainWithGrandpa for T
where
T: UnderlyingChainProvider,
T: Chain + UnderlyingChainProvider,
T::Chain: ChainWithGrandpa,
{
type ChainWithGrandpa = T::Chain;
const WITH_CHAIN_GRANDPA_PALLET_NAME: &'static str =
<T::Chain as ChainWithGrandpa>::WITH_CHAIN_GRANDPA_PALLET_NAME;
const MAX_AUTHORITIES_COUNT: u32 = <T::Chain as ChainWithGrandpa>::MAX_AUTHORITIES_COUNT;
const REASONABLE_HEADERS_IN_JUSTIFICATON_ANCESTRY: u32 =
<T::Chain as ChainWithGrandpa>::REASONABLE_HEADERS_IN_JUSTIFICATON_ANCESTRY;
const MAX_HEADER_SIZE: u32 = <T::Chain as ChainWithGrandpa>::MAX_HEADER_SIZE;
const AVERAGE_HEADER_SIZE_IN_JUSTIFICATION: u32 =
<T::Chain as ChainWithGrandpa>::AVERAGE_HEADER_SIZE_IN_JUSTIFICATION;
}
@@ -22,14 +22,15 @@
//! but their purpose is different.
use bp_header_chain::justification::{
verify_justification, GrandpaJustification, JustificationVerificationError, PrecommitError,
verify_justification, GrandpaJustification, JustificationVerificationContext,
JustificationVerificationError, PrecommitError,
};
use bp_test_utils::{
header_id, make_justification_for_header, signed_precommit, test_header, Account,
JustificationGeneratorParams, ALICE, BOB, CHARLIE, DAVE, EVE, FERDIE, TEST_GRANDPA_SET_ID,
};
use finality_grandpa::voter_set::VoterSet;
use sp_consensus_grandpa::{AuthorityId, AuthorityWeight};
use sp_consensus_grandpa::{AuthorityId, AuthorityWeight, SetId};
use sp_runtime::traits::Header as HeaderT;
type TestHeader = sp_runtime::testing::Header;
@@ -81,6 +82,11 @@ fn full_voter_set() -> VoterSet<AuthorityId> {
VoterSet::new(full_accounts_set().iter().map(|(id, w)| (AuthorityId::from(*id), *w))).unwrap()
}
pub fn full_verification_context(set_id: SetId) -> JustificationVerificationContext {
let voter_set = full_voter_set();
JustificationVerificationContext { voter_set, authority_set_id: set_id }
}
/// Get a minimal set of accounts.
fn minimal_accounts_set() -> Vec<(Account, AuthorityWeight)> {
// there are 5 accounts in the full set => we need 2/3 + 1 accounts, which results in 4 accounts
@@ -115,8 +121,7 @@ fn same_result_when_precommit_target_has_lower_number_than_commit_target() {
assert_eq!(
verify_justification::<TestHeader>(
header_id::<TestHeader>(1),
TEST_GRANDPA_SET_ID,
&full_voter_set(),
&full_verification_context(TEST_GRANDPA_SET_ID),
&justification,
),
Err(JustificationVerificationError::Precommit(PrecommitError::UnrelatedAncestryVote)),
@@ -148,8 +153,7 @@ fn same_result_when_precommit_target_is_not_descendant_of_commit_target() {
assert_eq!(
verify_justification::<TestHeader>(
header_id::<TestHeader>(1),
TEST_GRANDPA_SET_ID,
&full_voter_set(),
&full_verification_context(TEST_GRANDPA_SET_ID),
&justification,
),
Err(JustificationVerificationError::Precommit(PrecommitError::UnrelatedAncestryVote)),
@@ -182,8 +186,7 @@ fn same_result_when_there_are_not_enough_cumulative_weight_to_finalize_commit_ta
assert_eq!(
verify_justification::<TestHeader>(
header_id::<TestHeader>(1),
TEST_GRANDPA_SET_ID,
&full_voter_set(),
&full_verification_context(TEST_GRANDPA_SET_ID),
&justification,
),
Err(JustificationVerificationError::TooLowCumulativeWeight),
@@ -220,8 +223,7 @@ fn different_result_when_justification_contains_duplicate_vote() {
assert_eq!(
verify_justification::<TestHeader>(
header_id::<TestHeader>(1),
TEST_GRANDPA_SET_ID,
&full_voter_set(),
&full_verification_context(TEST_GRANDPA_SET_ID),
&justification,
),
Err(JustificationVerificationError::Precommit(PrecommitError::DuplicateAuthorityVote)),
@@ -261,8 +263,7 @@ fn different_results_when_authority_equivocates_once_in_a_round() {
assert_eq!(
verify_justification::<TestHeader>(
header_id::<TestHeader>(1),
TEST_GRANDPA_SET_ID,
&full_voter_set(),
&full_verification_context(TEST_GRANDPA_SET_ID),
&justification,
),
Err(JustificationVerificationError::Precommit(PrecommitError::DuplicateAuthorityVote)),
@@ -314,8 +315,7 @@ fn different_results_when_authority_equivocates_twice_in_a_round() {
assert_eq!(
verify_justification::<TestHeader>(
header_id::<TestHeader>(1),
TEST_GRANDPA_SET_ID,
&full_voter_set(),
&full_verification_context(TEST_GRANDPA_SET_ID),
&justification,
),
Err(JustificationVerificationError::Precommit(PrecommitError::DuplicateAuthorityVote)),
@@ -353,8 +353,7 @@ fn different_results_when_there_are_more_than_enough_votes() {
assert_eq!(
verify_justification::<TestHeader>(
header_id::<TestHeader>(1),
TEST_GRANDPA_SET_ID,
&full_voter_set(),
&full_verification_context(TEST_GRANDPA_SET_ID),
&justification,
),
Err(JustificationVerificationError::Precommit(PrecommitError::RedundantAuthorityVote)),
@@ -394,8 +393,7 @@ fn different_results_when_there_is_a_vote_of_unknown_authority() {
assert_eq!(
verify_justification::<TestHeader>(
header_id::<TestHeader>(1),
TEST_GRANDPA_SET_ID,
&full_voter_set(),
&full_verification_context(TEST_GRANDPA_SET_ID),
&justification,
),
Err(JustificationVerificationError::Precommit(PrecommitError::UnknownAuthorityVote)),
@@ -25,19 +25,18 @@ type TestHeader = sp_runtime::testing::Header;
#[test]
fn duplicate_votes_are_not_considered_equivocations() {
let voter_set = voter_set();
let verification_context = verification_context(TEST_GRANDPA_SET_ID);
let base_justification = make_default_justification::<TestHeader>(&test_header(1));
let mut collector =
EquivocationsCollector::new(TEST_GRANDPA_SET_ID, &voter_set, &base_justification).unwrap();
collector.parse_justification(&base_justification.clone()).unwrap();
EquivocationsCollector::new(&verification_context, &base_justification).unwrap();
collector.parse_justifications(&[base_justification.clone()]);
assert_eq!(collector.into_equivocation_proofs().len(), 0);
}
#[test]
fn equivocations_are_detected_in_base_justification_redundant_votes() {
let voter_set = voter_set();
let mut base_justification = make_default_justification::<TestHeader>(&test_header(1));
let first_vote = base_justification.commit.precommits[0].clone();
@@ -49,8 +48,9 @@ fn equivocations_are_detected_in_base_justification_redundant_votes() {
);
base_justification.commit.precommits.push(equivocation.clone());
let verification_context = verification_context(TEST_GRANDPA_SET_ID);
let collector =
EquivocationsCollector::new(TEST_GRANDPA_SET_ID, &voter_set, &base_justification).unwrap();
EquivocationsCollector::new(&verification_context, &base_justification).unwrap();
assert_eq!(
collector.into_equivocation_proofs(),
@@ -80,7 +80,6 @@ fn equivocations_are_detected_in_base_justification_redundant_votes() {
#[test]
fn equivocations_are_detected_in_extra_justification_redundant_votes() {
let voter_set = voter_set();
let base_justification = make_default_justification::<TestHeader>(&test_header(1));
let first_vote = base_justification.commit.precommits[0].clone();
@@ -93,9 +92,10 @@ fn equivocations_are_detected_in_extra_justification_redundant_votes() {
);
extra_justification.commit.precommits.push(equivocation.clone());
let verification_context = verification_context(TEST_GRANDPA_SET_ID);
let mut collector =
EquivocationsCollector::new(TEST_GRANDPA_SET_ID, &voter_set, &base_justification).unwrap();
collector.parse_justification(&extra_justification).unwrap();
EquivocationsCollector::new(&verification_context, &base_justification).unwrap();
collector.parse_justifications(&[extra_justification]);
assert_eq!(
collector.into_equivocation_proofs(),
@@ -30,8 +30,7 @@ fn optimizer_does_noting_with_minimal_justification() {
let num_precommits_before = justification.commit.precommits.len();
verify_and_optimize_justification::<TestHeader>(
header_id::<TestHeader>(1),
TEST_GRANDPA_SET_ID,
&voter_set(),
&verification_context(TEST_GRANDPA_SET_ID),
&mut justification,
)
.unwrap();
@@ -53,8 +52,7 @@ fn unknown_authority_votes_are_removed_by_optimizer() {
let num_precommits_before = justification.commit.precommits.len();
verify_and_optimize_justification::<TestHeader>(
header_id::<TestHeader>(1),
TEST_GRANDPA_SET_ID,
&voter_set(),
&verification_context(TEST_GRANDPA_SET_ID),
&mut justification,
)
.unwrap();
@@ -74,8 +72,7 @@ fn duplicate_authority_votes_are_removed_by_optimizer() {
let num_precommits_before = justification.commit.precommits.len();
verify_and_optimize_justification::<TestHeader>(
header_id::<TestHeader>(1),
TEST_GRANDPA_SET_ID,
&voter_set(),
&verification_context(TEST_GRANDPA_SET_ID),
&mut justification,
)
.unwrap();
@@ -105,8 +102,7 @@ fn invalid_authority_signatures_are_removed_by_optimizer() {
let num_precommits_before = justification.commit.precommits.len();
verify_and_optimize_justification::<TestHeader>(
header_id::<TestHeader>(1),
TEST_GRANDPA_SET_ID,
&voter_set(),
&verification_context(TEST_GRANDPA_SET_ID),
&mut justification,
)
.unwrap();
@@ -128,8 +124,7 @@ fn redundant_authority_votes_are_removed_by_optimizer() {
let num_precommits_before = justification.commit.precommits.len();
verify_and_optimize_justification::<TestHeader>(
header_id::<TestHeader>(1),
TEST_GRANDPA_SET_ID,
&voter_set(),
&verification_context(TEST_GRANDPA_SET_ID),
&mut justification,
)
.unwrap();
@@ -154,8 +149,7 @@ fn unrelated_ancestry_votes_are_removed_by_optimizer() {
let num_precommits_before = justification.commit.precommits.len();
verify_and_optimize_justification::<TestHeader>(
header_id::<TestHeader>(2),
TEST_GRANDPA_SET_ID,
&voter_set(),
&verification_context(TEST_GRANDPA_SET_ID),
&mut justification,
)
.unwrap();
@@ -172,8 +166,7 @@ fn redundant_votes_ancestries_are_removed_by_optimizer() {
let num_votes_ancestries_before = justification.votes_ancestries.len();
verify_and_optimize_justification::<TestHeader>(
header_id::<TestHeader>(1),
TEST_GRANDPA_SET_ID,
&voter_set(),
&verification_context(TEST_GRANDPA_SET_ID),
&mut justification,
)
.unwrap();
@@ -17,8 +17,8 @@
//! Tests for Grandpa strict justification verifier code.
use bp_header_chain::justification::{
required_justification_precommits, verify_justification, JustificationVerificationError,
PrecommitError,
required_justification_precommits, verify_justification, JustificationVerificationContext,
JustificationVerificationError, PrecommitError,
};
use bp_test_utils::*;
@@ -40,8 +40,7 @@ fn valid_justification_accepted() {
assert_eq!(
verify_justification::<TestHeader>(
header_id::<TestHeader>(1),
TEST_GRANDPA_SET_ID,
&voter_set(),
&verification_context(TEST_GRANDPA_SET_ID),
&justification,
),
Ok(()),
@@ -65,8 +64,7 @@ fn valid_justification_accepted_with_single_fork() {
assert_eq!(
verify_justification::<TestHeader>(
header_id::<TestHeader>(1),
TEST_GRANDPA_SET_ID,
&voter_set(),
&verification_context(TEST_GRANDPA_SET_ID),
&make_justification_for_header::<TestHeader>(params)
),
Ok(()),
@@ -100,8 +98,7 @@ fn valid_justification_accepted_with_arbitrary_number_of_authorities() {
assert_eq!(
verify_justification::<TestHeader>(
header_id::<TestHeader>(1),
TEST_GRANDPA_SET_ID,
&voter_set,
&JustificationVerificationContext { voter_set, authority_set_id: TEST_GRANDPA_SET_ID },
&make_justification_for_header::<TestHeader>(params)
),
Ok(()),
@@ -113,8 +110,7 @@ fn justification_with_invalid_target_rejected() {
assert_eq!(
verify_justification::<TestHeader>(
header_id::<TestHeader>(2),
TEST_GRANDPA_SET_ID,
&voter_set(),
&verification_context(TEST_GRANDPA_SET_ID),
&make_default_justification::<TestHeader>(&test_header(1)),
),
Err(JustificationVerificationError::InvalidJustificationTarget),
@@ -129,8 +125,7 @@ fn justification_with_invalid_commit_rejected() {
assert_eq!(
verify_justification::<TestHeader>(
header_id::<TestHeader>(1),
TEST_GRANDPA_SET_ID,
&voter_set(),
&verification_context(TEST_GRANDPA_SET_ID),
&justification,
),
Err(JustificationVerificationError::TooLowCumulativeWeight),
@@ -146,8 +141,7 @@ fn justification_with_invalid_authority_signature_rejected() {
assert_eq!(
verify_justification::<TestHeader>(
header_id::<TestHeader>(1),
TEST_GRANDPA_SET_ID,
&voter_set(),
&verification_context(TEST_GRANDPA_SET_ID),
&justification,
),
Err(JustificationVerificationError::Precommit(PrecommitError::InvalidAuthoritySignature)),
@@ -162,8 +156,7 @@ fn justification_with_invalid_precommit_ancestry() {
assert_eq!(
verify_justification::<TestHeader>(
header_id::<TestHeader>(1),
TEST_GRANDPA_SET_ID,
&voter_set(),
&verification_context(TEST_GRANDPA_SET_ID),
&justification,
),
Err(JustificationVerificationError::RedundantVotesAncestries),
@@ -187,8 +180,7 @@ fn justification_is_invalid_if_we_dont_meet_threshold() {
assert_eq!(
verify_justification::<TestHeader>(
header_id::<TestHeader>(1),
TEST_GRANDPA_SET_ID,
&voter_set(),
&verification_context(TEST_GRANDPA_SET_ID),
&make_justification_for_header::<TestHeader>(params)
),
Err(JustificationVerificationError::TooLowCumulativeWeight),
+1 -1
View File
@@ -321,7 +321,7 @@ macro_rules! decl_bridge_finality_runtime_apis {
}
};
($chain: ident, grandpa) => {
decl_bridge_finality_runtime_apis!($chain, grandpa => bp_header_chain::HeaderGrandpaInfo<Header>);
decl_bridge_finality_runtime_apis!($chain, grandpa => bp_header_chain::StoredHeaderGrandpaInfo<Header>);
};
}
+7 -1
View File
@@ -16,10 +16,11 @@
//! Utilities for working with test accounts.
use bp_header_chain::{justification::JustificationVerificationContext, AuthoritySet};
use codec::Encode;
use ed25519_dalek::{Keypair, PublicKey, SecretKey, Signature};
use finality_grandpa::voter_set::VoterSet;
use sp_consensus_grandpa::{AuthorityId, AuthorityList, AuthorityWeight};
use sp_consensus_grandpa::{AuthorityId, AuthorityList, AuthorityWeight, SetId};
use sp_runtime::RuntimeDebug;
use sp_std::prelude::*;
@@ -78,6 +79,11 @@ pub fn voter_set() -> VoterSet<AuthorityId> {
VoterSet::new(authority_list()).unwrap()
}
/// Get a valid justification verification context for a GRANDPA round.
pub fn verification_context(set_id: SetId) -> JustificationVerificationContext {
AuthoritySet { authorities: authority_list(), set_id }.try_into().unwrap()
}
/// Convenience function to get a list of Grandpa authorities.
pub fn authority_list() -> AuthorityList {
test_keyring().iter().map(|(id, w)| (AuthorityId::from(*id), *w)).collect()
@@ -15,8 +15,6 @@
// along with Parity Bridges Common. If not, see <http://www.gnu.org/licenses/>.
use async_trait::async_trait;
use relay_substrate_client::{AccountIdOf, AccountKeyPairOf};
use sp_core::Pair;
use structopt::StructOpt;
use strum::{EnumString, EnumVariantNames, VariantNames};
@@ -76,10 +74,7 @@ pub enum RelayHeadersBridge {
}
#[async_trait]
trait HeadersRelayer: RelayToRelayHeadersCliBridge
where
AccountIdOf<Self::Target>: From<<AccountKeyPairOf<Self::Target> as Pair>::Public>,
{
trait HeadersRelayer: RelayToRelayHeadersCliBridge {
/// Relay headers.
async fn relay_headers(data: RelayHeaders) -> anyhow::Result<()> {
let source_client = data.source.into_client::<Self::Source>().await?;
+5 -4
View File
@@ -16,16 +16,17 @@
use crate::calls::UtilityCall;
use bp_header_chain::UnderlyingChainWithGrandpaProvider;
use bp_header_chain::ChainWithGrandpa as ChainWithGrandpaBase;
use bp_messages::MessageNonce;
use bp_runtime::{
Chain as ChainBase, ChainId, EncodedOrDecodedCall, HashOf, Parachain as ParachainBase,
TransactionEra, TransactionEraOf, UnderlyingChainProvider,
};
use codec::{Codec, Encode};
use codec::{Codec, Decode, Encode};
use jsonrpsee::core::{DeserializeOwned, Serialize};
use num_traits::Zero;
use sc_transaction_pool_api::TransactionStatus;
use scale_info::TypeInfo;
use sp_core::{storage::StorageKey, Pair};
use sp_runtime::{
generic::SignedBlock,
@@ -78,7 +79,7 @@ pub trait RelayChain: Chain {
///
/// Keep in mind that parachains are relying on relay chain GRANDPA, so they should not implement
/// this trait.
pub trait ChainWithGrandpa: Chain + UnderlyingChainWithGrandpaProvider {
pub trait ChainWithGrandpa: Chain + ChainWithGrandpaBase {
/// Name of the runtime API method that is returning the GRANDPA info associated with the
/// headers accepted by the `submit_finality_proofs` extrinsic in the queried block.
///
@@ -87,7 +88,7 @@ pub trait ChainWithGrandpa: Chain + UnderlyingChainWithGrandpaProvider {
const SYNCED_HEADERS_GRANDPA_INFO_METHOD: &'static str;
/// The type of the key owner proof used by the grandpa engine.
type KeyOwnerProof;
type KeyOwnerProof: Decode + TypeInfo + Send;
}
/// Substrate-based parachain from minimal relay-client point of view.
@@ -22,8 +22,8 @@ use crate::{
SubstrateAuthorClient, SubstrateChainClient, SubstrateFinalityClient,
SubstrateFrameSystemClient, SubstrateStateClient, SubstrateSystemClient,
},
transaction_stall_timeout, AccountKeyPairOf, ConnectionParams, Error, HashOf, HeaderIdOf,
Result, SignParam, TransactionTracker, UnsignedTransaction,
transaction_stall_timeout, AccountKeyPairOf, ChainWithGrandpa, ConnectionParams, Error, HashOf,
HeaderIdOf, Result, SignParam, TransactionTracker, UnsignedTransaction,
};
use async_std::sync::{Arc, Mutex, RwLock};
@@ -715,15 +715,16 @@ impl<C: Chain> Client<C> {
Ok(Subscription(Mutex::new(receiver)))
}
// TODO: remove warning after implementing
// https://github.com/paritytech/parity-bridges-common/issues/39
#[allow(dead_code)]
async fn generate_grandpa_key_ownership_proof(
/// Generates a proof of key ownership for the given authority in the given set.
pub async fn generate_grandpa_key_ownership_proof(
&self,
at: HashOf<C>,
set_id: sp_consensus_grandpa::SetId,
authority_id: sp_consensus_grandpa::AuthorityId,
) -> Result<Option<sp_consensus_grandpa::OpaqueKeyOwnershipProof>> {
) -> Result<Option<sp_consensus_grandpa::OpaqueKeyOwnershipProof>>
where
C: ChainWithGrandpa,
{
self.typed_state_call(
SUB_API_GRANDPA_GENERATE_KEY_OWNERSHIP_PROOF.into(),
(set_id, authority_id),
+13
View File
@@ -0,0 +1,13 @@
[package]
name = "equivocation-detector"
version = "0.1.0"
authors = ["Parity Technologies <admin@parity.io>"]
edition = "2021"
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
description = "Equivocation detector"
[dependencies]
async-trait = "0.1"
bp-header-chain = { path = "../../primitives/header-chain" }
finality-relay = { path = "../finality" }
relay-utils = { path = "../utils" }
+70
View File
@@ -0,0 +1,70 @@
// Copyright 2019-2023 Parity Technologies (UK) Ltd.
// This file is part of Parity Bridges Common.
// Parity Bridges Common is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity Bridges Common is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity Bridges Common. If not, see <http://www.gnu.org/licenses/>.
use async_trait::async_trait;
use bp_header_chain::{FindEquivocations, HeaderFinalityInfo};
use finality_relay::{FinalityPipeline, SourceClientBase};
use relay_utils::{relay_loop::Client as RelayClient, TransactionTracker};
pub trait EquivocationDetectionPipeline: FinalityPipeline {
/// Block number of the target chain.
type TargetNumber: relay_utils::BlockNumberBase;
/// The context needed for validating finality proofs.
type FinalityVerificationContext;
/// The type of the equivocation proof.
type EquivocationProof;
/// The equivocations finder.
type EquivocationsFinder: FindEquivocations<
Self::FinalityProof,
Self::FinalityVerificationContext,
Self::EquivocationProof,
>;
}
/// Source client used in equivocation detection loop.
#[async_trait]
pub trait SourceClient<P: EquivocationDetectionPipeline>: SourceClientBase<P> {
/// Transaction tracker to track submitted transactions.
type TransactionTracker: TransactionTracker;
/// Report equivocation.
async fn report_equivocation(
&self,
at: P::Hash,
equivocation: P::EquivocationProof,
) -> Result<Self::TransactionTracker, Self::Error>;
}
/// Target client used in equivocation detection loop.
#[async_trait]
pub trait TargetClient<P: EquivocationDetectionPipeline>: RelayClient {
/// Get the data stored by the target at the specified block for validating source finality
/// proofs.
async fn finality_verification_context(
&self,
at: P::TargetNumber,
) -> Result<P::FinalityVerificationContext, Self::Error>;
/// Get the finality info associated to the source headers synced with the target chain at the
/// specified block.
async fn synced_headers_finality_info(
&self,
at: P::TargetNumber,
) -> Result<
Vec<HeaderFinalityInfo<P::FinalityProof, P::FinalityVerificationContext>>,
Self::Error,
>;
}
+51
View File
@@ -0,0 +1,51 @@
// Copyright 2019-2023 Parity Technologies (UK) Ltd.
// This file is part of Parity Bridges Common.
// Parity Bridges Common is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity Bridges Common is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity Bridges Common. If not, see <http://www.gnu.org/licenses/>.
use async_trait::async_trait;
use bp_header_chain::FinalityProof;
use futures::Stream;
use relay_utils::relay_loop::Client as RelayClient;
use std::fmt::Debug;
/// Base finality pipeline.
pub trait FinalityPipeline: 'static + Clone + Debug + Send + Sync {
/// Name of the finality proofs source.
const SOURCE_NAME: &'static str;
/// Name of the finality proofs target.
const TARGET_NAME: &'static str;
/// Synced headers are identified by this hash.
type Hash: Eq + Clone + Copy + Send + Sync + Debug;
/// Synced headers are identified by this number.
type Number: relay_utils::BlockNumberBase;
/// Finality proof type.
type FinalityProof: FinalityProof<Self::Number>;
}
/// Source client used in finality related loops.
#[async_trait]
pub trait SourceClientBase<P: FinalityPipeline>: RelayClient {
/// Stream of new finality proofs. The stream is allowed to miss proofs for some
/// headers, even if those headers are mandatory.
type FinalityProofsStream: Stream<Item = P::FinalityProof> + Send + Unpin;
/// Subscribe to new finality proofs.
async fn finality_proofs(&self) -> Result<Self::FinalityProofsStream, Self::Error>;
}
/// Target client used in finality related loops.
#[async_trait]
pub trait TargetClientBase<P: FinalityPipeline>: RelayClient {}
+6 -11
View File
@@ -20,11 +20,13 @@
//! assume that the persistent proof either exists, or will eventually become available.
use crate::{
sync_loop_metrics::SyncLoopMetrics, FinalityProof, FinalitySyncPipeline, SourceHeader,
sync_loop_metrics::SyncLoopMetrics, FinalityPipeline, FinalitySyncPipeline, SourceClientBase,
SourceHeader,
};
use async_trait::async_trait;
use backoff::backoff::Backoff;
use bp_header_chain::FinalityProof;
use futures::{select, Future, FutureExt, Stream, StreamExt};
use num_traits::{One, Saturating};
use relay_utils::{
@@ -66,11 +68,7 @@ pub struct FinalitySyncParams {
/// Source client used in finality synchronization loop.
#[async_trait]
pub trait SourceClient<P: FinalitySyncPipeline>: RelayClient {
/// Stream of new finality proofs. The stream is allowed to miss proofs for some
/// headers, even if those headers are mandatory.
type FinalityProofsStream: Stream<Item = P::FinalityProof> + Send;
pub trait SourceClient<P: FinalitySyncPipeline>: SourceClientBase<P> {
/// Get best finalized block number.
async fn best_finalized_block_number(&self) -> Result<P::Number, Self::Error>;
@@ -79,9 +77,6 @@ pub trait SourceClient<P: FinalitySyncPipeline>: RelayClient {
&self,
number: P::Number,
) -> Result<(P::Header, Option<P::FinalityProof>), Self::Error>;
/// Subscribe to new finality proofs.
async fn finality_proofs(&self) -> Result<Self::FinalityProofsStream, Self::Error>;
}
/// Target client used in finality synchronization loop.
@@ -143,10 +138,10 @@ pub async fn run<P: FinalitySyncPipeline>(
pub(crate) type UnjustifiedHeaders<H> = Vec<H>;
/// Finality proofs container. Ordered by target header number.
pub(crate) type FinalityProofs<P> =
Vec<(<P as FinalitySyncPipeline>::Number, <P as FinalitySyncPipeline>::FinalityProof)>;
Vec<(<P as FinalityPipeline>::Number, <P as FinalityPipeline>::FinalityProof)>;
/// Reference to finality proofs container.
pub(crate) type FinalityProofsRef<'a, P> =
&'a [(<P as FinalitySyncPipeline>::Number, <P as FinalitySyncPipeline>::FinalityProof)];
&'a [(<P as FinalityPipeline>::Number, <P as FinalityPipeline>::FinalityProof)];
/// Error that may happen inside finality synchronization loop.
#[derive(Debug)]
@@ -26,11 +26,11 @@ use crate::{
SourceClient, TargetClient,
},
sync_loop_metrics::SyncLoopMetrics,
FinalityProof, FinalitySyncPipeline, SourceHeader,
FinalityPipeline, FinalitySyncPipeline, SourceClientBase, SourceHeader,
};
use async_trait::async_trait;
use bp_header_chain::GrandpaConsensusLogReader;
use bp_header_chain::{FinalityProof, GrandpaConsensusLogReader};
use futures::{FutureExt, Stream, StreamExt};
use parking_lot::Mutex;
use relay_utils::{
@@ -80,15 +80,18 @@ impl MaybeConnectionError for TestError {
#[derive(Debug, Clone)]
struct TestFinalitySyncPipeline;
impl FinalitySyncPipeline for TestFinalitySyncPipeline {
impl FinalityPipeline for TestFinalitySyncPipeline {
const SOURCE_NAME: &'static str = "TestSource";
const TARGET_NAME: &'static str = "TestTarget";
type Hash = TestHash;
type Number = TestNumber;
type FinalityProof = TestFinalityProof;
}
impl FinalitySyncPipeline for TestFinalitySyncPipeline {
type ConsensusLogReader = GrandpaConsensusLogReader<TestNumber>;
type Header = TestSourceHeader;
type FinalityProof = TestFinalityProof;
}
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -146,9 +149,18 @@ impl RelayClient for TestSourceClient {
}
#[async_trait]
impl SourceClient<TestFinalitySyncPipeline> for TestSourceClient {
impl SourceClientBase<TestFinalitySyncPipeline> for TestSourceClient {
type FinalityProofsStream = Pin<Box<dyn Stream<Item = TestFinalityProof> + 'static + Send>>;
async fn finality_proofs(&self) -> Result<Self::FinalityProofsStream, TestError> {
let mut data = self.data.lock();
(self.on_method_call)(&mut data);
Ok(futures::stream::iter(data.source_proofs.clone()).boxed())
}
}
#[async_trait]
impl SourceClient<TestFinalitySyncPipeline> for TestSourceClient {
async fn best_finalized_block_number(&self) -> Result<TestNumber, TestError> {
let mut data = self.data.lock();
(self.on_method_call)(&mut data);
@@ -163,12 +175,6 @@ impl SourceClient<TestFinalitySyncPipeline> for TestSourceClient {
(self.on_method_call)(&mut data);
data.source_headers.get(&number).cloned().ok_or(TestError::NonConnection)
}
async fn finality_proofs(&self) -> Result<Self::FinalityProofsStream, TestError> {
let mut data = self.data.lock();
(self.on_method_call)(&mut data);
Ok(futures::stream::iter(data.source_proofs.clone()).boxed())
}
}
#[derive(Clone)]
+4 -13
View File
@@ -20,34 +20,25 @@
//! to submit all source headers to the target node.
pub use crate::{
base::{FinalityPipeline, SourceClientBase},
finality_loop::{metrics_prefix, run, FinalitySyncParams, SourceClient, TargetClient},
sync_loop_metrics::SyncLoopMetrics,
};
use bp_header_chain::{ConsensusLogReader, FinalityProof};
use bp_header_chain::ConsensusLogReader;
use std::fmt::Debug;
mod base;
mod finality_loop;
mod finality_loop_tests;
mod sync_loop_metrics;
/// Finality proofs synchronization pipeline.
pub trait FinalitySyncPipeline: 'static + Clone + Debug + Send + Sync {
/// Name of the finality proofs source.
const SOURCE_NAME: &'static str;
/// Name of the finality proofs target.
const TARGET_NAME: &'static str;
/// Headers we're syncing are identified by this hash.
type Hash: Eq + Clone + Copy + Send + Sync + Debug;
/// Headers we're syncing are identified by this number.
type Number: relay_utils::BlockNumberBase;
pub trait FinalitySyncPipeline: FinalityPipeline {
/// A reader that can extract the consensus log from the header digest and interpret it.
type ConsensusLogReader: ConsensusLogReader;
/// Type of header that we're syncing.
type Header: SourceHeader<Self::Hash, Self::Number, Self::ConsensusLogReader>;
/// Finality proof type.
type FinalityProof: FinalityProof<Self::Number>;
}
/// Header that we're receiving from source node.
@@ -24,6 +24,7 @@ bp-polkadot-core = { path = "../../primitives/polkadot-core" }
bp-relayers = { path = "../../primitives/relayers" }
bridge-runtime-common = { path = "../../bin/runtime-common" }
equivocation-detector = { path = "../equivocation" }
finality-grandpa = { version = "0.16.2" }
finality-relay = { path = "../finality" }
parachains-relay = { path = "../parachains" }
@@ -17,30 +17,96 @@
//! Types and functions intended to ease adding of new Substrate -> Substrate
//! equivocation detection pipelines.
use crate::finality_base::SubstrateFinalityPipeline;
mod source;
mod target;
use crate::finality_base::{engine::Engine, SubstrateFinalityPipeline, SubstrateFinalityProof};
use async_trait::async_trait;
use bp_runtime::{AccountIdOf, BlockNumberOf, HashOf};
use equivocation_detector::EquivocationDetectionPipeline;
use finality_relay::FinalityPipeline;
use pallet_grandpa::{Call as GrandpaCall, Config as GrandpaConfig};
use relay_substrate_client::{AccountKeyPairOf, CallOf, Chain, ChainWithTransactions};
use sp_core::Pair;
use sp_runtime::traits::{Block, Header};
use std::marker::PhantomData;
use crate::finality_base::engine::Engine;
use async_trait::async_trait;
use bp_runtime::{BlockNumberOf, HashOf};
use pallet_grandpa::{Call as GrandpaCall, Config as GrandpaConfig};
use relay_substrate_client::CallOf;
use sp_runtime::traits::{Block, Header};
/// Convenience trait that adds bounds to `SubstrateEquivocationDetectionPipeline`.
pub trait BaseSubstrateEquivocationDetectionPipeline:
SubstrateFinalityPipeline<SourceChain = Self::BoundedSourceChain>
{
/// Bounded `SubstrateFinalityPipeline::SourceChain`.
type BoundedSourceChain: ChainWithTransactions<AccountId = Self::BoundedSourceChainAccountId>;
/// Bounded `AccountIdOf<SubstrateFinalityPipeline::SourceChain>`.
type BoundedSourceChainAccountId: From<<AccountKeyPairOf<Self::BoundedSourceChain> as Pair>::Public>
+ Send;
}
impl<T> BaseSubstrateEquivocationDetectionPipeline for T
where
T: SubstrateFinalityPipeline,
T::SourceChain: ChainWithTransactions,
AccountIdOf<T::SourceChain>: From<<AccountKeyPairOf<Self::SourceChain> as Pair>::Public>,
{
type BoundedSourceChain = T::SourceChain;
type BoundedSourceChainAccountId = AccountIdOf<T::SourceChain>;
}
/// Substrate -> Substrate equivocation detection pipeline.
#[async_trait]
pub trait SubstrateEquivocationDetectionPipeline: SubstrateFinalityPipeline {
pub trait SubstrateEquivocationDetectionPipeline:
BaseSubstrateEquivocationDetectionPipeline
{
/// How the `report_equivocation` call is built ?
type ReportEquivocationCallBuilder: ReportEquivocationCallBuilder<Self>;
}
type FinalityProoffOf<P> = <<P as SubstrateFinalityPipeline>::FinalityEngine as Engine<
<P as SubstrateFinalityPipeline>::SourceChain,
>>::FinalityProof;
type FinalityVerificationContextfOf<P> =
<<P as SubstrateFinalityPipeline>::FinalityEngine as Engine<
<P as SubstrateFinalityPipeline>::SourceChain,
>>::FinalityVerificationContext;
type EquivocationProofOf<P> = <<P as SubstrateFinalityPipeline>::FinalityEngine as Engine<
<P as SubstrateFinalityPipeline>::SourceChain,
>>::EquivocationProof;
type EquivocationsFinderOf<P> = <<P as SubstrateFinalityPipeline>::FinalityEngine as Engine<
<P as SubstrateFinalityPipeline>::SourceChain,
>>::EquivocationsFinder;
type KeyOwnerProofOf<P> = <<P as SubstrateFinalityPipeline>::FinalityEngine as Engine<
<P as SubstrateFinalityPipeline>::SourceChain,
>>::KeyOwnerProof;
/// Adapter that allows a `SubstrateEquivocationDetectionPipeline` to act as an
/// `EquivocationDetectionPipeline`.
#[derive(Clone, Debug)]
pub struct EquivocationDetectionPipelineAdapter<P: SubstrateEquivocationDetectionPipeline> {
_phantom: PhantomData<P>,
}
impl<P: SubstrateEquivocationDetectionPipeline> FinalityPipeline
for EquivocationDetectionPipelineAdapter<P>
{
const SOURCE_NAME: &'static str = P::SourceChain::NAME;
const TARGET_NAME: &'static str = P::TargetChain::NAME;
type Hash = HashOf<P::SourceChain>;
type Number = BlockNumberOf<P::SourceChain>;
type FinalityProof = SubstrateFinalityProof<P>;
}
impl<P: SubstrateEquivocationDetectionPipeline> EquivocationDetectionPipeline
for EquivocationDetectionPipelineAdapter<P>
{
type TargetNumber = BlockNumberOf<P::TargetChain>;
type FinalityVerificationContext = FinalityVerificationContextfOf<P>;
type EquivocationProof = EquivocationProofOf<P>;
type EquivocationsFinder = EquivocationsFinderOf<P>;
}
/// Different ways of building `report_equivocation` calls.
pub trait ReportEquivocationCallBuilder<P: SubstrateEquivocationDetectionPipeline> {
/// Build a `report_equivocation` call to be executed on the source chain.
@@ -0,0 +1,101 @@
// Copyright 2019-2023 Parity Technologies (UK) Ltd.
// This file is part of Parity Bridges Common.
// Parity Bridges Common is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity Bridges Common is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity Bridges Common. If not, see <http://www.gnu.org/licenses/>.
//! Default generic implementation of equivocation source for basic Substrate client.
use crate::{
equivocation::{
EquivocationDetectionPipelineAdapter, EquivocationProofOf, ReportEquivocationCallBuilder,
SubstrateEquivocationDetectionPipeline,
},
finality_base::{engine::Engine, finality_proofs, SubstrateFinalityProofsStream},
TransactionParams,
};
use async_trait::async_trait;
use bp_runtime::{HashOf, TransactionEra};
use equivocation_detector::SourceClient;
use finality_relay::SourceClientBase;
use relay_substrate_client::{
AccountKeyPairOf, Client, Error, TransactionTracker, UnsignedTransaction,
};
use relay_utils::relay_loop::Client as RelayClient;
/// Substrate node as equivocation source.
pub struct SubstrateEquivocationSource<P: SubstrateEquivocationDetectionPipeline> {
client: Client<P::SourceChain>,
transaction_params: TransactionParams<AccountKeyPairOf<P::SourceChain>>,
}
impl<P: SubstrateEquivocationDetectionPipeline> SubstrateEquivocationSource<P> {}
impl<P: SubstrateEquivocationDetectionPipeline> Clone for SubstrateEquivocationSource<P> {
fn clone(&self) -> Self {
Self { client: self.client.clone(), transaction_params: self.transaction_params.clone() }
}
}
#[async_trait]
impl<P: SubstrateEquivocationDetectionPipeline> RelayClient for SubstrateEquivocationSource<P> {
type Error = Error;
async fn reconnect(&mut self) -> Result<(), Error> {
self.client.reconnect().await
}
}
#[async_trait]
impl<P: SubstrateEquivocationDetectionPipeline>
SourceClientBase<EquivocationDetectionPipelineAdapter<P>> for SubstrateEquivocationSource<P>
{
type FinalityProofsStream = SubstrateFinalityProofsStream<P>;
async fn finality_proofs(&self) -> Result<Self::FinalityProofsStream, Error> {
finality_proofs::<P>(&self.client).await
}
}
#[async_trait]
impl<P: SubstrateEquivocationDetectionPipeline>
SourceClient<EquivocationDetectionPipelineAdapter<P>> for SubstrateEquivocationSource<P>
{
type TransactionTracker = TransactionTracker<P::SourceChain, Client<P::SourceChain>>;
async fn report_equivocation(
&self,
at: HashOf<P::SourceChain>,
equivocation: EquivocationProofOf<P>,
) -> Result<Self::TransactionTracker, Self::Error> {
let key_owner_proof =
P::FinalityEngine::generate_source_key_ownership_proof(&self.client, at, &equivocation)
.await?;
let mortality = self.transaction_params.mortality;
let call = P::ReportEquivocationCallBuilder::build_report_equivocation_call(
equivocation,
key_owner_proof,
);
self.client
.submit_and_watch_signed_extrinsic(
&self.transaction_params.signer,
move |best_block_id, transaction_nonce| {
Ok(UnsignedTransaction::new(call.into(), transaction_nonce)
.era(TransactionEra::new(best_block_id, mortality)))
},
)
.await
}
}
@@ -0,0 +1,86 @@
// Copyright 2019-2023 Parity Technologies (UK) Ltd.
// This file is part of Parity Bridges Common.
// Parity Bridges Common is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity Bridges Common is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity Bridges Common. If not, see <http://www.gnu.org/licenses/>.
//! Default generic implementation of equivocation source for basic Substrate client.
use crate::{
equivocation::{EquivocationDetectionPipelineAdapter, SubstrateEquivocationDetectionPipeline},
finality_base::engine::Engine,
};
use crate::equivocation::{FinalityProoffOf, FinalityVerificationContextfOf};
use async_trait::async_trait;
use bp_header_chain::HeaderFinalityInfo;
use bp_runtime::BlockNumberOf;
use equivocation_detector::TargetClient;
use relay_substrate_client::{Client, Error};
use relay_utils::relay_loop::Client as RelayClient;
use sp_runtime::traits::Header;
use std::marker::PhantomData;
/// Substrate node as equivocation source.
pub struct SubstrateEquivocationTarget<P: SubstrateEquivocationDetectionPipeline> {
client: Client<P::TargetChain>,
_phantom: PhantomData<P>,
}
impl<P: SubstrateEquivocationDetectionPipeline> SubstrateEquivocationTarget<P> {}
impl<P: SubstrateEquivocationDetectionPipeline> Clone for SubstrateEquivocationTarget<P> {
fn clone(&self) -> Self {
Self { client: self.client.clone(), _phantom: Default::default() }
}
}
#[async_trait]
impl<P: SubstrateEquivocationDetectionPipeline> RelayClient for SubstrateEquivocationTarget<P> {
type Error = Error;
async fn reconnect(&mut self) -> Result<(), Error> {
self.client.reconnect().await
}
}
#[async_trait]
impl<P: SubstrateEquivocationDetectionPipeline>
TargetClient<EquivocationDetectionPipelineAdapter<P>> for SubstrateEquivocationTarget<P>
{
async fn finality_verification_context(
&self,
at: BlockNumberOf<P::TargetChain>,
) -> Result<FinalityVerificationContextfOf<P>, Self::Error> {
P::FinalityEngine::finality_verification_context(
&self.client,
self.client.header_by_number(at).await?.hash(),
)
.await
}
async fn synced_headers_finality_info(
&self,
at: BlockNumberOf<P::TargetChain>,
) -> Result<
Vec<HeaderFinalityInfo<FinalityProoffOf<P>, FinalityVerificationContextfOf<P>>>,
Self::Error,
> {
P::FinalityEngine::synced_headers_finality_info(
&self.client,
self.client.header_by_number(at).await?.hash(),
)
.await
}
}
@@ -18,21 +18,18 @@
//! finality proofs synchronization pipelines.
use crate::{
finality::{
source::{SubstrateFinalityProof, SubstrateFinalitySource},
target::SubstrateFinalityTarget,
},
finality::{source::SubstrateFinalitySource, target::SubstrateFinalityTarget},
finality_base::{engine::Engine, SubstrateFinalityPipeline, SubstrateFinalityProof},
TransactionParams,
};
use crate::finality_base::{engine::Engine, SubstrateFinalityPipeline};
use async_trait::async_trait;
use bp_header_chain::justification::GrandpaJustification;
use finality_relay::FinalitySyncPipeline;
use finality_relay::{FinalityPipeline, FinalitySyncPipeline};
use pallet_bridge_grandpa::{Call as BridgeGrandpaCall, Config as BridgeGrandpaConfig};
use relay_substrate_client::{
transaction_stall_timeout, AccountIdOf, AccountKeyPairOf, BlockNumberOf, CallOf, Chain, Client,
HashOf, HeaderOf, SyncHeader,
transaction_stall_timeout, AccountIdOf, AccountKeyPairOf, BlockNumberOf, CallOf, Chain,
ChainWithTransactions, Client, HashOf, HeaderOf, SyncHeader,
};
use relay_utils::metrics::MetricsParams;
use sp_core::Pair;
@@ -48,9 +45,31 @@ pub mod target;
/// Substrate+GRANDPA based chains (good to know).
pub(crate) const RECENT_FINALITY_PROOFS_LIMIT: usize = 4096;
/// Convenience trait that adds bounds to `SubstrateFinalitySyncPipeline`.
pub trait BaseSubstrateFinalitySyncPipeline:
SubstrateFinalityPipeline<TargetChain = Self::BoundedTargetChain>
{
/// Bounded `SubstrateFinalityPipeline::TargetChain`.
type BoundedTargetChain: ChainWithTransactions<AccountId = Self::BoundedTargetChainAccountId>;
/// Bounded `AccountIdOf<SubstrateFinalityPipeline::TargetChain>`.
type BoundedTargetChainAccountId: From<<AccountKeyPairOf<Self::BoundedTargetChain> as Pair>::Public>
+ Send;
}
impl<T> BaseSubstrateFinalitySyncPipeline for T
where
T: SubstrateFinalityPipeline,
T::TargetChain: ChainWithTransactions,
AccountIdOf<T::TargetChain>: From<<AccountKeyPairOf<Self::TargetChain> as Pair>::Public>,
{
type BoundedTargetChain = T::TargetChain;
type BoundedTargetChainAccountId = AccountIdOf<T::TargetChain>;
}
/// Substrate -> Substrate finality proofs synchronization pipeline.
#[async_trait]
pub trait SubstrateFinalitySyncPipeline: SubstrateFinalityPipeline {
pub trait SubstrateFinalitySyncPipeline: BaseSubstrateFinalitySyncPipeline {
/// How submit finality proof call is built?
type SubmitFinalityProofCallBuilder: SubmitFinalityProofCallBuilder<Self>;
@@ -70,15 +89,18 @@ pub struct FinalitySyncPipelineAdapter<P: SubstrateFinalitySyncPipeline> {
_phantom: PhantomData<P>,
}
impl<P: SubstrateFinalitySyncPipeline> FinalitySyncPipeline for FinalitySyncPipelineAdapter<P> {
impl<P: SubstrateFinalitySyncPipeline> FinalityPipeline for FinalitySyncPipelineAdapter<P> {
const SOURCE_NAME: &'static str = P::SourceChain::NAME;
const TARGET_NAME: &'static str = P::TargetChain::NAME;
type Hash = HashOf<P::SourceChain>;
type Number = BlockNumberOf<P::SourceChain>;
type FinalityProof = SubstrateFinalityProof<P>;
}
impl<P: SubstrateFinalitySyncPipeline> FinalitySyncPipeline for FinalitySyncPipelineAdapter<P> {
type ConsensusLogReader = <P::FinalityEngine as Engine<P::SourceChain>>::ConsensusLogReader;
type Header = SyncHeader<HeaderOf<P::SourceChain>>;
type FinalityProof = SubstrateFinalityProof<P>;
}
/// Different ways of building `submit_finality_proof` calls.
@@ -165,10 +187,7 @@ pub async fn run<P: SubstrateFinalitySyncPipeline>(
only_mandatory_headers: bool,
transaction_params: TransactionParams<AccountKeyPairOf<P::TargetChain>>,
metrics_params: MetricsParams,
) -> anyhow::Result<()>
where
AccountIdOf<P::TargetChain>: From<<AccountKeyPairOf<P::TargetChain> as Pair>::Public>,
{
) -> anyhow::Result<()> {
log::info!(
target: "bridge",
"Starting {} -> {} finality proof relay",
@@ -18,38 +18,27 @@
use crate::{
finality::{FinalitySyncPipelineAdapter, SubstrateFinalitySyncPipeline},
finality_base::engine::Engine,
finality_base::{
engine::Engine, finality_proofs, SubstrateFinalityProof, SubstrateFinalityProofsStream,
},
};
use crate::finality_base::SubstrateFinalityPipeline;
use async_std::sync::{Arc, Mutex};
use async_trait::async_trait;
use bp_header_chain::FinalityProof;
use codec::Decode;
use finality_relay::SourceClient;
use finality_relay::{SourceClient, SourceClientBase};
use futures::{
select,
stream::{try_unfold, unfold, Stream, StreamExt, TryStreamExt},
stream::{try_unfold, Stream, StreamExt, TryStreamExt},
};
use num_traits::One;
use relay_substrate_client::{
BlockNumberOf, BlockWithJustification, Chain, Client, Error, HeaderOf,
};
use relay_substrate_client::{BlockNumberOf, BlockWithJustification, Client, Error, HeaderOf};
use relay_utils::{relay_loop::Client as RelayClient, UniqueSaturatedInto};
use std::pin::Pin;
/// Shared updatable reference to the maximal header number that we want to sync from the source.
pub type RequiredHeaderNumberRef<C> = Arc<Mutex<<C as bp_runtime::Chain>::BlockNumber>>;
/// Substrate finality proofs stream.
pub type SubstrateFinalityProofsStream<P> =
Pin<Box<dyn Stream<Item = SubstrateFinalityProof<P>> + Send>>;
/// Substrate finality proof. Specific to the used `FinalityEngine`.
pub type SubstrateFinalityProof<P> = <<P as SubstrateFinalityPipeline>::FinalityEngine as Engine<
<P as SubstrateFinalityPipeline>::SourceChain,
>>::FinalityProof;
/// Substrate node as finality source.
pub struct SubstrateFinalitySource<P: SubstrateFinalitySyncPipeline> {
client: Client<P::SourceChain>,
@@ -204,11 +193,20 @@ impl<P: SubstrateFinalitySyncPipeline> RelayClient for SubstrateFinalitySource<P
}
#[async_trait]
impl<P: SubstrateFinalitySyncPipeline> SourceClient<FinalitySyncPipelineAdapter<P>>
impl<P: SubstrateFinalitySyncPipeline> SourceClientBase<FinalitySyncPipelineAdapter<P>>
for SubstrateFinalitySource<P>
{
type FinalityProofsStream = SubstrateFinalityProofsStream<P>;
async fn finality_proofs(&self) -> Result<Self::FinalityProofsStream, Error> {
finality_proofs::<P>(&self.client).await
}
}
#[async_trait]
impl<P: SubstrateFinalitySyncPipeline> SourceClient<FinalitySyncPipelineAdapter<P>>
for SubstrateFinalitySource<P>
{
async fn best_finalized_block_number(&self) -> Result<BlockNumberOf<P::SourceChain>, Error> {
let mut finalized_header_number = self.on_chain_best_finalized_block_number().await?;
// never return block number larger than requested. This way we'll never sync headers
@@ -234,46 +232,6 @@ impl<P: SubstrateFinalitySyncPipeline> SourceClient<FinalitySyncPipelineAdapter<
> {
header_and_finality_proof::<P>(&self.client, number).await
}
async fn finality_proofs(&self) -> Result<Self::FinalityProofsStream, Error> {
Ok(unfold(
P::FinalityEngine::source_finality_proofs(&self.client).await?,
move |subscription| async move {
loop {
let log_error = |err| {
log::error!(
target: "bridge",
"Failed to read justification target from the {} justifications stream: {:?}",
P::SourceChain::NAME,
err,
);
};
let next_justification = subscription
.next()
.await
.map_err(|err| log_error(err.to_string()))
.ok()??;
let decoded_justification =
<P::FinalityEngine as Engine<P::SourceChain>>::FinalityProof::decode(
&mut &next_justification[..],
);
let justification = match decoded_justification {
Ok(j) => j,
Err(err) => {
log_error(format!("decode failed with error {err:?}"));
continue
},
};
return Some((justification, subscription))
}
},
)
.boxed())
}
}
async fn header_and_finality_proof<P: SubstrateFinalitySyncPipeline>(
@@ -18,21 +18,19 @@
use crate::{
finality::{
source::SubstrateFinalityProof, FinalitySyncPipelineAdapter,
SubmitFinalityProofCallBuilder, SubstrateFinalitySyncPipeline,
FinalitySyncPipelineAdapter, SubmitFinalityProofCallBuilder, SubstrateFinalitySyncPipeline,
},
finality_base::engine::Engine,
finality_base::{engine::Engine, SubstrateFinalityProof},
TransactionParams,
};
use async_trait::async_trait;
use finality_relay::TargetClient;
use relay_substrate_client::{
AccountIdOf, AccountKeyPairOf, Client, Error, HeaderIdOf, HeaderOf, SyncHeader, TransactionEra,
AccountKeyPairOf, Client, Error, HeaderIdOf, HeaderOf, SyncHeader, TransactionEra,
TransactionTracker, UnsignedTransaction,
};
use relay_utils::relay_loop::Client as RelayClient;
use sp_core::Pair;
/// Substrate client as Substrate finality target.
pub struct SubstrateFinalityTarget<P: SubstrateFinalitySyncPipeline> {
@@ -86,8 +84,6 @@ impl<P: SubstrateFinalitySyncPipeline> RelayClient for SubstrateFinalityTarget<P
#[async_trait]
impl<P: SubstrateFinalitySyncPipeline> TargetClient<FinalitySyncPipelineAdapter<P>>
for SubstrateFinalityTarget<P>
where
AccountIdOf<P::TargetChain>: From<<AccountKeyPairOf<P::TargetChain> as Pair>::Public>,
{
type TransactionTracker = TransactionTracker<P::TargetChain, Client<P::TargetChain>>;
@@ -116,7 +112,7 @@ where
P::FinalityEngine::optimize_proof(&self.client, &header, &mut proof).await?;
// now we may submit optimized finality proof
let transaction_params = self.transaction_params.clone();
let mortality = self.transaction_params.mortality;
let call =
P::SubmitFinalityProofCallBuilder::build_submit_finality_proof_call(header, proof);
self.client
@@ -124,7 +120,7 @@ where
&self.transaction_params.signer,
move |best_block_id, transaction_nonce| {
Ok(UnsignedTransaction::new(call.into(), transaction_nonce)
.era(TransactionEra::new(best_block_id, transaction_params.mortality)))
.era(TransactionEra::new(best_block_id, mortality)))
},
)
.await
@@ -19,13 +19,15 @@
use crate::error::Error;
use async_trait::async_trait;
use bp_header_chain::{
justification::{verify_and_optimize_justification, GrandpaJustification},
ChainWithGrandpa as ChainWithGrandpaBase, ConsensusLogReader, FinalityProof,
GrandpaConsensusLogReader,
justification::{
verify_and_optimize_justification, GrandpaEquivocationsFinder, GrandpaJustification,
JustificationVerificationContext,
},
AuthoritySet, ConsensusLogReader, FinalityProof, FindEquivocations, GrandpaConsensusLogReader,
HeaderFinalityInfo, HeaderGrandpaInfo, StoredHeaderGrandpaInfo,
};
use bp_runtime::{BasicOperatingMode, HeaderIdProvider, OperatingMode};
use codec::{Decode, Encode};
use finality_grandpa::voter_set::VoterSet;
use num_traits::{One, Zero};
use relay_substrate_client::{
BlockNumberOf, Chain, ChainWithGrandpa, Client, Error as SubstrateError, HashOf, HeaderOf,
@@ -33,7 +35,7 @@ use relay_substrate_client::{
};
use sp_consensus_grandpa::{AuthorityList as GrandpaAuthoritiesSet, GRANDPA_ENGINE_ID};
use sp_core::{storage::StorageKey, Bytes};
use sp_runtime::{traits::Header, ConsensusEngineId};
use sp_runtime::{scale_info::TypeInfo, traits::Header, ConsensusEngineId};
use std::marker::PhantomData;
/// Finality engine, used by the Substrate chain.
@@ -47,10 +49,18 @@ pub trait Engine<C: Chain>: Send {
type FinalityClient: SubstrateFinalityClient<C>;
/// Type of finality proofs, used by consensus engine.
type FinalityProof: FinalityProof<BlockNumberOf<C>> + Decode + Encode;
/// The context needed for verifying finality proofs.
type FinalityVerificationContext;
/// The type of the equivocation proof used by the consensus engine.
type EquivocationProof;
type EquivocationProof: Send + Sync;
/// The equivocations finder.
type EquivocationsFinder: FindEquivocations<
Self::FinalityProof,
Self::FinalityVerificationContext,
Self::EquivocationProof,
>;
/// The type of the key owner proof used by the consensus engine.
type KeyOwnerProof;
type KeyOwnerProof: Send;
/// Type of bridge pallet initialization data.
type InitializationData: std::fmt::Debug + Send + Sync + 'static;
/// Type of bridge pallet operating mode.
@@ -105,6 +115,29 @@ pub trait Engine<C: Chain>: Send {
async fn prepare_initialization_data(
client: Client<C>,
) -> Result<Self::InitializationData, Error<HashOf<C>, BlockNumberOf<C>>>;
/// Get the context needed for validating a finality proof.
async fn finality_verification_context<TargetChain: Chain>(
target_client: &Client<TargetChain>,
at: HashOf<TargetChain>,
) -> Result<Self::FinalityVerificationContext, SubstrateError>;
/// Returns the finality info associated to the source headers synced with the target
/// at the provided block.
async fn synced_headers_finality_info<TargetChain: Chain>(
target_client: &Client<TargetChain>,
at: TargetChain::Hash,
) -> Result<
Vec<HeaderFinalityInfo<Self::FinalityProof, Self::FinalityVerificationContext>>,
SubstrateError,
>;
/// Generate key ownership proof for the provided equivocation.
async fn generate_source_key_ownership_proof(
source_client: &Client<C>,
at: C::Hash,
equivocation: &Self::EquivocationProof,
) -> Result<Self::KeyOwnerProof, SubstrateError>;
}
/// GRANDPA finality engine.
@@ -142,21 +175,19 @@ impl<C: ChainWithGrandpa> Engine<C> for Grandpa<C> {
type ConsensusLogReader = GrandpaConsensusLogReader<<C::Header as Header>::Number>;
type FinalityClient = SubstrateGrandpaFinalityClient;
type FinalityProof = GrandpaJustification<HeaderOf<C>>;
type FinalityVerificationContext = JustificationVerificationContext;
type EquivocationProof = sp_consensus_grandpa::EquivocationProof<HashOf<C>, BlockNumberOf<C>>;
type EquivocationsFinder = GrandpaEquivocationsFinder<C>;
type KeyOwnerProof = C::KeyOwnerProof;
type InitializationData = bp_header_chain::InitializationData<C::Header>;
type OperatingMode = BasicOperatingMode;
fn is_initialized_key() -> StorageKey {
bp_header_chain::storage_keys::best_finalized_key(
C::ChainWithGrandpa::WITH_CHAIN_GRANDPA_PALLET_NAME,
)
bp_header_chain::storage_keys::best_finalized_key(C::WITH_CHAIN_GRANDPA_PALLET_NAME)
}
fn pallet_operating_mode_key() -> StorageKey {
bp_header_chain::storage_keys::pallet_operating_mode_key(
C::ChainWithGrandpa::WITH_CHAIN_GRANDPA_PALLET_NAME,
)
bp_header_chain::storage_keys::pallet_operating_mode_key(C::WITH_CHAIN_GRANDPA_PALLET_NAME)
}
async fn optimize_proof<TargetChain: Chain>(
@@ -164,31 +195,18 @@ impl<C: ChainWithGrandpa> Engine<C> for Grandpa<C> {
header: &C::Header,
proof: &mut Self::FinalityProof,
) -> Result<(), SubstrateError> {
let current_authority_set_key = bp_header_chain::storage_keys::current_authority_set_key(
C::ChainWithGrandpa::WITH_CHAIN_GRANDPA_PALLET_NAME,
);
let (authority_set, authority_set_id): (
sp_consensus_grandpa::AuthorityList,
sp_consensus_grandpa::SetId,
) = target_client
.storage_value(current_authority_set_key, None)
.await?
.map(Ok)
.unwrap_or(Err(SubstrateError::Custom(format!(
"{} `CurrentAuthoritySet` is missing from the {} storage",
C::NAME,
TargetChain::NAME,
))))?;
let authority_set =
finality_grandpa::voter_set::VoterSet::new(authority_set).expect("TODO");
let verification_context = Grandpa::<C>::finality_verification_context(
target_client,
target_client.best_header().await?.hash(),
)
.await?;
// we're risking with race here - we have decided to submit justification some time ago and
// actual authorities set (which we have read now) may have changed, so this
// `optimize_justification` may fail. But if target chain is configured properly, it'll fail
// anyway, after we submit transaction and failing earlier is better. So - it is fine
verify_and_optimize_justification(
(header.hash(), *header.number()),
authority_set_id,
&authority_set,
&verification_context,
proof,
)
.map_err(|e| {
@@ -275,8 +293,6 @@ impl<C: ChainWithGrandpa> Engine<C> for Grandpa<C> {
// Now let's try to guess authorities set id by verifying justification.
let mut initial_authorities_set_id = 0;
let mut min_possible_block_number = C::BlockNumber::zero();
let authorities_for_verification = VoterSet::new(authorities_for_verification.clone())
.ok_or(Error::ReadInvalidAuthorities(C::NAME, authorities_for_verification))?;
loop {
log::trace!(
target: "bridge", "Trying {} GRANDPA authorities set id: {}",
@@ -286,8 +302,14 @@ impl<C: ChainWithGrandpa> Engine<C> for Grandpa<C> {
let is_valid_set_id = verify_and_optimize_justification(
(initial_header_hash, initial_header_number),
initial_authorities_set_id,
&authorities_for_verification,
&AuthoritySet {
authorities: authorities_for_verification.clone(),
set_id: initial_authorities_set_id,
}
.try_into()
.map_err(|_| {
Error::ReadInvalidAuthorities(C::NAME, authorities_for_verification.clone())
})?,
&mut justification.clone(),
)
.is_ok();
@@ -317,4 +339,82 @@ impl<C: ChainWithGrandpa> Engine<C> for Grandpa<C> {
operating_mode: BasicOperatingMode::Normal,
})
}
async fn finality_verification_context<TargetChain: Chain>(
target_client: &Client<TargetChain>,
at: HashOf<TargetChain>,
) -> Result<Self::FinalityVerificationContext, SubstrateError> {
let current_authority_set_key = bp_header_chain::storage_keys::current_authority_set_key(
C::WITH_CHAIN_GRANDPA_PALLET_NAME,
);
let authority_set: AuthoritySet = target_client
.storage_value(current_authority_set_key, Some(at))
.await?
.map(Ok)
.unwrap_or(Err(SubstrateError::Custom(format!(
"{} `CurrentAuthoritySet` is missing from the {} storage",
C::NAME,
TargetChain::NAME,
))))?;
authority_set.try_into().map_err(|e| {
SubstrateError::Custom(format!(
"{} `CurrentAuthoritySet` from the {} storage is invalid: {e:?}",
C::NAME,
TargetChain::NAME,
))
})
}
async fn synced_headers_finality_info<TargetChain: Chain>(
target_client: &Client<TargetChain>,
at: TargetChain::Hash,
) -> Result<Vec<HeaderGrandpaInfo<HeaderOf<C>>>, SubstrateError> {
let stored_headers_grandpa_info: Vec<StoredHeaderGrandpaInfo<HeaderOf<C>>> = target_client
.typed_state_call(C::SYNCED_HEADERS_GRANDPA_INFO_METHOD.to_string(), (), Some(at))
.await?;
let mut headers_grandpa_info = vec![];
for stored_header_grandpa_info in stored_headers_grandpa_info {
headers_grandpa_info.push(stored_header_grandpa_info.try_into().map_err(|e| {
SubstrateError::Custom(format!(
"{} `AuthoritySet` synced to {} is invalid: {e:?} ",
C::NAME,
TargetChain::NAME,
))
})?);
}
Ok(headers_grandpa_info)
}
async fn generate_source_key_ownership_proof(
source_client: &Client<C>,
at: C::Hash,
equivocation: &Self::EquivocationProof,
) -> Result<Self::KeyOwnerProof, SubstrateError> {
let set_id = equivocation.set_id();
let offender = equivocation.offender();
let opaque_key_owner_proof = source_client
.generate_grandpa_key_ownership_proof(at, set_id, offender.clone())
.await?
.ok_or(SubstrateError::Custom(format!(
"Couldn't get GRANDPA key ownership proof from {} at block: {at} \
for offender: {:?}, set_id: {set_id} ",
C::NAME,
offender.clone(),
)))?;
let key_owner_proof =
opaque_key_owner_proof.decode().ok_or(SubstrateError::Custom(format!(
"Couldn't decode GRANDPA `OpaqueKeyOwnnershipProof` from {} at block: {at}
to `{:?}` for offender: {:?}, set_id: {set_id}, at block: {at}",
C::NAME,
<C::KeyOwnerProof as TypeInfo>::type_info().path,
offender.clone(),
)))?;
Ok(key_owner_proof)
}
}
@@ -19,9 +19,12 @@
pub mod engine;
use crate::finality_base::engine::Engine;
use async_trait::async_trait;
use relay_substrate_client::{Chain, ChainWithTransactions};
use std::fmt::Debug;
use codec::Decode;
use futures::{stream::unfold, Stream, StreamExt};
use relay_substrate_client::{Chain, Client, Error};
use std::{fmt::Debug, pin::Pin};
/// Substrate -> Substrate finality related pipeline.
#[async_trait]
@@ -29,7 +32,56 @@ pub trait SubstrateFinalityPipeline: 'static + Clone + Debug + Send + Sync {
/// Headers of this chain are submitted to the `TargetChain`.
type SourceChain: Chain;
/// Headers of the `SourceChain` are submitted to this chain.
type TargetChain: ChainWithTransactions;
type TargetChain: Chain;
/// Finality engine.
type FinalityEngine: engine::Engine<Self::SourceChain>;
type FinalityEngine: Engine<Self::SourceChain>;
}
/// Substrate finality proof. Specific to the used `FinalityEngine`.
pub type SubstrateFinalityProof<P> = <<P as SubstrateFinalityPipeline>::FinalityEngine as Engine<
<P as SubstrateFinalityPipeline>::SourceChain,
>>::FinalityProof;
/// Substrate finality proofs stream.
pub type SubstrateFinalityProofsStream<P> =
Pin<Box<dyn Stream<Item = SubstrateFinalityProof<P>> + Send>>;
/// Subscribe to new finality proofs.
pub async fn finality_proofs<P: SubstrateFinalityPipeline>(
client: &Client<P::SourceChain>,
) -> Result<SubstrateFinalityProofsStream<P>, Error> {
Ok(unfold(
P::FinalityEngine::source_finality_proofs(client).await?,
move |subscription| async move {
loop {
let log_error = |err| {
log::error!(
target: "bridge",
"Failed to read justification target from the {} justifications stream: {:?}",
P::SourceChain::NAME,
err,
);
};
let next_justification =
subscription.next().await.map_err(|err| log_error(err.to_string())).ok()??;
let decoded_justification =
<P::FinalityEngine as Engine<P::SourceChain>>::FinalityProof::decode(
&mut &next_justification[..],
);
let justification = match decoded_justification {
Ok(j) => j,
Err(err) => {
log_error(format!("decode failed with error {err:?}"));
continue
},
};
return Some((justification, subscription))
}
},
)
.boxed())
}