diff --git a/bridges/bin/runtime-common/src/extensions.rs b/bridges/bin/runtime-common/src/extensions.rs
new file mode 100644
index 0000000000000..5dd98971acedc
--- /dev/null
+++ b/bridges/bin/runtime-common/src/extensions.rs
@@ -0,0 +1,797 @@
+// Copyright (C) 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 .
+
+//! Transaction extension that rejects bridge-related transactions, that include
+//! obsolete (duplicated) data or do not pass some additional pallet-specific
+//! checks.
+
+use bp_parachains::SubmitParachainHeadsInfo;
+use bp_relayers::ExplicitOrAccountParams;
+use bp_runtime::Parachain;
+use pallet_bridge_grandpa::{
+ BridgedBlockNumber, CallSubType as GrandpaCallSubType, SubmitFinalityProofHelper,
+};
+use pallet_bridge_messages::CallSubType as MessagesCallSubType;
+use pallet_bridge_parachains::{CallSubType as ParachainsCallSubtype, SubmitParachainHeadsHelper};
+use pallet_bridge_relayers::Pallet as RelayersPallet;
+use sp_runtime::{
+ traits::{Get, UniqueSaturatedInto},
+ transaction_validity::{TransactionPriority, TransactionValidity, ValidTransactionBuilder},
+};
+use sp_std::marker::PhantomData;
+
+/// A duplication of the `FilterCall` trait.
+///
+/// We need this trait in order to be able to implement it for the messages pallet,
+/// since the implementation is done outside of the pallet crate.
+pub trait BridgeRuntimeFilterCall {
+ /// Data that may be passed from the validate to `post_dispatch`.
+ type ToPostDispatch;
+ /// Called during validation. Needs to checks whether a runtime call, submitted
+ /// by the `who` is valid. `who` may be `None` if transaction is not signed
+ /// by a regular account.
+ fn validate(who: &AccountId, call: &Call) -> (Self::ToPostDispatch, TransactionValidity);
+ /// Called after transaction is dispatched.
+ fn post_dispatch(_who: &AccountId, _has_failed: bool, _to_post_dispatch: Self::ToPostDispatch) {
+ }
+}
+
+/// Wrapper for the bridge GRANDPA pallet that checks calls for obsolete submissions
+/// and also boosts transaction priority if it has submitted by registered relayer.
+/// The boost is computed as
+/// `(BundledHeaderNumber - 1 - BestFinalizedHeaderNumber) * Priority::get()`.
+/// The boost is only applied if submitter has active registration in the relayers
+/// pallet.
+pub struct CheckAndBoostBridgeGrandpaTransactions(
+ PhantomData<(T, I, Priority, SlashAccount)>,
+);
+
+impl, SlashAccount: Get>
+ BridgeRuntimeFilterCall
+ for CheckAndBoostBridgeGrandpaTransactions
+where
+ T: pallet_bridge_relayers::Config + pallet_bridge_grandpa::Config,
+ T::RuntimeCall: GrandpaCallSubType,
+{
+ // bridged header number, bundled in transaction
+ type ToPostDispatch = Option>;
+
+ fn validate(
+ who: &T::AccountId,
+ call: &T::RuntimeCall,
+ ) -> (Self::ToPostDispatch, TransactionValidity) {
+ match GrandpaCallSubType::::check_obsolete_submit_finality_proof(call) {
+ Ok(Some(our_tx)) => {
+ let to_post_dispatch = Some(our_tx.base.block_number);
+ let total_priority_boost =
+ compute_priority_boost::(who, our_tx.improved_by);
+ (
+ to_post_dispatch,
+ ValidTransactionBuilder::default().priority(total_priority_boost).build(),
+ )
+ },
+ Ok(None) => (None, ValidTransactionBuilder::default().build()),
+ Err(e) => (None, Err(e)),
+ }
+ }
+
+ fn post_dispatch(
+ relayer: &T::AccountId,
+ has_failed: bool,
+ bundled_block_number: Self::ToPostDispatch,
+ ) {
+ // we are only interested in associated pallet submissions
+ let Some(bundled_block_number) = bundled_block_number else { return };
+ // we are only interested in failed or unneeded transactions
+ let has_failed =
+ has_failed || !SubmitFinalityProofHelper::::was_successful(bundled_block_number);
+
+ if !has_failed {
+ return
+ }
+
+ // let's slash registered relayer
+ RelayersPallet::::slash_and_deregister(
+ relayer,
+ ExplicitOrAccountParams::Explicit(SlashAccount::get()),
+ );
+ }
+}
+
+/// Wrapper for the bridge parachains pallet that checks calls for obsolete submissions
+/// and also boosts transaction priority if it has submitted by registered relayer.
+/// The boost is computed as
+/// `(BundledHeaderNumber - 1 - BestKnownHeaderNumber) * Priority::get()`.
+/// The boost is only applied if submitter has active registration in the relayers
+/// pallet.
+pub struct CheckAndBoostBridgeParachainsTransactions<
+ T,
+ ParachainsInstance,
+ Para,
+ Priority,
+ SlashAccount,
+>(PhantomData<(T, ParachainsInstance, Para, Priority, SlashAccount)>);
+
+impl<
+ T,
+ ParachainsInstance,
+ Para,
+ Priority: Get,
+ SlashAccount: Get,
+ > BridgeRuntimeFilterCall
+ for CheckAndBoostBridgeParachainsTransactions
+where
+ T: pallet_bridge_relayers::Config + pallet_bridge_parachains::Config,
+ ParachainsInstance: 'static,
+ Para: Parachain,
+ T::RuntimeCall: ParachainsCallSubtype,
+{
+ // bridged header number, bundled in transaction
+ type ToPostDispatch = Option;
+
+ fn validate(
+ who: &T::AccountId,
+ call: &T::RuntimeCall,
+ ) -> (Self::ToPostDispatch, TransactionValidity) {
+ match ParachainsCallSubtype::::check_obsolete_submit_parachain_heads(
+ call,
+ ) {
+ Ok(Some(our_tx)) if our_tx.base.para_id.0 == Para::PARACHAIN_ID => {
+ let to_post_dispatch = Some(our_tx.base);
+ let total_priority_boost =
+ compute_priority_boost::(&who, our_tx.improved_by);
+ (
+ to_post_dispatch,
+ ValidTransactionBuilder::default().priority(total_priority_boost).build(),
+ )
+ },
+ Ok(_) => (None, ValidTransactionBuilder::default().build()),
+ Err(e) => (None, Err(e)),
+ }
+ }
+
+ fn post_dispatch(relayer: &T::AccountId, has_failed: bool, maybe_update: Self::ToPostDispatch) {
+ // we are only interested in associated pallet submissions
+ let Some(update) = maybe_update else { return };
+ // we are only interested in failed or unneeded transactions
+ let has_failed = has_failed ||
+ !SubmitParachainHeadsHelper::::was_successful(&update);
+
+ if !has_failed {
+ return
+ }
+
+ // let's slash registered relayer
+ RelayersPallet::::slash_and_deregister(
+ relayer,
+ ExplicitOrAccountParams::Explicit(SlashAccount::get()),
+ );
+ }
+}
+
+impl BridgeRuntimeFilterCall
+ for pallet_bridge_grandpa::Pallet
+where
+ T: pallet_bridge_grandpa::Config,
+ T::RuntimeCall: GrandpaCallSubType,
+{
+ type ToPostDispatch = ();
+ fn validate(_who: &T::AccountId, call: &T::RuntimeCall) -> ((), TransactionValidity) {
+ (
+ (),
+ GrandpaCallSubType::::check_obsolete_submit_finality_proof(call)
+ .and_then(|_| ValidTransactionBuilder::default().build()),
+ )
+ }
+}
+
+impl BridgeRuntimeFilterCall
+ for pallet_bridge_parachains::Pallet
+where
+ T: pallet_bridge_parachains::Config,
+ T::RuntimeCall: ParachainsCallSubtype,
+{
+ type ToPostDispatch = ();
+ fn validate(_who: &T::AccountId, call: &T::RuntimeCall) -> ((), TransactionValidity) {
+ (
+ (),
+ ParachainsCallSubtype::::check_obsolete_submit_parachain_heads(call)
+ .and_then(|_| ValidTransactionBuilder::default().build()),
+ )
+ }
+}
+
+impl, I: 'static>
+ BridgeRuntimeFilterCall for pallet_bridge_messages::Pallet
+where
+ T::RuntimeCall: MessagesCallSubType,
+{
+ type ToPostDispatch = ();
+ /// Validate messages in order to avoid "mining" messages delivery and delivery confirmation
+ /// transactions, that are delivering outdated messages/confirmations. Without this validation,
+ /// even honest relayers may lose their funds if there are multiple relays running and
+ /// submitting the same messages/confirmations.
+ fn validate(_who: &T::AccountId, call: &T::RuntimeCall) -> ((), TransactionValidity) {
+ ((), call.check_obsolete_call())
+ }
+}
+
+/// Computes priority boost that improved known header by `improved_by`
+fn compute_priority_boost(
+ relayer: &T::AccountId,
+ improved_by: N,
+) -> TransactionPriority
+where
+ T: pallet_bridge_relayers::Config,
+ N: UniqueSaturatedInto,
+ Priority: Get,
+{
+ // we only boost priority if relayer has staked required balance
+ let is_relayer_registration_active = RelayersPallet::::is_registration_active(relayer);
+ // if tx improves by just one, there's no need to bump its priority
+ let improved_by: TransactionPriority = improved_by.unique_saturated_into().saturating_sub(1);
+ // if relayer is registered, for every skipped header we improve by `Priority`
+ let boost_per_header = if is_relayer_registration_active { Priority::get() } else { 0 };
+ improved_by.saturating_mul(boost_per_header)
+}
+
+/// Declares a runtime-specific `BridgeRejectObsoleteHeadersAndMessages` signed extension.
+///
+/// ## Example
+///
+/// ```nocompile
+/// generate_bridge_reject_obsolete_headers_and_messages!{
+/// Call, AccountId
+/// BridgeRococoGrandpa, BridgeRococoMessages,
+/// BridgeRococoParachains
+/// }
+/// ```
+///
+/// The goal of this extension is to avoid "mining" transactions that provide outdated bridged
+/// headers and messages. Without that extension, even honest relayers may lose their funds if
+/// there are multiple relays running and submitting the same information.
+#[macro_export]
+macro_rules! generate_bridge_reject_obsolete_headers_and_messages {
+ ($call:ty, $account_id:ty, $($filter_call:ty),*) => {
+ #[derive(Clone, codec::Decode, Default, codec::Encode, Eq, PartialEq, sp_runtime::RuntimeDebug, scale_info::TypeInfo)]
+ pub struct BridgeRejectObsoleteHeadersAndMessages;
+ impl sp_runtime::traits::SignedExtension for BridgeRejectObsoleteHeadersAndMessages {
+ const IDENTIFIER: &'static str = "BridgeRejectObsoleteHeadersAndMessages";
+ type AccountId = $account_id;
+ type Call = $call;
+ type AdditionalSigned = ();
+ type Pre = (
+ $account_id,
+ ( $(
+ <$filter_call as $crate::extensions::BridgeRuntimeFilterCall<
+ $account_id,
+ $call,
+ >>::ToPostDispatch,
+ )* ),
+ );
+
+ fn additional_signed(&self) -> sp_std::result::Result<
+ (),
+ sp_runtime::transaction_validity::TransactionValidityError,
+ > {
+ Ok(())
+ }
+
+ #[allow(unused_variables)]
+ fn validate(
+ &self,
+ who: &Self::AccountId,
+ call: &Self::Call,
+ _info: &sp_runtime::traits::DispatchInfoOf,
+ _len: usize,
+ ) -> sp_runtime::transaction_validity::TransactionValidity {
+ let tx_validity = sp_runtime::transaction_validity::ValidTransaction::default();
+ let to_prepare = ();
+ $(
+ let (from_validate, call_filter_validity) = <
+ $filter_call as
+ $crate::extensions::BridgeRuntimeFilterCall<
+ Self::AccountId,
+ $call,
+ >>::validate(&who, call);
+ let tx_validity = tx_validity.combine_with(call_filter_validity?);
+ )*
+ Ok(tx_validity)
+ }
+
+ #[allow(unused_variables)]
+ fn pre_dispatch(
+ self,
+ relayer: &Self::AccountId,
+ call: &Self::Call,
+ info: &sp_runtime::traits::DispatchInfoOf,
+ len: usize,
+ ) -> Result {
+ use tuplex::PushBack;
+ let to_post_dispatch = ();
+ $(
+ let (from_validate, call_filter_validity) = <
+ $filter_call as
+ $crate::extensions::BridgeRuntimeFilterCall<
+ $account_id,
+ $call,
+ >>::validate(&relayer, call);
+ let _ = call_filter_validity?;
+ let to_post_dispatch = to_post_dispatch.push_back(from_validate);
+ )*
+ Ok((relayer.clone(), to_post_dispatch))
+ }
+
+ #[allow(unused_variables)]
+ fn post_dispatch(
+ to_post_dispatch: Option,
+ info: &sp_runtime::traits::DispatchInfoOf,
+ post_info: &sp_runtime::traits::PostDispatchInfoOf,
+ len: usize,
+ result: &sp_runtime::DispatchResult,
+ ) -> Result<(), sp_runtime::transaction_validity::TransactionValidityError> {
+ use tuplex::PopFront;
+ let Some((relayer, to_post_dispatch)) = to_post_dispatch else { return Ok(()) };
+ let has_failed = result.is_err();
+ $(
+ let (item, to_post_dispatch) = to_post_dispatch.pop_front();
+ <
+ $filter_call as
+ $crate::extensions::BridgeRuntimeFilterCall<
+ $account_id,
+ $call,
+ >>::post_dispatch(&relayer, has_failed, item);
+ )*
+ Ok(())
+ }
+ }
+ };
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::mock::*;
+ use bp_header_chain::StoredHeaderDataBuilder;
+ use bp_messages::{InboundLaneData, LaneId, MessageNonce, OutboundLaneData};
+ use bp_parachains::{BestParaHeadHash, ParaInfo};
+ use bp_polkadot_core::parachains::{ParaHeadsProof, ParaId};
+ use bp_relayers::{RewardsAccountOwner, RewardsAccountParams};
+ use bp_runtime::HeaderId;
+ use bp_test_utils::{make_default_justification, test_keyring, TEST_GRANDPA_SET_ID};
+ use frame_support::{assert_err, assert_ok, traits::fungible::Mutate};
+ use pallet_bridge_grandpa::{Call as GrandpaCall, StoredAuthoritySet};
+ use pallet_bridge_parachains::Call as ParachainsCall;
+ use sp_runtime::{
+ traits::{parameter_types, ConstU64, Header as _, SignedExtension},
+ transaction_validity::{InvalidTransaction, TransactionValidity, ValidTransaction},
+ DispatchError,
+ };
+
+ parameter_types! {
+ pub MsgProofsRewardsAccount: RewardsAccountParams = RewardsAccountParams::new(
+ test_lane_id(),
+ TEST_BRIDGED_CHAIN_ID,
+ RewardsAccountOwner::ThisChain,
+ );
+ pub MsgDeliveryProofsRewardsAccount: RewardsAccountParams = RewardsAccountParams::new(
+ test_lane_id(),
+ TEST_BRIDGED_CHAIN_ID,
+ RewardsAccountOwner::BridgedChain,
+ );
+ pub TestLaneId: LaneId = test_lane_id();
+ }
+
+ pub struct MockCall {
+ data: u32,
+ }
+
+ impl sp_runtime::traits::Dispatchable for MockCall {
+ type RuntimeOrigin = u64;
+ type Config = ();
+ type Info = ();
+ type PostInfo = ();
+
+ fn dispatch(
+ self,
+ _origin: Self::RuntimeOrigin,
+ ) -> sp_runtime::DispatchResultWithInfo {
+ unimplemented!()
+ }
+ }
+
+ pub struct FirstFilterCall;
+ impl FirstFilterCall {
+ fn post_dispatch_called_with(success: bool) {
+ frame_support::storage::unhashed::put(&[1], &success);
+ }
+
+ fn verify_post_dispatch_called_with(success: bool) {
+ assert_eq!(frame_support::storage::unhashed::get::(&[1]), Some(success));
+ }
+ }
+
+ impl BridgeRuntimeFilterCall for FirstFilterCall {
+ type ToPostDispatch = u64;
+ fn validate(_who: &u64, call: &MockCall) -> (u64, TransactionValidity) {
+ if call.data <= 1 {
+ return (1, InvalidTransaction::Custom(1).into())
+ }
+
+ (1, Ok(ValidTransaction { priority: 1, ..Default::default() }))
+ }
+
+ fn post_dispatch(_who: &u64, has_failed: bool, to_post_dispatch: Self::ToPostDispatch) {
+ Self::post_dispatch_called_with(!has_failed);
+ assert_eq!(to_post_dispatch, 1);
+ }
+ }
+
+ pub struct SecondFilterCall;
+
+ impl SecondFilterCall {
+ fn post_dispatch_called_with(success: bool) {
+ frame_support::storage::unhashed::put(&[2], &success);
+ }
+
+ fn verify_post_dispatch_called_with(success: bool) {
+ assert_eq!(frame_support::storage::unhashed::get::(&[2]), Some(success));
+ }
+ }
+
+ impl BridgeRuntimeFilterCall for SecondFilterCall {
+ type ToPostDispatch = u64;
+ fn validate(_who: &u64, call: &MockCall) -> (u64, TransactionValidity) {
+ if call.data <= 2 {
+ return (2, InvalidTransaction::Custom(2).into())
+ }
+
+ (2, Ok(ValidTransaction { priority: 2, ..Default::default() }))
+ }
+
+ fn post_dispatch(_who: &u64, has_failed: bool, to_post_dispatch: Self::ToPostDispatch) {
+ Self::post_dispatch_called_with(!has_failed);
+ assert_eq!(to_post_dispatch, 2);
+ }
+ }
+
+ fn test_lane_id() -> LaneId {
+ LaneId::new(1, 2)
+ }
+
+ fn initial_balance_of_relayer_account_at_this_chain() -> ThisChainBalance {
+ let test_stake: ThisChainBalance = TestStake::get();
+ ExistentialDeposit::get().saturating_add(test_stake * 100)
+ }
+
+ // in tests, the following accounts are equal (because of how `into_sub_account_truncating`
+ // works)
+
+ fn delivery_rewards_account() -> ThisChainAccountId {
+ TestPaymentProcedure::rewards_account(MsgProofsRewardsAccount::get())
+ }
+
+ fn confirmation_rewards_account() -> ThisChainAccountId {
+ TestPaymentProcedure::rewards_account(MsgDeliveryProofsRewardsAccount::get())
+ }
+
+ fn relayer_account_at_this_chain() -> ThisChainAccountId {
+ 0
+ }
+
+ fn initialize_environment(
+ best_relay_header_number: BridgedChainBlockNumber,
+ parachain_head_at_relay_header_number: BridgedChainBlockNumber,
+ best_message: MessageNonce,
+ ) {
+ let authorities = test_keyring().into_iter().map(|(a, w)| (a.into(), w)).collect();
+ let best_relay_header = HeaderId(best_relay_header_number, BridgedChainHash::default());
+ pallet_bridge_grandpa::CurrentAuthoritySet::::put(
+ StoredAuthoritySet::try_new(authorities, TEST_GRANDPA_SET_ID).unwrap(),
+ );
+ pallet_bridge_grandpa::BestFinalized::::put(best_relay_header);
+ pallet_bridge_grandpa::ImportedHeaders::::insert(
+ best_relay_header.hash(),
+ bp_test_utils::test_header::(0).build(),
+ );
+
+ let para_id = ParaId(BridgedUnderlyingParachain::PARACHAIN_ID);
+ let para_info = ParaInfo {
+ best_head_hash: BestParaHeadHash {
+ at_relay_block_number: parachain_head_at_relay_header_number,
+ head_hash: [parachain_head_at_relay_header_number as u8; 32].into(),
+ },
+ next_imported_hash_position: 0,
+ };
+ pallet_bridge_parachains::ParasInfo::::insert(para_id, para_info);
+
+ let lane_id = test_lane_id();
+ let in_lane_data =
+ InboundLaneData { last_confirmed_nonce: best_message, ..Default::default() };
+ pallet_bridge_messages::InboundLanes::::insert(lane_id, in_lane_data);
+
+ let out_lane_data =
+ OutboundLaneData { latest_received_nonce: best_message, ..Default::default() };
+ pallet_bridge_messages::OutboundLanes::::insert(lane_id, out_lane_data);
+
+ Balances::mint_into(&delivery_rewards_account(), ExistentialDeposit::get()).unwrap();
+ Balances::mint_into(&confirmation_rewards_account(), ExistentialDeposit::get()).unwrap();
+ Balances::mint_into(
+ &relayer_account_at_this_chain(),
+ initial_balance_of_relayer_account_at_this_chain(),
+ )
+ .unwrap();
+ }
+
+ fn submit_relay_header_call(relay_header_number: BridgedChainBlockNumber) -> RuntimeCall {
+ let relay_header = BridgedChainHeader::new(
+ relay_header_number,
+ Default::default(),
+ Default::default(),
+ Default::default(),
+ Default::default(),
+ );
+ let relay_justification = make_default_justification(&relay_header);
+
+ RuntimeCall::BridgeGrandpa(GrandpaCall::submit_finality_proof {
+ finality_target: Box::new(relay_header),
+ justification: relay_justification,
+ })
+ }
+
+ fn submit_parachain_head_call(
+ parachain_head_at_relay_header_number: BridgedChainBlockNumber,
+ ) -> RuntimeCall {
+ RuntimeCall::BridgeParachains(ParachainsCall::submit_parachain_heads {
+ at_relay_block: (parachain_head_at_relay_header_number, BridgedChainHash::default()),
+ parachains: vec![(
+ ParaId(BridgedUnderlyingParachain::PARACHAIN_ID),
+ [parachain_head_at_relay_header_number as u8; 32].into(),
+ )],
+ parachain_heads_proof: ParaHeadsProof { storage_proof: Default::default() },
+ })
+ }
+
+ #[test]
+ fn test_generated_obsolete_extension() {
+ generate_bridge_reject_obsolete_headers_and_messages!(
+ MockCall,
+ u64,
+ FirstFilterCall,
+ SecondFilterCall
+ );
+
+ run_test(|| {
+ assert_err!(
+ BridgeRejectObsoleteHeadersAndMessages.validate(&42, &MockCall { data: 1 }, &(), 0),
+ InvalidTransaction::Custom(1)
+ );
+ assert_err!(
+ BridgeRejectObsoleteHeadersAndMessages.pre_dispatch(
+ &42,
+ &MockCall { data: 1 },
+ &(),
+ 0
+ ),
+ InvalidTransaction::Custom(1)
+ );
+
+ assert_err!(
+ BridgeRejectObsoleteHeadersAndMessages.validate(&42, &MockCall { data: 2 }, &(), 0),
+ InvalidTransaction::Custom(2)
+ );
+ assert_err!(
+ BridgeRejectObsoleteHeadersAndMessages.pre_dispatch(
+ &42,
+ &MockCall { data: 2 },
+ &(),
+ 0
+ ),
+ InvalidTransaction::Custom(2)
+ );
+
+ assert_eq!(
+ BridgeRejectObsoleteHeadersAndMessages
+ .validate(&42, &MockCall { data: 3 }, &(), 0)
+ .unwrap(),
+ ValidTransaction { priority: 3, ..Default::default() },
+ );
+ assert_eq!(
+ BridgeRejectObsoleteHeadersAndMessages
+ .pre_dispatch(&42, &MockCall { data: 3 }, &(), 0)
+ .unwrap(),
+ (42, (1, 2)),
+ );
+
+ // when post_dispatch is called with `Ok(())`, it is propagated to all "nested"
+ // extensions
+ assert_ok!(BridgeRejectObsoleteHeadersAndMessages::post_dispatch(
+ Some((0, (1, 2))),
+ &(),
+ &(),
+ 0,
+ &Ok(())
+ ));
+ FirstFilterCall::verify_post_dispatch_called_with(true);
+ SecondFilterCall::verify_post_dispatch_called_with(true);
+
+ // when post_dispatch is called with `Err(())`, it is propagated to all "nested"
+ // extensions
+ assert_ok!(BridgeRejectObsoleteHeadersAndMessages::post_dispatch(
+ Some((0, (1, 2))),
+ &(),
+ &(),
+ 0,
+ &Err(DispatchError::BadOrigin)
+ ));
+ FirstFilterCall::verify_post_dispatch_called_with(false);
+ SecondFilterCall::verify_post_dispatch_called_with(false);
+ });
+ }
+
+ frame_support::parameter_types! {
+ pub SlashDestination: ThisChainAccountId = 42;
+ }
+
+ type BridgeGrandpaWrapper =
+ CheckAndBoostBridgeGrandpaTransactions, SlashDestination>;
+
+ #[test]
+ fn grandpa_wrapper_does_not_boost_extensions_for_unregistered_relayer() {
+ run_test(|| {
+ initialize_environment(100, 100, 100);
+
+ let priority_boost = BridgeGrandpaWrapper::validate(
+ &relayer_account_at_this_chain(),
+ &submit_relay_header_call(200),
+ )
+ .1
+ .unwrap()
+ .priority;
+ assert_eq!(priority_boost, 0);
+ })
+ }
+
+ #[test]
+ fn grandpa_wrapper_boosts_extensions_for_registered_relayer() {
+ run_test(|| {
+ initialize_environment(100, 100, 100);
+ BridgeRelayers::register(RuntimeOrigin::signed(relayer_account_at_this_chain()), 1000)
+ .unwrap();
+
+ let priority_boost = BridgeGrandpaWrapper::validate(
+ &relayer_account_at_this_chain(),
+ &submit_relay_header_call(200),
+ )
+ .1
+ .unwrap()
+ .priority;
+ assert_eq!(priority_boost, 99_000);
+ })
+ }
+
+ #[test]
+ fn grandpa_wrapper_slashes_registered_relayer_if_transaction_fails() {
+ run_test(|| {
+ initialize_environment(100, 100, 100);
+ BridgeRelayers::register(RuntimeOrigin::signed(relayer_account_at_this_chain()), 1000)
+ .unwrap();
+
+ assert!(BridgeRelayers::is_registration_active(&relayer_account_at_this_chain()));
+ BridgeGrandpaWrapper::post_dispatch(&relayer_account_at_this_chain(), true, Some(150));
+ assert!(!BridgeRelayers::is_registration_active(&relayer_account_at_this_chain()));
+ })
+ }
+
+ #[test]
+ fn grandpa_wrapper_does_not_slash_registered_relayer_if_transaction_succeeds() {
+ run_test(|| {
+ initialize_environment(100, 100, 100);
+ BridgeRelayers::register(RuntimeOrigin::signed(relayer_account_at_this_chain()), 1000)
+ .unwrap();
+
+ assert!(BridgeRelayers::is_registration_active(&relayer_account_at_this_chain()));
+ BridgeGrandpaWrapper::post_dispatch(&relayer_account_at_this_chain(), false, Some(100));
+ assert!(BridgeRelayers::is_registration_active(&relayer_account_at_this_chain()));
+ })
+ }
+
+ type BridgeParachainsWrapper = CheckAndBoostBridgeParachainsTransactions<
+ TestRuntime,
+ (),
+ BridgedUnderlyingParachain,
+ ConstU64<1_000>,
+ SlashDestination,
+ >;
+
+ #[test]
+ fn parachains_wrapper_does_not_boost_extensions_for_unregistered_relayer() {
+ run_test(|| {
+ initialize_environment(100, 100, 100);
+
+ let priority_boost = BridgeParachainsWrapper::validate(
+ &relayer_account_at_this_chain(),
+ &submit_parachain_head_call(200),
+ )
+ .1
+ .unwrap()
+ .priority;
+ assert_eq!(priority_boost, 0);
+ })
+ }
+
+ #[test]
+ fn parachains_wrapper_boosts_extensions_for_registered_relayer() {
+ run_test(|| {
+ initialize_environment(100, 100, 100);
+ BridgeRelayers::register(RuntimeOrigin::signed(relayer_account_at_this_chain()), 1000)
+ .unwrap();
+
+ let priority_boost = BridgeParachainsWrapper::validate(
+ &relayer_account_at_this_chain(),
+ &submit_parachain_head_call(200),
+ )
+ .1
+ .unwrap()
+ .priority;
+ assert_eq!(priority_boost, 99_000);
+ })
+ }
+
+ #[test]
+ fn parachains_wrapper_slashes_registered_relayer_if_transaction_fails() {
+ run_test(|| {
+ initialize_environment(100, 100, 100);
+ BridgeRelayers::register(RuntimeOrigin::signed(relayer_account_at_this_chain()), 1000)
+ .unwrap();
+
+ assert!(BridgeRelayers::is_registration_active(&relayer_account_at_this_chain()));
+ BridgeParachainsWrapper::post_dispatch(
+ &relayer_account_at_this_chain(),
+ true,
+ Some(SubmitParachainHeadsInfo {
+ at_relay_block: HeaderId(150, Default::default()),
+ para_id: ParaId(BridgedUnderlyingParachain::PARACHAIN_ID),
+ para_head_hash: [150u8; 32].into(),
+ is_free_execution_expected: false,
+ }),
+ );
+ assert!(!BridgeRelayers::is_registration_active(&relayer_account_at_this_chain()));
+ })
+ }
+
+ #[test]
+ fn parachains_wrapper_does_not_slash_registered_relayer_if_transaction_succeeds() {
+ run_test(|| {
+ initialize_environment(100, 100, 100);
+ BridgeRelayers::register(RuntimeOrigin::signed(relayer_account_at_this_chain()), 1000)
+ .unwrap();
+
+ assert!(BridgeRelayers::is_registration_active(&relayer_account_at_this_chain()));
+ BridgeParachainsWrapper::post_dispatch(
+ &relayer_account_at_this_chain(),
+ false,
+ Some(SubmitParachainHeadsInfo {
+ at_relay_block: HeaderId(100, Default::default()),
+ para_id: ParaId(BridgedUnderlyingParachain::PARACHAIN_ID),
+ para_head_hash: [100u8; 32].into(),
+ is_free_execution_expected: false,
+ }),
+ );
+ assert!(BridgeRelayers::is_registration_active(&relayer_account_at_this_chain()));
+ })
+ }
+}
diff --git a/bridges/bin/runtime-common/src/extensions/check_obsolete_extension.rs b/bridges/bin/runtime-common/src/extensions/check_obsolete_extension.rs
deleted file mode 100644
index 52e79086bc7f5..0000000000000
--- a/bridges/bin/runtime-common/src/extensions/check_obsolete_extension.rs
+++ /dev/null
@@ -1,659 +0,0 @@
-// Copyright (C) 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 .
-
-//! Transaction extension that rejects bridge-related transactions, that include
-//! obsolete (duplicated) data or do not pass some additional pallet-specific
-//! checks.
-
-use pallet_bridge_grandpa::CallSubType as GrandpaCallSubType;
-use pallet_bridge_messages::CallSubType as MessagesCallSubType;
-use pallet_bridge_parachains::CallSubType as ParachainsCallSubtype;
-use sp_runtime::transaction_validity::{TransactionValidity, ValidTransactionBuilder};
-
-/// A duplication of the `FilterCall` trait.
-///
-/// We need this trait in order to be able to implement it for the messages pallet,
-/// since the implementation is done outside of the pallet crate.
-pub trait BridgeRuntimeFilterCall {
- /// Data that may be passed from the validate to `post_dispatch`.
- type ToPostDispatch;
- /// Called during validation. Needs to checks whether a runtime call, submitted
- /// by the `who` is valid. `who` may be `None` if transaction is not signed
- /// by a regular account.
- fn validate(who: &AccountId, call: &Call) -> (Self::ToPostDispatch, TransactionValidity);
- /// Called after transaction is dispatched.
- fn post_dispatch(_who: &AccountId, _has_failed: bool, _to_post_dispatch: Self::ToPostDispatch) {
- }
-}
-
-// TODO:(bridges-v2) - most of that stuff was introduced with free header execution: https://github.com/paritytech/polkadot-sdk/pull/4102
-// /// Wrapper for the bridge GRANDPA pallet that checks calls for obsolete submissions
-// /// and also boosts transaction priority if it has submitted by registered relayer.
-// /// The boost is computed as
-// /// `(BundledHeaderNumber - 1 - BestFinalizedHeaderNumber) * Priority::get()`.
-// /// The boost is only applied if submitter has active registration in the relayers
-// /// pallet.
-// pub struct CheckAndBoostBridgeGrandpaTransactions(
-// PhantomData<(T, I, Priority, SlashAccount)>,
-// );
-//
-// impl, SlashAccount: Get>
-// BridgeRuntimeFilterCall
-// for CheckAndBoostBridgeGrandpaTransactions
-// where
-// T: pallet_bridge_relayers::Config + pallet_bridge_grandpa::Config,
-// T::RuntimeCall: GrandpaCallSubType,
-// {
-// // bridged header number, bundled in transaction
-// type ToPostDispatch = Option>;
-//
-// fn validate(
-// who: &T::AccountId,
-// call: &T::RuntimeCall,
-// ) -> (Self::ToPostDispatch, TransactionValidity) {
-// match GrandpaCallSubType::::check_obsolete_submit_finality_proof(call) {
-// Ok(Some(our_tx)) => {
-// let to_post_dispatch = Some(our_tx.base.block_number);
-// let total_priority_boost =
-// compute_priority_boost::(who, our_tx.improved_by);
-// (
-// to_post_dispatch,
-// ValidTransactionBuilder::default().priority(total_priority_boost).build(),
-// )
-// },
-// Ok(None) => (None, ValidTransactionBuilder::default().build()),
-// Err(e) => (None, Err(e)),
-// }
-// }
-//
-// fn post_dispatch(
-// relayer: &T::AccountId,
-// has_failed: bool,
-// bundled_block_number: Self::ToPostDispatch,
-// ) {
-// // we are only interested in associated pallet submissions
-// let Some(bundled_block_number) = bundled_block_number else { return };
-// // we are only interested in failed or unneeded transactions
-// let has_failed =
-// has_failed || !SubmitFinalityProofHelper::::was_successful(bundled_block_number);
-//
-// if !has_failed {
-// return
-// }
-//
-// // let's slash registered relayer
-// RelayersPallet::::slash_and_deregister(
-// relayer,
-// ExplicitOrAccountParams::Explicit(SlashAccount::get()),
-// );
-// }
-// }
-//
-// /// Wrapper for the bridge parachains pallet that checks calls for obsolete submissions
-// /// and also boosts transaction priority if it has submitted by registered relayer.
-// /// The boost is computed as
-// /// `(BundledHeaderNumber - 1 - BestKnownHeaderNumber) * Priority::get()`.
-// /// The boost is only applied if submitter has active registration in the relayers
-// /// pallet.
-// pub struct CheckAndBoostBridgeParachainsTransactions(
-// PhantomData<(T, RefPara, Priority, SlashAccount)>,
-// );
-//
-// impl, SlashAccount: Get>
-// BridgeRuntimeFilterCall
-// for CheckAndBoostBridgeParachainsTransactions
-// where
-// T: pallet_bridge_relayers::Config + pallet_bridge_parachains::Config,
-// RefPara: RefundableParachainId,
-// T::RuntimeCall: ParachainsCallSubtype,
-// {
-// // bridged header number, bundled in transaction
-// type ToPostDispatch = Option;
-//
-// fn validate(
-// who: &T::AccountId,
-// call: &T::RuntimeCall,
-// ) -> (Self::ToPostDispatch, TransactionValidity) {
-// match ParachainsCallSubtype::::check_obsolete_submit_parachain_heads(
-// call,
-// ) {
-// Ok(Some(our_tx)) if our_tx.base.para_id.0 == RefPara::BridgedChain::PARACHAIN_ID => {
-// let to_post_dispatch = Some(our_tx.base);
-// let total_priority_boost =
-// compute_priority_boost::(&who, our_tx.improved_by);
-// (
-// to_post_dispatch,
-// ValidTransactionBuilder::default().priority(total_priority_boost).build(),
-// )
-// },
-// Ok(_) => (None, ValidTransactionBuilder::default().build()),
-// Err(e) => (None, Err(e)),
-// }
-// }
-//
-// fn post_dispatch(relayer: &T::AccountId, has_failed: bool, maybe_update: Self::ToPostDispatch) {
-// // we are only interested in associated pallet submissions
-// let Some(update) = maybe_update else { return };
-// // we are only interested in failed or unneeded transactions
-// let has_failed = has_failed ||
-// !SubmitParachainHeadsHelper::::was_successful(&update);
-//
-// if !has_failed {
-// return
-// }
-//
-// // let's slash registered relayer
-// RelayersPallet::::slash_and_deregister(
-// relayer,
-// ExplicitOrAccountParams::Explicit(SlashAccount::get()),
-// );
-// }
-// }
-
-impl BridgeRuntimeFilterCall
- for pallet_bridge_grandpa::Pallet
-where
- T: pallet_bridge_grandpa::Config,
- T::RuntimeCall: GrandpaCallSubType,
-{
- type ToPostDispatch = ();
- fn validate(_who: &T::AccountId, call: &T::RuntimeCall) -> ((), TransactionValidity) {
- (
- (),
- GrandpaCallSubType::::check_obsolete_submit_finality_proof(call)
- .and_then(|_| ValidTransactionBuilder::default().build()),
- )
- }
-}
-
-impl BridgeRuntimeFilterCall
- for pallet_bridge_parachains::Pallet
-where
- T: pallet_bridge_parachains::Config,
- T::RuntimeCall: ParachainsCallSubtype,
-{
- type ToPostDispatch = ();
- fn validate(_who: &T::AccountId, call: &T::RuntimeCall) -> ((), TransactionValidity) {
- (
- (),
- ParachainsCallSubtype::::check_obsolete_submit_parachain_heads(call)
- .and_then(|_| ValidTransactionBuilder::default().build()),
- )
- }
-}
-
-impl, I: 'static>
- BridgeRuntimeFilterCall for pallet_bridge_messages::Pallet
-where
- T::RuntimeCall: MessagesCallSubType,
-{
- type ToPostDispatch = ();
- /// Validate messages in order to avoid "mining" messages delivery and delivery confirmation
- /// transactions, that are delivering outdated messages/confirmations. Without this validation,
- /// even honest relayers may lose their funds if there are multiple relays running and
- /// submitting the same messages/confirmations.
- fn validate(_who: &T::AccountId, call: &T::RuntimeCall) -> ((), TransactionValidity) {
- ((), call.check_obsolete_call())
- }
-}
-
-// TODO:(bridges-v2) - most of that stuff was introduced with free header execution: https://github.com/paritytech/polkadot-sdk/pull/4102
-/// Computes priority boost that improved known header by `improved_by`
-// fn compute_priority_boost(
-// relayer: &T::AccountId,
-// improved_by: N,
-// ) -> TransactionPriority
-// where
-// T: pallet_bridge_relayers::Config,
-// N: UniqueSaturatedInto,
-// Priority: Get,
-// {
-// // we only boost priority if relayer has staked required balance
-// let is_relayer_registration_active = RelayersPallet::::is_registration_active(relayer);
-// // if tx improves by just one, there's no need to bump its priority
-// let improved_by: TransactionPriority = improved_by.unique_saturated_into().saturating_sub(1);
-// // if relayer is registered, for every skipped header we improve by `Priority`
-// let boost_per_header = if is_relayer_registration_active { Priority::get() } else { 0 };
-// improved_by.saturating_mul(boost_per_header)
-// }
-
-/// Declares a runtime-specific `BridgeRejectObsoleteHeadersAndMessages` signed extension.
-///
-/// ## Example
-///
-/// ```nocompile
-/// generate_bridge_reject_obsolete_headers_and_messages!{
-/// Call, AccountId
-/// BridgeRococoGrandpa, BridgeRococoMessages,
-/// BridgeRococoParachains
-/// }
-/// ```
-///
-/// The goal of this extension is to avoid "mining" transactions that provide outdated bridged
-/// headers and messages. Without that extension, even honest relayers may lose their funds if
-/// there are multiple relays running and submitting the same information.
-#[macro_export]
-macro_rules! generate_bridge_reject_obsolete_headers_and_messages {
- ($call:ty, $account_id:ty, $($filter_call:ty),*) => {
- #[derive(Clone, codec::Decode, Default, codec::Encode, Eq, PartialEq, sp_runtime::RuntimeDebug, scale_info::TypeInfo)]
- pub struct BridgeRejectObsoleteHeadersAndMessages;
- impl sp_runtime::traits::SignedExtension for BridgeRejectObsoleteHeadersAndMessages {
- const IDENTIFIER: &'static str = "BridgeRejectObsoleteHeadersAndMessages";
- type AccountId = $account_id;
- type Call = $call;
- type AdditionalSigned = ();
- type Pre = (
- $account_id,
- ( $(
- <$filter_call as $crate::extensions::check_obsolete_extension::BridgeRuntimeFilterCall<
- $account_id,
- $call,
- >>::ToPostDispatch,
- )* ),
- );
-
- fn additional_signed(&self) -> sp_std::result::Result<
- (),
- sp_runtime::transaction_validity::TransactionValidityError,
- > {
- Ok(())
- }
-
- #[allow(unused_variables)]
- fn validate(
- &self,
- who: &Self::AccountId,
- call: &Self::Call,
- _info: &sp_runtime::traits::DispatchInfoOf,
- _len: usize,
- ) -> sp_runtime::transaction_validity::TransactionValidity {
- let tx_validity = sp_runtime::transaction_validity::ValidTransaction::default();
- let to_prepare = ();
- $(
- let (from_validate, call_filter_validity) = <
- $filter_call as
- $crate::extensions::check_obsolete_extension::BridgeRuntimeFilterCall<
- Self::AccountId,
- $call,
- >>::validate(&who, call);
- let tx_validity = tx_validity.combine_with(call_filter_validity?);
- )*
- Ok(tx_validity)
- }
-
- #[allow(unused_variables)]
- fn pre_dispatch(
- self,
- relayer: &Self::AccountId,
- call: &Self::Call,
- info: &sp_runtime::traits::DispatchInfoOf,
- len: usize,
- ) -> Result {
- use tuplex::PushBack;
- let to_post_dispatch = ();
- $(
- let (from_validate, call_filter_validity) = <
- $filter_call as
- $crate::extensions::check_obsolete_extension::BridgeRuntimeFilterCall<
- $account_id,
- $call,
- >>::validate(&relayer, call);
- let _ = call_filter_validity?;
- let to_post_dispatch = to_post_dispatch.push_back(from_validate);
- )*
- Ok((relayer.clone(), to_post_dispatch))
- }
-
- #[allow(unused_variables)]
- fn post_dispatch(
- to_post_dispatch: Option,
- info: &sp_runtime::traits::DispatchInfoOf,
- post_info: &sp_runtime::traits::PostDispatchInfoOf,
- len: usize,
- result: &sp_runtime::DispatchResult,
- ) -> Result<(), sp_runtime::transaction_validity::TransactionValidityError> {
- use tuplex::PopFront;
- let Some((relayer, to_post_dispatch)) = to_post_dispatch else { return Ok(()) };
- let has_failed = result.is_err();
- $(
- let (item, to_post_dispatch) = to_post_dispatch.pop_front();
- <
- $filter_call as
- $crate::extensions::check_obsolete_extension::BridgeRuntimeFilterCall<
- $account_id,
- $call,
- >>::post_dispatch(&relayer, has_failed, item);
- )*
- Ok(())
- }
- }
- };
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::mock::*;
- use frame_support::{assert_err, assert_ok};
- use sp_runtime::{
- traits::SignedExtension,
- transaction_validity::{InvalidTransaction, TransactionValidity, ValidTransaction},
- DispatchError,
- };
-
- pub struct MockCall {
- data: u32,
- }
-
- impl sp_runtime::traits::Dispatchable for MockCall {
- type RuntimeOrigin = u64;
- type Config = ();
- type Info = ();
- type PostInfo = ();
-
- fn dispatch(
- self,
- _origin: Self::RuntimeOrigin,
- ) -> sp_runtime::DispatchResultWithInfo {
- unimplemented!()
- }
- }
-
- pub struct FirstFilterCall;
- impl FirstFilterCall {
- fn post_dispatch_called_with(success: bool) {
- frame_support::storage::unhashed::put(&[1], &success);
- }
-
- fn verify_post_dispatch_called_with(success: bool) {
- assert_eq!(frame_support::storage::unhashed::get::(&[1]), Some(success));
- }
- }
-
- impl BridgeRuntimeFilterCall for FirstFilterCall {
- type ToPostDispatch = u64;
- fn validate(_who: &u64, call: &MockCall) -> (u64, TransactionValidity) {
- if call.data <= 1 {
- return (1, InvalidTransaction::Custom(1).into())
- }
-
- (1, Ok(ValidTransaction { priority: 1, ..Default::default() }))
- }
-
- fn post_dispatch(_who: &u64, has_failed: bool, to_post_dispatch: Self::ToPostDispatch) {
- Self::post_dispatch_called_with(!has_failed);
- assert_eq!(to_post_dispatch, 1);
- }
- }
-
- pub struct SecondFilterCall;
-
- impl SecondFilterCall {
- fn post_dispatch_called_with(success: bool) {
- frame_support::storage::unhashed::put(&[2], &success);
- }
-
- fn verify_post_dispatch_called_with(success: bool) {
- assert_eq!(frame_support::storage::unhashed::get::(&[2]), Some(success));
- }
- }
-
- impl BridgeRuntimeFilterCall for SecondFilterCall {
- type ToPostDispatch = u64;
- fn validate(_who: &u64, call: &MockCall) -> (u64, TransactionValidity) {
- if call.data <= 2 {
- return (2, InvalidTransaction::Custom(2).into())
- }
-
- (2, Ok(ValidTransaction { priority: 2, ..Default::default() }))
- }
-
- fn post_dispatch(_who: &u64, has_failed: bool, to_post_dispatch: Self::ToPostDispatch) {
- Self::post_dispatch_called_with(!has_failed);
- assert_eq!(to_post_dispatch, 2);
- }
- }
-
- #[test]
- fn test_generated_obsolete_extension() {
- generate_bridge_reject_obsolete_headers_and_messages!(
- MockCall,
- u64,
- FirstFilterCall,
- SecondFilterCall
- );
-
- run_test(|| {
- assert_err!(
- BridgeRejectObsoleteHeadersAndMessages.validate(&42, &MockCall { data: 1 }, &(), 0),
- InvalidTransaction::Custom(1)
- );
- assert_err!(
- BridgeRejectObsoleteHeadersAndMessages.pre_dispatch(
- &42,
- &MockCall { data: 1 },
- &(),
- 0
- ),
- InvalidTransaction::Custom(1)
- );
-
- assert_err!(
- BridgeRejectObsoleteHeadersAndMessages.validate(&42, &MockCall { data: 2 }, &(), 0),
- InvalidTransaction::Custom(2)
- );
- assert_err!(
- BridgeRejectObsoleteHeadersAndMessages.pre_dispatch(
- &42,
- &MockCall { data: 2 },
- &(),
- 0
- ),
- InvalidTransaction::Custom(2)
- );
-
- assert_eq!(
- BridgeRejectObsoleteHeadersAndMessages
- .validate(&42, &MockCall { data: 3 }, &(), 0)
- .unwrap(),
- ValidTransaction { priority: 3, ..Default::default() },
- );
- assert_eq!(
- BridgeRejectObsoleteHeadersAndMessages
- .pre_dispatch(&42, &MockCall { data: 3 }, &(), 0)
- .unwrap(),
- (42, (1, 2)),
- );
-
- // when post_dispatch is called with `Ok(())`, it is propagated to all "nested"
- // extensions
- assert_ok!(BridgeRejectObsoleteHeadersAndMessages::post_dispatch(
- Some((0, (1, 2))),
- &(),
- &(),
- 0,
- &Ok(())
- ));
- FirstFilterCall::verify_post_dispatch_called_with(true);
- SecondFilterCall::verify_post_dispatch_called_with(true);
-
- // when post_dispatch is called with `Err(())`, it is propagated to all "nested"
- // extensions
- assert_ok!(BridgeRejectObsoleteHeadersAndMessages::post_dispatch(
- Some((0, (1, 2))),
- &(),
- &(),
- 0,
- &Err(DispatchError::BadOrigin)
- ));
- FirstFilterCall::verify_post_dispatch_called_with(false);
- SecondFilterCall::verify_post_dispatch_called_with(false);
- });
- }
-
- frame_support::parameter_types! {
- pub SlashDestination: ThisChainAccountId = 42;
- }
-
- // TODO:(bridges-v2) - most of that stuff was introduced with free header execution: https://github.com/paritytech/polkadot-sdk/pull/4102
- // type BridgeGrandpaWrapper =
- // CheckAndBoostBridgeGrandpaTransactions, SlashDestination>;
- //
- // #[test]
- // fn grandpa_wrapper_does_not_boost_extensions_for_unregistered_relayer() {
- // run_test(|| {
- // initialize_environment(100, 100, 100);
- //
- // let priority_boost = BridgeGrandpaWrapper::validate(
- // &relayer_account_at_this_chain(),
- // &submit_relay_header_call_ex(200),
- // )
- // .1
- // .unwrap()
- // .priority;
- // assert_eq!(priority_boost, 0);
- // })
- // }
- //
- // #[test]
- // fn grandpa_wrapper_boosts_extensions_for_registered_relayer() {
- // run_test(|| {
- // initialize_environment(100, 100, 100);
- // BridgeRelayers::register(RuntimeOrigin::signed(relayer_account_at_this_chain()), 1000)
- // .unwrap();
- //
- // let priority_boost = BridgeGrandpaWrapper::validate(
- // &relayer_account_at_this_chain(),
- // &submit_relay_header_call_ex(200),
- // )
- // .1
- // .unwrap()
- // .priority;
- // assert_eq!(priority_boost, 99_000);
- // })
- // }
- //
- // #[test]
- // fn grandpa_wrapper_slashes_registered_relayer_if_transaction_fails() {
- // run_test(|| {
- // initialize_environment(100, 100, 100);
- // BridgeRelayers::register(RuntimeOrigin::signed(relayer_account_at_this_chain()), 1000)
- // .unwrap();
- //
- // assert!(BridgeRelayers::is_registration_active(&relayer_account_at_this_chain()));
- // BridgeGrandpaWrapper::post_dispatch(&relayer_account_at_this_chain(), true, Some(150));
- // assert!(!BridgeRelayers::is_registration_active(&relayer_account_at_this_chain()));
- // })
- // }
- //
- // #[test]
- // fn grandpa_wrapper_does_not_slash_registered_relayer_if_transaction_succeeds() {
- // run_test(|| {
- // initialize_environment(100, 100, 100);
- // BridgeRelayers::register(RuntimeOrigin::signed(relayer_account_at_this_chain()), 1000)
- // .unwrap();
- //
- // assert!(BridgeRelayers::is_registration_active(&relayer_account_at_this_chain()));
- // BridgeGrandpaWrapper::post_dispatch(&relayer_account_at_this_chain(), false, Some(100));
- // assert!(BridgeRelayers::is_registration_active(&relayer_account_at_this_chain()));
- // })
- // }
- //
- // type BridgeParachainsWrapper = CheckAndBoostBridgeParachainsTransactions<
- // TestRuntime,
- // RefundableParachain<(), BridgedUnderlyingParachain>,
- // ConstU64<1_000>,
- // SlashDestination,
- // >;
- //
- // #[test]
- // fn parachains_wrapper_does_not_boost_extensions_for_unregistered_relayer() {
- // run_test(|| {
- // initialize_environment(100, 100, 100);
- //
- // let priority_boost = BridgeParachainsWrapper::validate(
- // &relayer_account_at_this_chain(),
- // &submit_parachain_head_call_ex(200),
- // )
- // .1
- // .unwrap()
- // .priority;
- // assert_eq!(priority_boost, 0);
- // })
- // }
- //
- // #[test]
- // fn parachains_wrapper_boosts_extensions_for_registered_relayer() {
- // run_test(|| {
- // initialize_environment(100, 100, 100);
- // BridgeRelayers::register(RuntimeOrigin::signed(relayer_account_at_this_chain()), 1000)
- // .unwrap();
- //
- // let priority_boost = BridgeParachainsWrapper::validate(
- // &relayer_account_at_this_chain(),
- // &submit_parachain_head_call_ex(200),
- // )
- // .1
- // .unwrap()
- // .priority;
- // assert_eq!(priority_boost, 99_000);
- // })
- // }
- //
- // #[test]
- // fn parachains_wrapper_slashes_registered_relayer_if_transaction_fails() {
- // run_test(|| {
- // initialize_environment(100, 100, 100);
- // BridgeRelayers::register(RuntimeOrigin::signed(relayer_account_at_this_chain()), 1000)
- // .unwrap();
- //
- // assert!(BridgeRelayers::is_registration_active(&relayer_account_at_this_chain()));
- // BridgeParachainsWrapper::post_dispatch(
- // &relayer_account_at_this_chain(),
- // true,
- // Some(SubmitParachainHeadsInfo {
- // at_relay_block: HeaderId(150, Default::default()),
- // para_id: ParaId(BridgedUnderlyingParachain::PARACHAIN_ID),
- // para_head_hash: [150u8; 32].into(),
- // is_free_execution_expected: false,
- // }),
- // );
- // assert!(!BridgeRelayers::is_registration_active(&relayer_account_at_this_chain()));
- // })
- // }
- //
- // #[test]
- // fn parachains_wrapper_does_not_slash_registered_relayer_if_transaction_succeeds() {
- // run_test(|| {
- // initialize_environment(100, 100, 100);
- // BridgeRelayers::register(RuntimeOrigin::signed(relayer_account_at_this_chain()), 1000)
- // .unwrap();
- //
- // assert!(BridgeRelayers::is_registration_active(&relayer_account_at_this_chain()));
- // BridgeParachainsWrapper::post_dispatch(
- // &relayer_account_at_this_chain(),
- // false,
- // Some(SubmitParachainHeadsInfo {
- // at_relay_block: HeaderId(100, Default::default()),
- // para_id: ParaId(BridgedUnderlyingParachain::PARACHAIN_ID),
- // para_head_hash: [100u8; 32].into(),
- // is_free_execution_expected: false,
- // }),
- // );
- // assert!(BridgeRelayers::is_registration_active(&relayer_account_at_this_chain()));
- // })
- // }
-}
diff --git a/bridges/bin/runtime-common/src/extensions/mod.rs b/bridges/bin/runtime-common/src/extensions/mod.rs
deleted file mode 100644
index 690cfe1c8c680..0000000000000
--- a/bridges/bin/runtime-common/src/extensions/mod.rs
+++ /dev/null
@@ -1,19 +0,0 @@
-// Copyright (C) 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 .
-
-//! Bridge-specific transaction extensions.
-
-pub mod check_obsolete_extension;
diff --git a/bridges/bin/runtime-common/src/lib.rs b/bridges/bin/runtime-common/src/lib.rs
index de9ee1b0bbbca..ac8b013086b1b 100644
--- a/bridges/bin/runtime-common/src/lib.rs
+++ b/bridges/bin/runtime-common/src/lib.rs
@@ -20,7 +20,6 @@
#![cfg_attr(not(feature = "std"), no_std)]
pub mod extensions;
-
pub mod messages_api;
pub mod messages_benchmarking;
pub mod parachains_benchmarking;
diff --git a/bridges/modules/relayers/src/extension/grandpa_adapter.rs b/bridges/modules/relayers/src/extension/grandpa_adapter.rs
index f784d0e9ea9a4..6c9ae1c2968c0 100644
--- a/bridges/modules/relayers/src/extension/grandpa_adapter.rs
+++ b/bridges/modules/relayers/src/extension/grandpa_adapter.rs
@@ -84,7 +84,6 @@ where
{
type IdProvider = ID;
type Runtime = R;
- type BatchCallUnpacker = BCU;
type BridgeMessagesPalletInstance = MI;
type PriorityBoostPerMessage = P;
type Reward = R::Reward;
@@ -96,7 +95,7 @@ where
Option>,
TransactionValidityError,
> {
- let calls = Self::BatchCallUnpacker::unpack(call, 2);
+ let calls = BCU::unpack(call, 2);
let total_calls = calls.len();
let mut calls = calls.into_iter().map(Self::check_obsolete_parsed_call).rev();
diff --git a/bridges/modules/relayers/src/extension/messages_adapter.rs b/bridges/modules/relayers/src/extension/messages_adapter.rs
new file mode 100644
index 0000000000000..f84e25ba5aa17
--- /dev/null
+++ b/bridges/modules/relayers/src/extension/messages_adapter.rs
@@ -0,0 +1,94 @@
+// Copyright (C) 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 .
+
+//! Adapter that allows using `pallet-bridge-relayers` as a signed extension in the
+//! bridge with any remote chain. This adapter does not refund any finality transactions.
+
+use crate::{extension::verify_messages_call_succeeded, Config as BridgeRelayersConfig};
+
+use bp_relayers::{ExtensionCallData, ExtensionCallInfo, ExtensionConfig};
+use bp_runtime::StaticStrProvider;
+use frame_support::dispatch::{DispatchInfo, PostDispatchInfo};
+use pallet_bridge_messages::{
+ CallSubType as BridgeMessagesCallSubType, Config as BridgeMessagesConfig,
+};
+use sp_runtime::{
+ traits::{Dispatchable, Get},
+ transaction_validity::{TransactionPriority, TransactionValidityError},
+};
+use sp_std::marker::PhantomData;
+
+/// Transaction extension that refunds a relayer for standalone messages delivery and confirmation
+/// transactions. Finality transactions are not refunded.
+pub struct MessagesExtensionConfig<
+ IdProvider,
+ Runtime,
+ BridgeMessagesPalletInstance,
+ PriorityBoostPerMessage,
+>(
+ PhantomData<(
+ // signed extension identifier
+ IdProvider,
+ // runtime with `pallet-bridge-messages` pallet deployed
+ Runtime,
+ // instance of BridgedChain `pallet-bridge-messages`, tracked by this extension
+ BridgeMessagesPalletInstance,
+ // message delivery transaction priority boost for every additional message
+ PriorityBoostPerMessage,
+ )>,
+);
+
+impl ExtensionConfig for MessagesExtensionConfig
+where
+ ID: StaticStrProvider,
+ R: BridgeRelayersConfig + BridgeMessagesConfig,
+ MI: 'static,
+ P: Get,
+ R::RuntimeCall: Dispatchable
+ + BridgeMessagesCallSubType,
+{
+ type IdProvider = ID;
+ type Runtime = R;
+ type BridgeMessagesPalletInstance = MI;
+ type PriorityBoostPerMessage = P;
+ type Reward = R::Reward;
+ type RemoteGrandpaChainBlockNumber = ();
+
+ fn parse_and_check_for_obsolete_call(
+ call: &R::RuntimeCall,
+ ) -> Result<
+ Option>,
+ TransactionValidityError,
+ > {
+ let call = Self::check_obsolete_parsed_call(call)?;
+ Ok(call.call_info().map(ExtensionCallInfo::Msgs))
+ }
+
+ fn check_obsolete_parsed_call(
+ call: &R::RuntimeCall,
+ ) -> Result<&R::RuntimeCall, TransactionValidityError> {
+ call.check_obsolete_call()?;
+ Ok(call)
+ }
+
+ fn check_call_result(
+ call_info: &ExtensionCallInfo,
+ call_data: &mut ExtensionCallData,
+ relayer: &R::AccountId,
+ ) -> bool {
+ verify_messages_call_succeeded::(call_info, call_data, relayer)
+ }
+}
diff --git a/bridges/modules/relayers/src/extension/mod.rs b/bridges/modules/relayers/src/extension/mod.rs
index 865e8a2a8c62a..33d854bcf8138 100644
--- a/bridges/modules/relayers/src/extension/mod.rs
+++ b/bridges/modules/relayers/src/extension/mod.rs
@@ -51,10 +51,12 @@ use sp_runtime::{
use sp_std::{fmt::Debug, marker::PhantomData};
pub use grandpa_adapter::WithGrandpaChainExtensionConfig;
+pub use messages_adapter::MessagesExtensionConfig;
pub use parachain_adapter::WithParachainExtensionConfig;
pub use priority::*;
mod grandpa_adapter;
+mod messages_adapter;
mod parachain_adapter;
mod priority;
diff --git a/bridges/modules/relayers/src/extension/parachain_adapter.rs b/bridges/modules/relayers/src/extension/parachain_adapter.rs
index 60b2f0f4e2d01..b6f57cebc309d 100644
--- a/bridges/modules/relayers/src/extension/parachain_adapter.rs
+++ b/bridges/modules/relayers/src/extension/parachain_adapter.rs
@@ -90,7 +90,6 @@ where
{
type IdProvider = ID;
type Runtime = R;
- type BatchCallUnpacker = BCU;
type BridgeMessagesPalletInstance = MI;
type PriorityBoostPerMessage = P;
type Reward = R::Reward;
@@ -103,8 +102,7 @@ where
Option>,
TransactionValidityError,
> {
- log::trace!(target: LOG_TARGET, "=== call: {:?}", call);
- let calls = Self::BatchCallUnpacker::unpack(call, 3);
+ let calls = BCU::unpack(call, 3);
let total_calls = calls.len();
let mut calls = calls.into_iter().map(Self::check_obsolete_parsed_call).rev();
@@ -117,7 +115,6 @@ where
});
let relay_finality_call =
calls.next().transpose()?.and_then(|c| c.submit_finality_proof_info());
- log::trace!(target: LOG_TARGET, "=== {} {:?} {:?} {:?}", total_calls, relay_finality_call, para_finality_call, msgs_call);
Ok(match (total_calls, relay_finality_call, para_finality_call, msgs_call) {
(3, Some(relay_finality_call), Some(para_finality_call), Some(msgs_call)) =>
Some(ExtensionCallInfo::AllFinalityAndMsgs(
diff --git a/bridges/modules/relayers/src/extension/priority.rs b/bridges/modules/relayers/src/extension/priority.rs
index f96d8632592f5..da188eaf5bdd4 100644
--- a/bridges/modules/relayers/src/extension/priority.rs
+++ b/bridges/modules/relayers/src/extension/priority.rs
@@ -26,6 +26,7 @@ use frame_support::traits::Get;
use sp_runtime::transaction_validity::TransactionPriority;
// reexport everything from `integrity_tests` module
+#[allow(unused_imports)]
pub use integrity_tests::*;
/// We'll deal with different bridge items here - messages, headers, ...
@@ -237,7 +238,12 @@ mod integrity_tests {
/// almost the same priority if we'll add `tip_boost_per_header` tip to the `TX1`. We want
/// to be sure that if we add plain `PriorityBoostPerHeader` priority to `TX1`, the priority
/// will be close to `TX2` as well.
- pub fn ensure_priority_boost_is_sane(
+ pub fn ensure_priority_boost_is_sane<
+ Runtime,
+ ParachainsInstance,
+ Para,
+ PriorityBoostPerHeader,
+ >(
tip_boost_per_header: BalanceOf,
) where
Runtime: pallet_transaction_payment::Config
@@ -271,7 +277,11 @@ mod integrity_tests {
/// Estimate parachain header delivery transaction priority.
#[cfg(feature = "integrity-test")]
- fn estimate_parachain_header_submit_transaction_priority(
+ fn estimate_parachain_header_submit_transaction_priority<
+ Runtime,
+ ParachainsInstance,
+ Para,
+ >(
tip: BalanceOf,
) -> TransactionPriority
where
diff --git a/bridges/primitives/relayers/src/extension.rs b/bridges/primitives/relayers/src/extension.rs
index 91d0a92b0de31..5ab8e6cde96b4 100644
--- a/bridges/primitives/relayers/src/extension.rs
+++ b/bridges/primitives/relayers/src/extension.rs
@@ -119,8 +119,6 @@ pub trait ExtensionConfig {
/// Runtime that optionally supports batched calls. We assume that batched call
/// succeeds if and only if all of its nested calls succeed.
type Runtime: frame_system::Config;
- /// Batch calls unpacker.
- type BatchCallUnpacker: BatchCallUnpacker;
/// Messages pallet instance.
type BridgeMessagesPalletInstance: 'static;
/// Additional priority that is added to base message delivery transaction priority