// Copyright (C) Parity Technologies (UK) Ltd. and Dijital Kurdistan Tech Institute // 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 pezpallet-specific //! checks. use pezbp_relayers::ExplicitOrAccountParams; use pezbp_runtime::Teyrchain; use pezbp_teyrchains::SubmitTeyrchainHeadsInfo; use pezpallet_bridge_grandpa::{ BridgedBlockNumber, CallSubType as GrandpaCallSubType, SubmitFinalityProofHelper, }; use pezpallet_bridge_messages::CallSubType as MessagesCallSubType; use pezpallet_bridge_relayers::Pezpallet as RelayersPallet; use pezpallet_bridge_teyrchains::{ CallSubType as TeyrchainsCallSubtype, SubmitTeyrchainHeadsHelper, }; use pezsp_runtime::{ traits::{Get, UniqueSaturatedInto}, transaction_validity::{TransactionPriority, TransactionValidity, ValidTransactionBuilder}, }; use pezsp_std::marker::PhantomData; // Re-export to avoid include tuplex dependency everywhere. #[doc(hidden)] pub mod __private { pub use tuplex; } /// A duplication of the `FilterCall` trait. /// /// We need this trait in order to be able to implement it for the messages pezpallet, /// since the implementation is done outside of the pezpallet 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. Transactions not signed are not validated. 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 pezpallet 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 /// pezpallet. pub struct CheckAndBoostBridgeGrandpaTransactions( PhantomData<(T, I, Priority, SlashAccount)>, ); impl, SlashAccount: Get> BridgeRuntimeFilterCall for CheckAndBoostBridgeGrandpaTransactions where T: pezpallet_bridge_relayers::Config + pezpallet_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 pezpallet 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 teyrchains pezpallet 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 /// pezpallet. pub struct CheckAndBoostBridgeTeyrchainsTransactions< T, TeyrchainsInstance, Para, Priority, SlashAccount, >(PhantomData<(T, TeyrchainsInstance, Para, Priority, SlashAccount)>); impl< T, TeyrchainsInstance, Para, Priority: Get, SlashAccount: Get, > BridgeRuntimeFilterCall for CheckAndBoostBridgeTeyrchainsTransactions where T: pezpallet_bridge_relayers::Config + pezpallet_bridge_teyrchains::Config, TeyrchainsInstance: 'static, Para: Teyrchain, T::RuntimeCall: TeyrchainsCallSubtype, { // bridged header number, bundled in transaction type ToPostDispatch = Option; fn validate( who: &T::AccountId, call: &T::RuntimeCall, ) -> (Self::ToPostDispatch, TransactionValidity) { match TeyrchainsCallSubtype::::check_obsolete_submit_teyrchain_heads( call, ) { Ok(Some(our_tx)) if our_tx.base.para_id.0 == Para::TEYRCHAIN_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 pezpallet submissions let Some(update) = maybe_update else { return }; // we are only interested in failed or unneeded transactions let has_failed = has_failed || !SubmitTeyrchainHeadsHelper::::was_successful(&update); if !has_failed { return; } // let's slash registered relayer RelayersPallet::::slash_and_deregister( relayer, ExplicitOrAccountParams::Explicit::<_, ()>(SlashAccount::get()), ); } } impl BridgeRuntimeFilterCall for pezpallet_bridge_grandpa::Pezpallet where T: pezpallet_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 pezpallet_bridge_teyrchains::Pezpallet where T: pezpallet_bridge_teyrchains::Config, T::RuntimeCall: TeyrchainsCallSubtype, { type ToPostDispatch = (); fn validate(_who: &T::AccountId, call: &T::RuntimeCall) -> ((), TransactionValidity) { ( (), TeyrchainsCallSubtype::::check_obsolete_submit_teyrchain_heads(call) .and_then(|_| ValidTransactionBuilder::default().build()), ) } } impl, I: 'static> BridgeRuntimeFilterCall for pezpallet_bridge_messages::Pezpallet 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: pezpallet_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 /// BridgePezkuwichainGrandpa, BridgePezkuwichainMessages, /// BridgePezkuwichainTeyrchains /// } /// ``` /// /// 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, codec::DecodeWithMemTracking, Default, codec::Encode, Eq, PartialEq, pezsp_runtime::RuntimeDebug, scale_info::TypeInfo)] pub struct BridgeRejectObsoleteHeadersAndMessages; impl pezsp_runtime::traits::TransactionExtension<$call> for BridgeRejectObsoleteHeadersAndMessages { const IDENTIFIER: &'static str = "BridgeRejectObsoleteHeadersAndMessages"; type Implicit = (); type Val = Option<( $account_id, ( $( <$filter_call as $crate::extensions::BridgeRuntimeFilterCall< $account_id, $call, >>::ToPostDispatch, )* ), )>; type Pre = Self::Val; fn weight(&self, _: &$call) -> pezframe_support::pezpallet_prelude::Weight { pezframe_support::pezpallet_prelude::Weight::zero() } fn validate( &self, origin: <$call as pezsp_runtime::traits::Dispatchable>::RuntimeOrigin, call: &$call, _info: &pezsp_runtime::traits::DispatchInfoOf<$call>, _len: usize, _self_implicit: Self::Implicit, _inherited_implication: &impl codec::Encode, _source: pezsp_runtime::transaction_validity::TransactionSource, ) -> Result< ( pezsp_runtime::transaction_validity::ValidTransaction, Self::Val, <$call as pezsp_runtime::traits::Dispatchable>::RuntimeOrigin, ), pezsp_runtime::transaction_validity::TransactionValidityError > { use $crate::extensions::__private::tuplex::PushBack; use pezsp_runtime::traits::AsSystemOriginSigner; let Some(who) = origin.as_system_origin_signer() else { return Ok((Default::default(), None, origin)); }; let to_post_dispatch = (); let tx_validity = pezsp_runtime::transaction_validity::ValidTransaction::default(); $( let (from_validate, call_filter_validity) = < $filter_call as $crate::extensions::BridgeRuntimeFilterCall< $account_id, $call, >>::validate(who, call); let to_post_dispatch = to_post_dispatch.push_back(from_validate); let tx_validity = tx_validity.combine_with(call_filter_validity?); )* Ok((tx_validity, Some((who.clone(), to_post_dispatch)), origin)) } fn prepare( self, val: Self::Val, _origin: &<$call as pezsp_runtime::traits::Dispatchable>::RuntimeOrigin, _call: &$call, _info: &pezsp_runtime::traits::DispatchInfoOf<$call>, _len: usize, ) -> Result { Ok(val) } #[allow(unused_variables)] fn post_dispatch_details( to_post_dispatch: Self::Pre, info: &pezsp_runtime::traits::DispatchInfoOf<$call>, post_info: &pezsp_runtime::traits::PostDispatchInfoOf<$call>, len: usize, result: &pezsp_runtime::DispatchResult, ) -> Result { use $crate::extensions::__private::tuplex::PopFront; let Some((relayer, to_post_dispatch)) = to_post_dispatch else { return Ok(pezframe_support::pezpallet_prelude::Weight::zero()) }; 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(pezframe_support::pezpallet_prelude::Weight::zero()) } } }; } #[cfg(test)] mod tests { use super::*; use crate::mock::*; use codec::{Decode, Encode, MaxEncodedLen}; use pezbp_header_pez_chain::StoredHeaderDataBuilder; use pezbp_messages::{InboundLaneData, MessageNonce, OutboundLaneData}; use pezbp_pezkuwi_core::teyrchains::{ParaHeadsProof, ParaId}; use pezbp_relayers::{RewardsAccountOwner, RewardsAccountParams}; use pezbp_runtime::HeaderId; use pezbp_test_utils::{make_default_justification, test_keyring, TEST_GRANDPA_SET_ID}; use pezbp_teyrchains::{BestParaHeadHash, ParaInfo}; use pezframe_support::{assert_err, assert_ok, traits::fungible::Mutate}; use pezpallet_bridge_grandpa::{Call as GrandpaCall, StoredAuthoritySet}; use pezpallet_bridge_teyrchains::Call as TeyrchainsCall; use pezsp_runtime::{ traits::{ parameter_types, AsSystemOriginSigner, AsTransactionAuthorizedOrigin, ConstU64, DispatchTransaction, Header as _, TransactionExtension, }, transaction_validity::{ InvalidTransaction, TransactionSource::External, TransactionValidity, ValidTransaction, }, DispatchError, }; use scale_info::TypeInfo; 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, ); } #[derive(Debug, Clone, PartialEq, Encode, Decode, TypeInfo, MaxEncodedLen)] pub struct MockCall { data: u32, } #[derive(Debug, Clone, PartialEq, Encode, Decode, TypeInfo, MaxEncodedLen)] pub struct MockOrigin(pub u64); impl AsSystemOriginSigner for MockOrigin { fn as_system_origin_signer(&self) -> Option<&u64> { Some(&self.0) } } impl AsTransactionAuthorizedOrigin for MockOrigin { fn is_transaction_authorized(&self) -> bool { true } } impl From for MockOrigin { fn from(o: u64) -> Self { Self(o) } } impl pezsp_runtime::traits::Dispatchable for MockCall { type RuntimeOrigin = MockOrigin; type Config = (); type Info = (); type PostInfo = (); fn dispatch( self, _origin: Self::RuntimeOrigin, ) -> pezsp_runtime::DispatchResultWithInfo { unimplemented!() } } pub struct FirstFilterCall; impl FirstFilterCall { fn post_dispatch_called_with(success: bool) { pezframe_support::storage::unhashed::put(&[1], &success); } fn verify_post_dispatch_called_with(success: bool) { assert_eq!(pezframe_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) { pezframe_support::storage::unhashed::put(&[2], &success); } fn verify_post_dispatch_called_with(success: bool) { assert_eq!(pezframe_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 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, teyrchain_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()); pezpallet_bridge_grandpa::CurrentAuthoritySet::::put( StoredAuthoritySet::try_new(authorities, TEST_GRANDPA_SET_ID).unwrap(), ); pezpallet_bridge_grandpa::BestFinalized::::put(best_relay_header); pezpallet_bridge_grandpa::ImportedHeaders::::insert( best_relay_header.hash(), pezbp_test_utils::test_header::(0).build(), ); let para_id = ParaId(BridgedUnderlyingTeyrchain::TEYRCHAIN_ID); let para_info = ParaInfo { best_head_hash: BestParaHeadHash { at_relay_block_number: teyrchain_head_at_relay_header_number, head_hash: [teyrchain_head_at_relay_header_number as u8; 32].into(), }, next_imported_hash_position: 0, }; pezpallet_bridge_teyrchains::ParasInfo::::insert(para_id, para_info); let lane_id = test_lane_id(); let in_lane_data = InboundLaneData { last_confirmed_nonce: best_message, ..Default::default() }; pezpallet_bridge_messages::InboundLanes::::insert(lane_id, in_lane_data); let out_lane_data = OutboundLaneData { latest_received_nonce: best_message, ..Default::default() }; pezpallet_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_teyrchain_head_call( teyrchain_head_at_relay_header_number: BridgedChainBlockNumber, ) -> RuntimeCall { RuntimeCall::BridgeTeyrchains(TeyrchainsCall::submit_teyrchain_heads { at_relay_block: (teyrchain_head_at_relay_header_number, BridgedChainHash::default()), teyrchains: vec![( ParaId(BridgedUnderlyingTeyrchain::TEYRCHAIN_ID), [teyrchain_head_at_relay_header_number as u8; 32].into(), )], teyrchain_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_only( 42u64.into(), &MockCall { data: 1 }, &(), 0, External, 0, ), InvalidTransaction::Custom(1) ); assert_err!( BridgeRejectObsoleteHeadersAndMessages.validate_and_prepare( 42u64.into(), &MockCall { data: 1 }, &(), 0, 0, ), InvalidTransaction::Custom(1) ); assert_err!( BridgeRejectObsoleteHeadersAndMessages.validate_only( 42u64.into(), &MockCall { data: 2 }, &(), 0, External, 0, ), InvalidTransaction::Custom(2) ); assert_err!( BridgeRejectObsoleteHeadersAndMessages.validate_and_prepare( 42u64.into(), &MockCall { data: 2 }, &(), 0, 0, ), InvalidTransaction::Custom(2) ); assert_eq!( BridgeRejectObsoleteHeadersAndMessages .validate_only(42u64.into(), &MockCall { data: 3 }, &(), 0, External, 0) .unwrap() .0, ValidTransaction { priority: 3, ..Default::default() }, ); assert_eq!( BridgeRejectObsoleteHeadersAndMessages .validate_and_prepare(42u64.into(), &MockCall { data: 3 }, &(), 0, 0) .unwrap() .0 .unwrap(), (42, (1, 2)), ); // when post_dispatch is called with `Ok(())`, it is propagated to all "nested" // extensions assert_ok!(BridgeRejectObsoleteHeadersAndMessages::post_dispatch_details( 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_details( Some((0, (1, 2))), &(), &(), 0, &Err(DispatchError::BadOrigin), )); FirstFilterCall::verify_post_dispatch_called_with(false); SecondFilterCall::verify_post_dispatch_called_with(false); }); } pezframe_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 BridgeTeyrchainsWrapper = CheckAndBoostBridgeTeyrchainsTransactions< TestRuntime, (), BridgedUnderlyingTeyrchain, ConstU64<1_000>, SlashDestination, >; #[test] fn teyrchains_wrapper_does_not_boost_extensions_for_unregistered_relayer() { run_test(|| { initialize_environment(100, 100, 100); let priority_boost = BridgeTeyrchainsWrapper::validate( &relayer_account_at_this_chain(), &submit_teyrchain_head_call(200), ) .1 .unwrap() .priority; assert_eq!(priority_boost, 0); }) } #[test] fn teyrchains_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 = BridgeTeyrchainsWrapper::validate( &relayer_account_at_this_chain(), &submit_teyrchain_head_call(200), ) .1 .unwrap() .priority; assert_eq!(priority_boost, 99_000); }) } #[test] fn teyrchains_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())); BridgeTeyrchainsWrapper::post_dispatch( &relayer_account_at_this_chain(), true, Some(SubmitTeyrchainHeadsInfo { at_relay_block: HeaderId(150, Default::default()), para_id: ParaId(BridgedUnderlyingTeyrchain::TEYRCHAIN_ID), para_head_hash: [150u8; 32].into(), is_free_execution_expected: false, }), ); assert!(!BridgeRelayers::is_registration_active(&relayer_account_at_this_chain())); }) } #[test] fn teyrchains_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())); BridgeTeyrchainsWrapper::post_dispatch( &relayer_account_at_this_chain(), false, Some(SubmitTeyrchainHeadsInfo { at_relay_block: HeaderId(100, Default::default()), para_id: ParaId(BridgedUnderlyingTeyrchain::TEYRCHAIN_ID), para_head_hash: [100u8; 32].into(), is_free_execution_expected: false, }), ); assert!(BridgeRelayers::is_registration_active(&relayer_account_at_this_chain())); }) } }