From bcc6204ac99ce1ceb0342c55a2b7995a1a795664 Mon Sep 17 00:00:00 2001 From: Svyatoslav Nikolsky Date: Thu, 23 May 2024 15:59:38 +0300 Subject: [PATCH] fix extensions --- bridges/bin/runtime-common/src/extensions.rs | 797 ++++++++++++++++++ .../extensions/check_obsolete_extension.rs | 659 --------------- .../bin/runtime-common/src/extensions/mod.rs | 19 - bridges/bin/runtime-common/src/lib.rs | 1 - .../relayers/src/extension/grandpa_adapter.rs | 3 +- .../src/extension/messages_adapter.rs | 94 +++ bridges/modules/relayers/src/extension/mod.rs | 2 + .../src/extension/parachain_adapter.rs | 5 +- .../relayers/src/extension/priority.rs | 14 +- bridges/primitives/relayers/src/extension.rs | 2 - 10 files changed, 907 insertions(+), 689 deletions(-) create mode 100644 bridges/bin/runtime-common/src/extensions.rs delete mode 100644 bridges/bin/runtime-common/src/extensions/check_obsolete_extension.rs delete mode 100644 bridges/bin/runtime-common/src/extensions/mod.rs create mode 100644 bridges/modules/relayers/src/extension/messages_adapter.rs 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