From 61dd6d47b92811ac7e1943aafc748a5294de92f5 Mon Sep 17 00:00:00 2001 From: Serban Iorga Date: Wed, 16 Aug 2023 08:20:09 +0300 Subject: [PATCH] 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 --- bin/millau/runtime/src/lib.rs | 4 +- bin/rialto-parachain/runtime/src/lib.rs | 2 +- bin/rialto/runtime/src/lib.rs | 2 +- modules/grandpa/src/lib.rs | 48 ++--- modules/parachains/src/lib.rs | 26 +-- .../header-chain/src/justification/mod.rs | 5 +- .../verification/equivocation.rs | 110 ++++++----- .../src/justification/verification/mod.rs | 31 +++- .../justification/verification/optimizer.rs | 17 +- .../src/justification/verification/strict.rs | 17 +- primitives/header-chain/src/lib.rs | 71 ++++++-- .../tests/implementation_match.rs | 34 ++-- .../tests/justification/equivocation.rs | 16 +- .../tests/justification/optimizer.rs | 21 +-- .../tests/justification/strict.rs | 28 +-- primitives/runtime/src/chain.rs | 2 +- primitives/test-utils/src/keyring.rs | 8 +- relays/bin-substrate/src/cli/relay_headers.rs | 7 +- relays/client-substrate/src/chain.rs | 9 +- relays/client-substrate/src/client.rs | 15 +- relays/equivocation/Cargo.toml | 13 ++ relays/equivocation/src/lib.rs | 70 +++++++ relays/finality/src/base.rs | 51 ++++++ relays/finality/src/finality_loop.rs | 17 +- relays/finality/src/finality_loop_tests.rs | 28 +-- relays/finality/src/lib.rs | 17 +- relays/lib-substrate-relay/Cargo.toml | 1 + .../src/equivocation/mod.rs | 78 +++++++- .../src/equivocation/source.rs | 101 ++++++++++ .../src/equivocation/target.rs | 86 +++++++++ .../lib-substrate-relay/src/finality/mod.rs | 49 +++-- .../src/finality/source.rs | 74 ++------ .../src/finality/target.rs | 14 +- .../src/finality_base/engine.rs | 172 ++++++++++++++---- .../src/finality_base/mod.rs | 60 +++++- 35 files changed, 930 insertions(+), 374 deletions(-) create mode 100644 relays/equivocation/Cargo.toml create mode 100644 relays/equivocation/src/lib.rs create mode 100644 relays/finality/src/base.rs create mode 100644 relays/lib-substrate-relay/src/equivocation/source.rs create mode 100644 relays/lib-substrate-relay/src/equivocation/target.rs diff --git a/bin/millau/runtime/src/lib.rs b/bin/millau/runtime/src/lib.rs index 75f82c534398..a1526bc2e181 100644 --- a/bin/millau/runtime/src/lib.rs +++ b/bin/millau/runtime/src/lib.rs @@ -884,7 +884,7 @@ impl_runtime_apis! { } fn synced_headers_grandpa_info( - ) -> Vec> { + ) -> Vec> { BridgeRialtoGrandpa::synced_headers_grandpa_info() } } @@ -895,7 +895,7 @@ impl_runtime_apis! { } fn synced_headers_grandpa_info( - ) -> Vec> { + ) -> Vec> { BridgeWestendGrandpa::synced_headers_grandpa_info() } } diff --git a/bin/rialto-parachain/runtime/src/lib.rs b/bin/rialto-parachain/runtime/src/lib.rs index eb15806b4e39..cae840de9674 100644 --- a/bin/rialto-parachain/runtime/src/lib.rs +++ b/bin/rialto-parachain/runtime/src/lib.rs @@ -748,7 +748,7 @@ impl_runtime_apis! { } fn synced_headers_grandpa_info( - ) -> Vec> { + ) -> Vec> { BridgeMillauGrandpa::synced_headers_grandpa_info() } } diff --git a/bin/rialto/runtime/src/lib.rs b/bin/rialto/runtime/src/lib.rs index 8af330b328b6..dbfa0cf7eb61 100644 --- a/bin/rialto/runtime/src/lib.rs +++ b/bin/rialto/runtime/src/lib.rs @@ -696,7 +696,7 @@ impl_runtime_apis! { } fn synced_headers_grandpa_info( - ) -> Vec> { + ) -> Vec> { BridgeMillauGrandpa::synced_headers_grandpa_info() } } diff --git a/modules/grandpa/src/lib.rs b/modules/grandpa/src/lib.rs index 259a6c0f36b7..425712ad9a20 100644 --- a/modules/grandpa/src/lib.rs +++ b/modules/grandpa/src/lib.rs @@ -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, hash: BridgedBlockHash, /// The Grandpa info associated to the new best finalized header. - grandpa_info: HeaderGrandpaInfo>, + grandpa_info: StoredHeaderGrandpaInfo>, }, } @@ -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(>::InvalidAuthoritySet)?; - let set_id = authority_set.set_id; - Ok(verify_justification::>( (hash, number), - set_id, - &voter_set, + &authority_set.try_into().map_err(|_| >::InvalidAuthoritySet)?, justification, ) .map_err(|e| { @@ -617,7 +612,7 @@ where ::RuntimeEvent: TryInto>, { /// Get the GRANDPA justifications accepted in the current block. - pub fn synced_headers_grandpa_info() -> Vec>> { + pub fn synced_headers_grandpa_info() -> Vec>> { frame_system::Pallet::::read_events_no_consensus() .filter_map(|event| { if let Event::::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::::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(>::get().into()), + grandpa_info: StoredHeaderGrandpaInfo { + finality_proof: justification.clone(), + new_verification_context: Some( + >::get().into() + ), }, }), topics: vec![], @@ -1085,9 +1085,11 @@ mod tests { ); assert_eq!( Pallet::::synced_headers_grandpa_info(), - vec![HeaderGrandpaInfo { - justification, - authority_set: Some(>::get().into()), + vec![StoredHeaderGrandpaInfo { + finality_proof: justification, + new_verification_context: Some( + >::get().into() + ), }] ); }) diff --git a/modules/parachains/src/lib.rs b/modules/parachains/src/lib.rs index 24bc3535e619..be46fae3c925 100644 --- a/modules/parachains/src/lib.rs +++ b/modules/parachains/src/lib.rs @@ -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, } } ), diff --git a/primitives/header-chain/src/justification/mod.rs b/primitives/header-chain/src/justification/mod.rs index 17be6cfd7961..5fa5d7d607c8 100644 --- a/primitives/header-chain/src/justification/mod.rs +++ b/primitives/header-chain/src/justification/mod.rs @@ -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}; diff --git a/primitives/header-chain/src/justification/verification/equivocation.rs b/primitives/header-chain/src/justification/verification/equivocation.rs index 0ade3736c227..2484fc4d6b2b 100644 --- a/primitives/header-chain/src/justification/verification/equivocation.rs +++ b/primitives/header-chain/src/justification/verification/equivocation.rs @@ -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 { SingleVote(SignedPrecommit
), Equivocation( @@ -53,8 +46,7 @@ enum AuthorityVotes { /// 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, + context: &'a JustificationVerificationContext, votes: BTreeMap>, } @@ -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, + context: &'a JustificationVerificationContext, base_justification: &GrandpaJustification
, - ) -> Result { - let mut checker = Self { - round: base_justification.round, - authorities_set_id, - authorities_set, - votes: BTreeMap::new(), - }; - - checker.parse_justification(base_justification)?; + ) -> Result { + 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, + )?; + Ok(checker) } - /// Parse an additional justification for equivocations. - pub fn parse_justification( - &mut self, - justification: &GrandpaJustification
, - ) -> 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
]) { + 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
for EquivocationsCollect Ok(()) } } + +/// Helper struct for finding equivocations in GRANDPA proofs. +pub struct GrandpaEquivocationsFinder(sp_std::marker::PhantomData); + +impl + FindEquivocations< + GrandpaJustification>, + JustificationVerificationContext, + EquivocationProof, BlockNumberOf>, + > for GrandpaEquivocationsFinder +{ + type Error = JustificationVerificationError; + + fn find_equivocations( + verification_context: &JustificationVerificationContext, + synced_proof: &GrandpaJustification>, + source_proofs: &[GrandpaJustification>], + ) -> Result, BlockNumberOf>>, Self::Error> { + let mut equivocations_collector = + EquivocationsCollector::new(verification_context, synced_proof)?; + + equivocations_collector.parse_justifications(source_proofs); + + Ok(equivocations_collector.into_equivocation_proofs()) + } +} diff --git a/primitives/header-chain/src/justification/verification/mod.rs b/primitives/header-chain/src/justification/verification/mod.rs index 7cec1f14e966..7af5292256cb 100644 --- a/primitives/header-chain/src/justification/verification/mod.rs +++ b/primitives/header-chain/src/justification/verification/mod.rs @@ -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 AncestryChain
{ /// 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, + /// The ID of the authority set used to verify the justification. + pub authority_set_id: SetId, +} + +impl TryFrom for JustificationVerificationContext { + type Error = Error; + + fn try_from(authority_set: AuthoritySet) -> Result { + 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 { fn verify_justification( &mut self, finalized_target: (Header::Hash, Header::Number), - authorities_set_id: SetId, - authorities_set: &VoterSet, + context: &JustificationVerificationContext, justification: &GrandpaJustification
, ) -> Result<(), Error> { // ensure that it is justification for the expected header @@ -196,7 +215,7 @@ trait JustificationVerifier { 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 { } // 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 { &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)?; diff --git a/primitives/header-chain/src/justification/verification/optimizer.rs b/primitives/header-chain/src/justification/verification/optimizer.rs index 4cc6778ff511..99ccdd50616b 100644 --- a/primitives/header-chain/src/justification/verification/optimizer.rs +++ b/primitives/header-chain/src/justification/verification/optimizer.rs @@ -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 JustificationVerifier
for JustificationOptimizer( finalized_target: (Header::Hash, Header::Number), - authorities_set_id: SetId, - authorities_set: &VoterSet, + context: &JustificationVerificationContext, justification: &mut GrandpaJustification
, ) -> Result<(), Error> { let mut optimizer = JustificationOptimizer { @@ -120,12 +120,7 @@ pub fn verify_and_optimize_justification( 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(()) diff --git a/primitives/header-chain/src/justification/verification/strict.rs b/primitives/header-chain/src/justification/verification/strict.rs index da936c235827..a9d5f4c1f736 100644 --- a/primitives/header-chain/src/justification/verification/strict.rs +++ b/primitives/header-chain/src/justification/verification/strict.rs @@ -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 JustificationVerifier
for StrictJustificationVerif /// Verify that justification, that is generated by given authority set, finalizes given header. pub fn verify_justification( finalized_target: (Header::Hash, Header::Number), - authorities_set_id: SetId, - authorities_set: &VoterSet, + context: &JustificationVerificationContext, justification: &GrandpaJustification
, ) -> 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) } diff --git a/primitives/header-chain/src/lib.rs b/primitives/header-chain/src/lib.rs index 720dafdd93b0..ea6c58f4c097 100644 --- a/primitives/header-chain/src/lib.rs +++ b/primitives/header-chain/src/lib.rs @@ -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 ConsensusLogReader for GrandpaConsensusLogReader { } } -/// 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 { - /// The header justification - pub justification: justification::GrandpaJustification
, - /// The authority set introduced by the header. - pub authority_set: Option, +pub struct HeaderFinalityInfo { + /// The header finality proof. + pub finality_proof: FinalityProof, + /// The new verification context introduced by the header. + pub new_verification_context: Option, +} + +/// Grandpa-related info associated to a header. This info can be saved to events. +pub type StoredHeaderGrandpaInfo
= + HeaderFinalityInfo, AuthoritySet>; + +/// Processed Grandpa-related info associated to a header. +pub type HeaderGrandpaInfo
= + HeaderFinalityInfo, JustificationVerificationContext>; + +impl TryFrom> for HeaderGrandpaInfo
{ + type Error = JustificationVerificationError; + + fn try_from(grandpa_info: StoredHeaderGrandpaInfo
) -> Result { + 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 { + /// 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, 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 UnderlyingChainWithGrandpaProvider for T +impl ChainWithGrandpa for T where - T: UnderlyingChainProvider, + T: Chain + UnderlyingChainProvider, T::Chain: ChainWithGrandpa, { - type ChainWithGrandpa = T::Chain; + const WITH_CHAIN_GRANDPA_PALLET_NAME: &'static str = + ::WITH_CHAIN_GRANDPA_PALLET_NAME; + const MAX_AUTHORITIES_COUNT: u32 = ::MAX_AUTHORITIES_COUNT; + const REASONABLE_HEADERS_IN_JUSTIFICATON_ANCESTRY: u32 = + ::REASONABLE_HEADERS_IN_JUSTIFICATON_ANCESTRY; + const MAX_HEADER_SIZE: u32 = ::MAX_HEADER_SIZE; + const AVERAGE_HEADER_SIZE_IN_JUSTIFICATION: u32 = + ::AVERAGE_HEADER_SIZE_IN_JUSTIFICATION; } diff --git a/primitives/header-chain/tests/implementation_match.rs b/primitives/header-chain/tests/implementation_match.rs index c4cd7f5f5b26..db96961832d4 100644 --- a/primitives/header-chain/tests/implementation_match.rs +++ b/primitives/header-chain/tests/implementation_match.rs @@ -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 { 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::( header_id::(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::( header_id::(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::( header_id::(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::( header_id::(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::( header_id::(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::( header_id::(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::( header_id::(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::( header_id::(1), - TEST_GRANDPA_SET_ID, - &full_voter_set(), + &full_verification_context(TEST_GRANDPA_SET_ID), &justification, ), Err(JustificationVerificationError::Precommit(PrecommitError::UnknownAuthorityVote)), diff --git a/primitives/header-chain/tests/justification/equivocation.rs b/primitives/header-chain/tests/justification/equivocation.rs index 072d5668edeb..f3c133481fb8 100644 --- a/primitives/header-chain/tests/justification/equivocation.rs +++ b/primitives/header-chain/tests/justification/equivocation.rs @@ -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::(&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::(&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::(&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(), diff --git a/primitives/header-chain/tests/justification/optimizer.rs b/primitives/header-chain/tests/justification/optimizer.rs index 8d1ba5ac6fac..bdc90a3b07cf 100644 --- a/primitives/header-chain/tests/justification/optimizer.rs +++ b/primitives/header-chain/tests/justification/optimizer.rs @@ -30,8 +30,7 @@ fn optimizer_does_noting_with_minimal_justification() { let num_precommits_before = justification.commit.precommits.len(); verify_and_optimize_justification::( header_id::(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::( header_id::(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::( header_id::(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::( header_id::(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::( header_id::(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::( header_id::(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::( header_id::(1), - TEST_GRANDPA_SET_ID, - &voter_set(), + &verification_context(TEST_GRANDPA_SET_ID), &mut justification, ) .unwrap(); diff --git a/primitives/header-chain/tests/justification/strict.rs b/primitives/header-chain/tests/justification/strict.rs index bf8fa5c9f457..9568f7f7cf9a 100644 --- a/primitives/header-chain/tests/justification/strict.rs +++ b/primitives/header-chain/tests/justification/strict.rs @@ -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::( header_id::(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::( header_id::(1), - TEST_GRANDPA_SET_ID, - &voter_set(), + &verification_context(TEST_GRANDPA_SET_ID), &make_justification_for_header::(params) ), Ok(()), @@ -100,8 +98,7 @@ fn valid_justification_accepted_with_arbitrary_number_of_authorities() { assert_eq!( verify_justification::( header_id::(1), - TEST_GRANDPA_SET_ID, - &voter_set, + &JustificationVerificationContext { voter_set, authority_set_id: TEST_GRANDPA_SET_ID }, &make_justification_for_header::(params) ), Ok(()), @@ -113,8 +110,7 @@ fn justification_with_invalid_target_rejected() { assert_eq!( verify_justification::( header_id::(2), - TEST_GRANDPA_SET_ID, - &voter_set(), + &verification_context(TEST_GRANDPA_SET_ID), &make_default_justification::(&test_header(1)), ), Err(JustificationVerificationError::InvalidJustificationTarget), @@ -129,8 +125,7 @@ fn justification_with_invalid_commit_rejected() { assert_eq!( verify_justification::( header_id::(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::( header_id::(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::( header_id::(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::( header_id::(1), - TEST_GRANDPA_SET_ID, - &voter_set(), + &verification_context(TEST_GRANDPA_SET_ID), &make_justification_for_header::(params) ), Err(JustificationVerificationError::TooLowCumulativeWeight), diff --git a/primitives/runtime/src/chain.rs b/primitives/runtime/src/chain.rs index a1b7e8d54b4f..43fdaf8da1b7 100644 --- a/primitives/runtime/src/chain.rs +++ b/primitives/runtime/src/chain.rs @@ -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
); + decl_bridge_finality_runtime_apis!($chain, grandpa => bp_header_chain::StoredHeaderGrandpaInfo
); }; } diff --git a/primitives/test-utils/src/keyring.rs b/primitives/test-utils/src/keyring.rs index b1782109668a..a4e818a3b888 100644 --- a/primitives/test-utils/src/keyring.rs +++ b/primitives/test-utils/src/keyring.rs @@ -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 { 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() diff --git a/relays/bin-substrate/src/cli/relay_headers.rs b/relays/bin-substrate/src/cli/relay_headers.rs index fbe360d3cd78..fd84782c07ce 100644 --- a/relays/bin-substrate/src/cli/relay_headers.rs +++ b/relays/bin-substrate/src/cli/relay_headers.rs @@ -15,8 +15,6 @@ // along with Parity Bridges Common. If not, see . 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: From< as Pair>::Public>, -{ +trait HeadersRelayer: RelayToRelayHeadersCliBridge { /// Relay headers. async fn relay_headers(data: RelayHeaders) -> anyhow::Result<()> { let source_client = data.source.into_client::().await?; diff --git a/relays/client-substrate/src/chain.rs b/relays/client-substrate/src/chain.rs index fb99fc7af16f..ec51b8dd1f2a 100644 --- a/relays/client-substrate/src/chain.rs +++ b/relays/client-substrate/src/chain.rs @@ -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. diff --git a/relays/client-substrate/src/client.rs b/relays/client-substrate/src/client.rs index a4001a19aa05..688303fb7379 100644 --- a/relays/client-substrate/src/client.rs +++ b/relays/client-substrate/src/client.rs @@ -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 Client { 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, set_id: sp_consensus_grandpa::SetId, authority_id: sp_consensus_grandpa::AuthorityId, - ) -> Result> { + ) -> Result> + where + C: ChainWithGrandpa, + { self.typed_state_call( SUB_API_GRANDPA_GENERATE_KEY_OWNERSHIP_PROOF.into(), (set_id, authority_id), diff --git a/relays/equivocation/Cargo.toml b/relays/equivocation/Cargo.toml new file mode 100644 index 000000000000..93a1470c6a97 --- /dev/null +++ b/relays/equivocation/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "equivocation-detector" +version = "0.1.0" +authors = ["Parity Technologies "] +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" } diff --git a/relays/equivocation/src/lib.rs b/relays/equivocation/src/lib.rs new file mode 100644 index 000000000000..9019c3cf69f6 --- /dev/null +++ b/relays/equivocation/src/lib.rs @@ -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 . + +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: SourceClientBase

{ + /// Transaction tracker to track submitted transactions. + type TransactionTracker: TransactionTracker; + + /// Report equivocation. + async fn report_equivocation( + &self, + at: P::Hash, + equivocation: P::EquivocationProof, + ) -> Result; +} + +/// Target client used in equivocation detection loop. +#[async_trait] +pub trait TargetClient: 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; + + /// 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>, + Self::Error, + >; +} diff --git a/relays/finality/src/base.rs b/relays/finality/src/base.rs new file mode 100644 index 000000000000..bf9acbdf9821 --- /dev/null +++ b/relays/finality/src/base.rs @@ -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 . + +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; +} + +/// Source client used in finality related loops. +#[async_trait] +pub trait SourceClientBase: 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 + Send + Unpin; + + /// Subscribe to new finality proofs. + async fn finality_proofs(&self) -> Result; +} + +/// Target client used in finality related loops. +#[async_trait] +pub trait TargetClientBase: RelayClient {} diff --git a/relays/finality/src/finality_loop.rs b/relays/finality/src/finality_loop.rs index 1ee1a8d9db6b..7c8217c209fd 100644 --- a/relays/finality/src/finality_loop.rs +++ b/relays/finality/src/finality_loop.rs @@ -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: 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 + Send; - +pub trait SourceClient: SourceClientBase

{ /// Get best finalized block number. async fn best_finalized_block_number(&self) -> Result; @@ -79,9 +77,6 @@ pub trait SourceClient: RelayClient { &self, number: P::Number, ) -> Result<(P::Header, Option), Self::Error>; - - /// Subscribe to new finality proofs. - async fn finality_proofs(&self) -> Result; } /// Target client used in finality synchronization loop. @@ -143,10 +138,10 @@ pub async fn run( pub(crate) type UnjustifiedHeaders = Vec; /// Finality proofs container. Ordered by target header number. pub(crate) type FinalityProofs

= - Vec<(

::Number,

::FinalityProof)>; + Vec<(

::Number,

::FinalityProof)>; /// Reference to finality proofs container. pub(crate) type FinalityProofsRef<'a, P> = - &'a [(

::Number,

::FinalityProof)]; + &'a [(

::Number,

::FinalityProof)]; /// Error that may happen inside finality synchronization loop. #[derive(Debug)] diff --git a/relays/finality/src/finality_loop_tests.rs b/relays/finality/src/finality_loop_tests.rs index 1853c095f703..774a5c0c6730 100644 --- a/relays/finality/src/finality_loop_tests.rs +++ b/relays/finality/src/finality_loop_tests.rs @@ -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; type Header = TestSourceHeader; - type FinalityProof = TestFinalityProof; } #[derive(Debug, Clone, PartialEq, Eq)] @@ -146,9 +149,18 @@ impl RelayClient for TestSourceClient { } #[async_trait] -impl SourceClient for TestSourceClient { +impl SourceClientBase for TestSourceClient { type FinalityProofsStream = Pin + 'static + Send>>; + async fn finality_proofs(&self) -> Result { + 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 for TestSourceClient { async fn best_finalized_block_number(&self) -> Result { let mut data = self.data.lock(); (self.on_method_call)(&mut data); @@ -163,12 +175,6 @@ impl SourceClient for TestSourceClient { (self.on_method_call)(&mut data); data.source_headers.get(&number).cloned().ok_or(TestError::NonConnection) } - - async fn finality_proofs(&self) -> Result { - let mut data = self.data.lock(); - (self.on_method_call)(&mut data); - Ok(futures::stream::iter(data.source_proofs.clone()).boxed()) - } } #[derive(Clone)] diff --git a/relays/finality/src/lib.rs b/relays/finality/src/lib.rs index dca47c6a572c..599cf2f9f9de 100644 --- a/relays/finality/src/lib.rs +++ b/relays/finality/src/lib.rs @@ -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; - /// Finality proof type. - type FinalityProof: FinalityProof; } /// Header that we're receiving from source node. diff --git a/relays/lib-substrate-relay/Cargo.toml b/relays/lib-substrate-relay/Cargo.toml index 869b6e00122e..72e97dbe69ac 100644 --- a/relays/lib-substrate-relay/Cargo.toml +++ b/relays/lib-substrate-relay/Cargo.toml @@ -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" } diff --git a/relays/lib-substrate-relay/src/equivocation/mod.rs b/relays/lib-substrate-relay/src/equivocation/mod.rs index 31668490f90c..40750088bfb4 100644 --- a/relays/lib-substrate-relay/src/equivocation/mod.rs +++ b/relays/lib-substrate-relay/src/equivocation/mod.rs @@ -17,30 +17,96 @@ //! Types and functions intended to ease adding of new Substrate -> Substrate //! equivocation detection pipelines. -use crate::finality_base::SubstrateFinalityPipeline; -use std::marker::PhantomData; +mod source; +mod target; + +use crate::finality_base::{engine::Engine, SubstrateFinalityPipeline, SubstrateFinalityProof}; -use crate::finality_base::engine::Engine; use async_trait::async_trait; -use bp_runtime::{BlockNumberOf, HashOf}; +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::CallOf; +use relay_substrate_client::{AccountKeyPairOf, CallOf, Chain, ChainWithTransactions}; +use sp_core::Pair; use sp_runtime::traits::{Block, Header}; +use std::marker::PhantomData; + +/// Convenience trait that adds bounds to `SubstrateEquivocationDetectionPipeline`. +pub trait BaseSubstrateEquivocationDetectionPipeline: + SubstrateFinalityPipeline +{ + /// Bounded `SubstrateFinalityPipeline::SourceChain`. + type BoundedSourceChain: ChainWithTransactions; + + /// Bounded `AccountIdOf`. + type BoundedSourceChainAccountId: From< as Pair>::Public> + + Send; +} + +impl BaseSubstrateEquivocationDetectionPipeline for T +where + T: SubstrateFinalityPipeline, + T::SourceChain: ChainWithTransactions, + AccountIdOf: From< as Pair>::Public>, +{ + type BoundedSourceChain = T::SourceChain; + type BoundedSourceChainAccountId = AccountIdOf; +} /// 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; } +type FinalityProoffOf

= <

::FinalityEngine as Engine< +

::SourceChain, +>>::FinalityProof; +type FinalityVerificationContextfOf

= + <

::FinalityEngine as Engine< +

::SourceChain, + >>::FinalityVerificationContext; type EquivocationProofOf

= <

::FinalityEngine as Engine<

::SourceChain, >>::EquivocationProof; +type EquivocationsFinderOf

= <

::FinalityEngine as Engine< +

::SourceChain, +>>::EquivocationsFinder; type KeyOwnerProofOf

= <

::FinalityEngine as Engine<

::SourceChain, >>::KeyOwnerProof; +/// Adapter that allows a `SubstrateEquivocationDetectionPipeline` to act as an +/// `EquivocationDetectionPipeline`. +#[derive(Clone, Debug)] +pub struct EquivocationDetectionPipelineAdapter { + _phantom: PhantomData

, +} + +impl FinalityPipeline + for EquivocationDetectionPipelineAdapter

+{ + const SOURCE_NAME: &'static str = P::SourceChain::NAME; + const TARGET_NAME: &'static str = P::TargetChain::NAME; + + type Hash = HashOf; + type Number = BlockNumberOf; + type FinalityProof = SubstrateFinalityProof

; +} + +impl EquivocationDetectionPipeline + for EquivocationDetectionPipelineAdapter

+{ + type TargetNumber = BlockNumberOf; + type FinalityVerificationContext = FinalityVerificationContextfOf

; + type EquivocationProof = EquivocationProofOf

; + type EquivocationsFinder = EquivocationsFinderOf

; +} + /// Different ways of building `report_equivocation` calls. pub trait ReportEquivocationCallBuilder { /// Build a `report_equivocation` call to be executed on the source chain. diff --git a/relays/lib-substrate-relay/src/equivocation/source.rs b/relays/lib-substrate-relay/src/equivocation/source.rs new file mode 100644 index 000000000000..543c057ee9ae --- /dev/null +++ b/relays/lib-substrate-relay/src/equivocation/source.rs @@ -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 . + +//! 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 { + client: Client, + transaction_params: TransactionParams>, +} + +impl SubstrateEquivocationSource

{} + +impl Clone for SubstrateEquivocationSource

{ + fn clone(&self) -> Self { + Self { client: self.client.clone(), transaction_params: self.transaction_params.clone() } + } +} + +#[async_trait] +impl RelayClient for SubstrateEquivocationSource

{ + type Error = Error; + + async fn reconnect(&mut self) -> Result<(), Error> { + self.client.reconnect().await + } +} + +#[async_trait] +impl + SourceClientBase> for SubstrateEquivocationSource

+{ + type FinalityProofsStream = SubstrateFinalityProofsStream

; + + async fn finality_proofs(&self) -> Result { + finality_proofs::

(&self.client).await + } +} + +#[async_trait] +impl + SourceClient> for SubstrateEquivocationSource

+{ + type TransactionTracker = TransactionTracker>; + + async fn report_equivocation( + &self, + at: HashOf, + equivocation: EquivocationProofOf

, + ) -> Result { + 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 + } +} diff --git a/relays/lib-substrate-relay/src/equivocation/target.rs b/relays/lib-substrate-relay/src/equivocation/target.rs new file mode 100644 index 000000000000..36343a6eddb8 --- /dev/null +++ b/relays/lib-substrate-relay/src/equivocation/target.rs @@ -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 . + +//! 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 { + client: Client, + + _phantom: PhantomData

, +} + +impl SubstrateEquivocationTarget

{} + +impl Clone for SubstrateEquivocationTarget

{ + fn clone(&self) -> Self { + Self { client: self.client.clone(), _phantom: Default::default() } + } +} + +#[async_trait] +impl RelayClient for SubstrateEquivocationTarget

{ + type Error = Error; + + async fn reconnect(&mut self) -> Result<(), Error> { + self.client.reconnect().await + } +} + +#[async_trait] +impl + TargetClient> for SubstrateEquivocationTarget

+{ + async fn finality_verification_context( + &self, + at: BlockNumberOf, + ) -> Result, 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, + ) -> Result< + Vec, FinalityVerificationContextfOf

>>, + Self::Error, + > { + P::FinalityEngine::synced_headers_finality_info( + &self.client, + self.client.header_by_number(at).await?.hash(), + ) + .await + } +} diff --git a/relays/lib-substrate-relay/src/finality/mod.rs b/relays/lib-substrate-relay/src/finality/mod.rs index bae49efe72e0..98aec1d52b90 100644 --- a/relays/lib-substrate-relay/src/finality/mod.rs +++ b/relays/lib-substrate-relay/src/finality/mod.rs @@ -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 +{ + /// Bounded `SubstrateFinalityPipeline::TargetChain`. + type BoundedTargetChain: ChainWithTransactions; + + /// Bounded `AccountIdOf`. + type BoundedTargetChainAccountId: From< as Pair>::Public> + + Send; +} + +impl BaseSubstrateFinalitySyncPipeline for T +where + T: SubstrateFinalityPipeline, + T::TargetChain: ChainWithTransactions, + AccountIdOf: From< as Pair>::Public>, +{ + type BoundedTargetChain = T::TargetChain; + type BoundedTargetChainAccountId = AccountIdOf; +} + /// 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; @@ -70,15 +89,18 @@ pub struct FinalitySyncPipelineAdapter { _phantom: PhantomData

, } -impl FinalitySyncPipeline for FinalitySyncPipelineAdapter

{ +impl FinalityPipeline for FinalitySyncPipelineAdapter

{ const SOURCE_NAME: &'static str = P::SourceChain::NAME; const TARGET_NAME: &'static str = P::TargetChain::NAME; type Hash = HashOf; type Number = BlockNumberOf; + type FinalityProof = SubstrateFinalityProof

; +} + +impl FinalitySyncPipeline for FinalitySyncPipelineAdapter

{ type ConsensusLogReader = >::ConsensusLogReader; type Header = SyncHeader>; - type FinalityProof = SubstrateFinalityProof

; } /// Different ways of building `submit_finality_proof` calls. @@ -165,10 +187,7 @@ pub async fn run( only_mandatory_headers: bool, transaction_params: TransactionParams>, metrics_params: MetricsParams, -) -> anyhow::Result<()> -where - AccountIdOf: From< as Pair>::Public>, -{ +) -> anyhow::Result<()> { log::info!( target: "bridge", "Starting {} -> {} finality proof relay", diff --git a/relays/lib-substrate-relay/src/finality/source.rs b/relays/lib-substrate-relay/src/finality/source.rs index 208299f9cb2b..41c6c53daf4d 100644 --- a/relays/lib-substrate-relay/src/finality/source.rs +++ b/relays/lib-substrate-relay/src/finality/source.rs @@ -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 = Arc::BlockNumber>>; -/// Substrate finality proofs stream. -pub type SubstrateFinalityProofsStream

= - Pin> + Send>>; - -/// Substrate finality proof. Specific to the used `FinalityEngine`. -pub type SubstrateFinalityProof

= <

::FinalityEngine as Engine< -

::SourceChain, ->>::FinalityProof; - /// Substrate node as finality source. pub struct SubstrateFinalitySource { client: Client, @@ -204,11 +193,20 @@ impl RelayClient for SubstrateFinalitySource

SourceClient> +impl SourceClientBase> for SubstrateFinalitySource

{ type FinalityProofsStream = SubstrateFinalityProofsStream

; + async fn finality_proofs(&self) -> Result { + finality_proofs::

(&self.client).await + } +} + +#[async_trait] +impl SourceClient> + for SubstrateFinalitySource

+{ async fn best_finalized_block_number(&self) -> Result, 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 SourceClient { header_and_finality_proof::

(&self.client, number).await } - - async fn finality_proofs(&self) -> Result { - 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 = - >::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( diff --git a/relays/lib-substrate-relay/src/finality/target.rs b/relays/lib-substrate-relay/src/finality/target.rs index 3d1fa780eaef..1be668392dd3 100644 --- a/relays/lib-substrate-relay/src/finality/target.rs +++ b/relays/lib-substrate-relay/src/finality/target.rs @@ -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 { @@ -86,8 +84,6 @@ impl RelayClient for SubstrateFinalityTarget

TargetClient> for SubstrateFinalityTarget

-where - AccountIdOf: From< as Pair>::Public>, { type TransactionTracker = TransactionTracker>; @@ -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 diff --git a/relays/lib-substrate-relay/src/finality_base/engine.rs b/relays/lib-substrate-relay/src/finality_base/engine.rs index dc84ebaa4d5c..49614638f89e 100644 --- a/relays/lib-substrate-relay/src/finality_base/engine.rs +++ b/relays/lib-substrate-relay/src/finality_base/engine.rs @@ -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: Send { type FinalityClient: SubstrateFinalityClient; /// Type of finality proofs, used by consensus engine. type FinalityProof: FinalityProof> + 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: Send { async fn prepare_initialization_data( client: Client, ) -> Result, BlockNumberOf>>; + + /// Get the context needed for validating a finality proof. + async fn finality_verification_context( + target_client: &Client, + at: HashOf, + ) -> Result; + + /// Returns the finality info associated to the source headers synced with the target + /// at the provided block. + async fn synced_headers_finality_info( + target_client: &Client, + at: TargetChain::Hash, + ) -> Result< + Vec>, + SubstrateError, + >; + + /// Generate key ownership proof for the provided equivocation. + async fn generate_source_key_ownership_proof( + source_client: &Client, + at: C::Hash, + equivocation: &Self::EquivocationProof, + ) -> Result; } /// GRANDPA finality engine. @@ -142,21 +175,19 @@ impl Engine for Grandpa { type ConsensusLogReader = GrandpaConsensusLogReader<::Number>; type FinalityClient = SubstrateGrandpaFinalityClient; type FinalityProof = GrandpaJustification>; + type FinalityVerificationContext = JustificationVerificationContext; type EquivocationProof = sp_consensus_grandpa::EquivocationProof, BlockNumberOf>; + type EquivocationsFinder = GrandpaEquivocationsFinder; type KeyOwnerProof = C::KeyOwnerProof; type InitializationData = bp_header_chain::InitializationData; 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( @@ -164,31 +195,18 @@ impl Engine for Grandpa { 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::::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 Engine for Grandpa { // 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 Engine for Grandpa { 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 Engine for Grandpa { operating_mode: BasicOperatingMode::Normal, }) } + + async fn finality_verification_context( + target_client: &Client, + at: HashOf, + ) -> Result { + 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( + target_client: &Client, + at: TargetChain::Hash, + ) -> Result>>, SubstrateError> { + let stored_headers_grandpa_info: Vec>> = 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, + at: C::Hash, + equivocation: &Self::EquivocationProof, + ) -> Result { + 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, + ::type_info().path, + offender.clone(), + )))?; + + Ok(key_owner_proof) + } } diff --git a/relays/lib-substrate-relay/src/finality_base/mod.rs b/relays/lib-substrate-relay/src/finality_base/mod.rs index e48f82480325..bcd3f008d67c 100644 --- a/relays/lib-substrate-relay/src/finality_base/mod.rs +++ b/relays/lib-substrate-relay/src/finality_base/mod.rs @@ -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; + type FinalityEngine: Engine; +} + +/// Substrate finality proof. Specific to the used `FinalityEngine`. +pub type SubstrateFinalityProof

= <

::FinalityEngine as Engine< +

::SourceChain, +>>::FinalityProof; + +/// Substrate finality proofs stream. +pub type SubstrateFinalityProofsStream

= + Pin> + Send>>; + +/// Subscribe to new finality proofs. +pub async fn finality_proofs( + client: &Client, +) -> Result, 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 = + >::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()) }