From 53e1b7e2641866fd95cbb75303c1c92aa3d470ed Mon Sep 17 00:00:00 2001
From: Svyatoslav Nikolsky <svyatonik@gmail.com>
Date: Tue, 25 Apr 2023 16:24:13 +0300
Subject: [PATCH] Slash relayers for invalid transactions (#2025)

* slash relayer balance for invalid transactions

* require some gap before unstake is possible

* more clippy

* log priority boost

* add issue ref to TODO

* fix typo

* is_message_delivery_call -> is_receive_messages_proof_call

* moved is_receive_messages_proof_call above

* only slash relayers for priority transactions

* Update primitives/relayers/src/registration.rs

Co-authored-by: Adrian Catangiu <adrian@parity.io>

* Update primitives/relayers/src/registration.rs

Co-authored-by: Adrian Catangiu <adrian@parity.io>

* Update bin/runtime-common/src/refund_relayer_extension.rs

Co-authored-by: Adrian Catangiu <adrian@parity.io>

* Update bin/runtime-common/src/refund_relayer_extension.rs

Co-authored-by: Adrian Catangiu <adrian@parity.io>

* Update bin/runtime-common/src/refund_relayer_extension.rs

Co-authored-by: Adrian Catangiu <adrian@parity.io>

* Update modules/relayers/src/lib.rs

Co-authored-by: Adrian Catangiu <adrian@parity.io>

* Update primitives/relayers/src/registration.rs

Co-authored-by: Adrian Catangiu <adrian@parity.io>

* benificiary -> beneficiary

---------

Co-authored-by: Adrian Catangiu <adrian@parity.io>
---
 bridges/bin/millau/runtime/src/lib.rs         |   9 +
 .../bin/rialto-parachain/runtime/src/lib.rs   |   1 +
 bridges/bin/rialto/runtime/src/lib.rs         |   1 +
 bridges/bin/runtime-common/Cargo.toml         |   2 +
 .../runtime-common/src/messages_call_ext.rs   |  10 +
 bridges/bin/runtime-common/src/mock.rs        |  24 +-
 .../src/refund_relayer_extension.rs           | 694 +++++++++++++-----
 bridges/modules/relayers/src/lib.rs           | 581 ++++++++++++++-
 bridges/modules/relayers/src/mock.rs          |  45 +-
 bridges/modules/relayers/src/stake_adapter.rs | 186 +++++
 bridges/primitives/relayers/src/lib.rs        |   4 +
 .../primitives/relayers/src/registration.rs   | 121 +++
 12 files changed, 1495 insertions(+), 183 deletions(-)
 create mode 100644 bridges/modules/relayers/src/stake_adapter.rs
 create mode 100644 bridges/primitives/relayers/src/registration.rs

diff --git a/bridges/bin/millau/runtime/src/lib.rs b/bridges/bin/millau/runtime/src/lib.rs
index 4e6f1e43e8c..dccd75a5b00 100644
--- a/bridges/bin/millau/runtime/src/lib.rs
+++ b/bridges/bin/millau/runtime/src/lib.rs
@@ -372,6 +372,7 @@ parameter_types! {
 	/// Authorities are changing every 5 minutes.
 	pub const Period: BlockNumber = bp_millau::SESSION_LENGTH;
 	pub const Offset: BlockNumber = 0;
+	pub const RelayerStakeReserveId: [u8; 8] = *b"brdgrlrs";
 }
 
 impl pallet_session::Config for Runtime {
@@ -392,6 +393,14 @@ impl pallet_bridge_relayers::Config for Runtime {
 	type Reward = Balance;
 	type PaymentProcedure =
 		bp_relayers::PayRewardFromAccount<pallet_balances::Pallet<Runtime>, AccountId>;
+	type StakeAndSlash = pallet_bridge_relayers::StakeAndSlashNamed<
+		AccountId,
+		BlockNumber,
+		Balances,
+		RelayerStakeReserveId,
+		ConstU64<1_000>,
+		ConstU64<8>,
+	>;
 	type WeightInfo = ();
 }
 
diff --git a/bridges/bin/rialto-parachain/runtime/src/lib.rs b/bridges/bin/rialto-parachain/runtime/src/lib.rs
index cd4e256f420..cd5c45ec4ba 100644
--- a/bridges/bin/rialto-parachain/runtime/src/lib.rs
+++ b/bridges/bin/rialto-parachain/runtime/src/lib.rs
@@ -533,6 +533,7 @@ impl pallet_bridge_relayers::Config for Runtime {
 	type Reward = Balance;
 	type PaymentProcedure =
 		bp_relayers::PayRewardFromAccount<pallet_balances::Pallet<Runtime>, AccountId>;
+	type StakeAndSlash = ();
 	type WeightInfo = ();
 }
 
diff --git a/bridges/bin/rialto/runtime/src/lib.rs b/bridges/bin/rialto/runtime/src/lib.rs
index b325332acba..0d2c667efa5 100644
--- a/bridges/bin/rialto/runtime/src/lib.rs
+++ b/bridges/bin/rialto/runtime/src/lib.rs
@@ -389,6 +389,7 @@ impl pallet_bridge_relayers::Config for Runtime {
 	type Reward = Balance;
 	type PaymentProcedure =
 		bp_relayers::PayRewardFromAccount<pallet_balances::Pallet<Runtime>, AccountId>;
+	type StakeAndSlash = ();
 	type WeightInfo = ();
 }
 
diff --git a/bridges/bin/runtime-common/Cargo.toml b/bridges/bin/runtime-common/Cargo.toml
index 3db4ae9abca..e7cd39da90b 100644
--- a/bridges/bin/runtime-common/Cargo.toml
+++ b/bridges/bin/runtime-common/Cargo.toml
@@ -30,6 +30,7 @@ pallet-bridge-relayers = { path = "../../modules/relayers", default-features = f
 
 frame-support = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false }
 frame-system = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false }
+pallet-balances = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false }
 pallet-transaction-payment = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false }
 pallet-utility = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false }
 sp-api = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false }
@@ -62,6 +63,7 @@ std = [
 	"frame-system/std",
 	"hash-db/std",
 	"log/std",
+	"pallet-balances/std",
 	"pallet-bridge-grandpa/std",
 	"pallet-bridge-messages/std",
 	"pallet-bridge-parachains/std",
diff --git a/bridges/bin/runtime-common/src/messages_call_ext.rs b/bridges/bin/runtime-common/src/messages_call_ext.rs
index f3665a8d93b..3f48ce583f9 100644
--- a/bridges/bin/runtime-common/src/messages_call_ext.rs
+++ b/bridges/bin/runtime-common/src/messages_call_ext.rs
@@ -115,6 +115,16 @@ pub enum CallInfo {
 	ReceiveMessagesDeliveryProof(ReceiveMessagesDeliveryProofInfo),
 }
 
+impl CallInfo {
+	/// Returns range of messages, bundled with the call.
+	pub fn bundled_messages(&self) -> RangeInclusive<MessageNonce> {
+		match *self {
+			Self::ReceiveMessagesProof(ref info) => info.base.bundled_range.clone(),
+			Self::ReceiveMessagesDeliveryProof(ref info) => info.0.bundled_range.clone(),
+		}
+	}
+}
+
 /// Helper struct that provides methods for working with a call supported by `CallInfo`.
 pub struct CallHelper<T: Config<I>, I: 'static> {
 	pub _phantom_data: sp_std::marker::PhantomData<(T, I)>,
diff --git a/bridges/bin/runtime-common/src/mock.rs b/bridges/bin/runtime-common/src/mock.rs
index 036813f6fd5..c1767199676 100644
--- a/bridges/bin/runtime-common/src/mock.rs
+++ b/bridges/bin/runtime-common/src/mock.rs
@@ -35,6 +35,7 @@ use crate::messages::{
 use bp_header_chain::{ChainWithGrandpa, HeaderChain};
 use bp_messages::{target_chain::ForbidInboundMessages, LaneId, MessageNonce};
 use bp_parachains::SingleParaStoredHeaderDataBuilder;
+use bp_relayers::PayRewardFromAccount;
 use bp_runtime::{Chain, ChainId, Parachain, UnderlyingChainProvider};
 use codec::{Decode, Encode};
 use frame_support::{
@@ -83,6 +84,20 @@ pub type BridgedChainHasher = BlakeTwo256;
 pub type BridgedChainHeader =
 	sp_runtime::generic::Header<BridgedChainBlockNumber, BridgedChainHasher>;
 
+/// Rewards payment procedure.
+pub type TestPaymentProcedure = PayRewardFromAccount<Balances, ThisChainAccountId>;
+/// Stake that we are using in tests.
+pub type TestStake = ConstU64<5_000>;
+/// Stake and slash mechanism to use in tests.
+pub type TestStakeAndSlash = pallet_bridge_relayers::StakeAndSlashNamed<
+	ThisChainAccountId,
+	ThisChainBlockNumber,
+	Balances,
+	ReserveId,
+	TestStake,
+	ConstU32<8>,
+>;
+
 /// Message lane used in tests.
 pub const TEST_LANE_ID: LaneId = LaneId([0, 0, 0, 0]);
 /// Bridged chain id used in tests.
@@ -128,6 +143,7 @@ parameter_types! {
 	pub MaximumMultiplier: Multiplier = sp_runtime::traits::Bounded::max_value();
 	pub const MaxUnrewardedRelayerEntriesAtInboundLane: MessageNonce = 16;
 	pub const MaxUnconfirmedMessagesAtInboundLane: MessageNonce = 1_000;
+	pub const ReserveId: [u8; 8] = *b"brdgrlrs";
 }
 
 impl frame_system::Config for TestRuntime {
@@ -244,7 +260,8 @@ impl pallet_bridge_messages::Config for TestRuntime {
 impl pallet_bridge_relayers::Config for TestRuntime {
 	type RuntimeEvent = RuntimeEvent;
 	type Reward = ThisChainBalance;
-	type PaymentProcedure = ();
+	type PaymentProcedure = TestPaymentProcedure;
+	type StakeAndSlash = TestStakeAndSlash;
 	type WeightInfo = ();
 }
 
@@ -400,3 +417,8 @@ impl ThisChainWithMessages for BridgedChain {
 }
 
 impl BridgedChainWithMessages for BridgedChain {}
+
+/// Run test within test externalities.
+pub fn run_test(test: impl FnOnce()) {
+	sp_io::TestExternalities::new(Default::default()).execute_with(test)
+}
diff --git a/bridges/bin/runtime-common/src/refund_relayer_extension.rs b/bridges/bin/runtime-common/src/refund_relayer_extension.rs
index 925fea2a743..7d65263e9fd 100644
--- a/bridges/bin/runtime-common/src/refund_relayer_extension.rs
+++ b/bridges/bin/runtime-common/src/refund_relayer_extension.rs
@@ -22,7 +22,7 @@
 use crate::messages_call_ext::{
 	CallHelper as MessagesCallHelper, CallInfo as MessagesCallInfo, MessagesCallSubType,
 };
-use bp_messages::LaneId;
+use bp_messages::{LaneId, MessageNonce};
 use bp_relayers::{RewardsAccountOwner, RewardsAccountParams};
 use bp_runtime::{RangeInclusiveExt, StaticStrProvider};
 use codec::{Decode, Encode};
@@ -30,7 +30,7 @@ use frame_support::{
 	dispatch::{CallableCallFor, DispatchInfo, Dispatchable, PostDispatchInfo},
 	traits::IsSubType,
 	weights::Weight,
-	CloneNoBound, DefaultNoBound, EqNoBound, PartialEqNoBound, RuntimeDebugNoBound,
+	CloneNoBound, DefaultNoBound, EqNoBound, PartialEqNoBound, RuntimeDebug, RuntimeDebugNoBound,
 };
 use pallet_bridge_grandpa::{
 	CallSubType as GrandpaCallSubType, SubmitFinalityProofHelper, SubmitFinalityProofInfo,
@@ -53,6 +53,7 @@ use sp_runtime::{
 };
 use sp_std::{marker::PhantomData, vec, vec::Vec};
 
+type AccountIdOf<R> = <R as frame_system::Config>::AccountId;
 // without this typedef rustfmt fails with internal err
 type BalanceOf<R> =
 	<<R as TransactionPaymentConfig>::OnChargeTransaction as OnChargeTransaction<R>>::Balance;
@@ -158,6 +159,14 @@ pub enum CallInfo {
 }
 
 impl CallInfo {
+	/// Returns true if call is a message delivery call (with optional finality calls).
+	fn is_receive_messages_proof_call(&self) -> bool {
+		match self.messages_call_info() {
+			MessagesCallInfo::ReceiveMessagesProof(_) => true,
+			MessagesCallInfo::ReceiveMessagesDeliveryProof(_) => false,
+		}
+	}
+
 	/// Returns the pre-dispatch `finality_target` sent to the `SubmitFinalityProof` call.
 	fn submit_finality_proof_info(&self) -> Option<SubmitFinalityProofInfo<RelayBlockNumber>> {
 		match *self {
@@ -185,6 +194,17 @@ impl CallInfo {
 	}
 }
 
+/// The actions on relayer account that need to be performed because of his actions.
+#[derive(RuntimeDebug, PartialEq)]
+enum RelayerAccountAction<AccountId, Reward> {
+	/// Do nothing with relayer account.
+	None,
+	/// Reward the relayer.
+	Reward(AccountId, RewardsAccountParams, Reward),
+	/// Slash the relayer.
+	Slash(AccountId, RewardsAccountParams),
+}
+
 /// Signed extension that refunds a relayer for new messages coming from a parachain.
 ///
 /// Also refunds relayer for successful finality delivery if it comes in batch (`utility.batchAll`)
@@ -205,7 +225,25 @@ impl CallInfo {
 )]
 #[scale_info(skip_type_params(Runtime, Para, Msgs, Refund, Priority, Id))]
 pub struct RefundBridgedParachainMessages<Runtime, Para, Msgs, Refund, Priority, Id>(
-	PhantomData<(Runtime, Para, Msgs, Refund, Priority, Id)>,
+	PhantomData<(
+		// runtime with `frame-utility`, `pallet-bridge-grandpa`, `pallet-bridge-parachains`,
+		// `pallet-bridge-messages` and `pallet-bridge-relayers` pallets deployed
+		Runtime,
+		// implementation of `RefundableParachainId` trait, which specifies the instance of
+		// the used `pallet-bridge-parachains` pallet and the bridged parachain id
+		Para,
+		// implementation of `RefundableMessagesLaneId` trait, which specifies the instance of
+		// the used `pallet-bridge-messages` pallet and the lane within this pallet
+		Msgs,
+		// implementation of the `RefundCalculator` trait, that is used to compute refund that
+		// we give to relayer for his transaction
+		Refund,
+		// getter for per-message `TransactionPriority` boost that we give to message
+		// delivery transactions
+		Priority,
+		// the runtime-unique identifier of this signed extension
+		Id,
+	)>,
 );
 
 impl<Runtime, Para, Msgs, Refund, Priority, Id>
@@ -215,9 +253,13 @@ where
 	Runtime: UtilityConfig<RuntimeCall = CallOf<Runtime>>
 		+ BoundedBridgeGrandpaConfig<Runtime::BridgesGrandpaPalletInstance>
 		+ ParachainsConfig<Para::Instance>
-		+ MessagesConfig<Msgs::Instance>,
+		+ MessagesConfig<Msgs::Instance>
+		+ RelayersConfig,
 	Para: RefundableParachainId,
 	Msgs: RefundableMessagesLaneId,
+	Refund: RefundCalculator<Balance = Runtime::Reward>,
+	Priority: Get<TransactionPriority>,
+	Id: StaticStrProvider,
 	CallOf<Runtime>: Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo>
 		+ IsSubType<CallableCallFor<UtilityPallet<Runtime>, Runtime>>
 		+ GrandpaCallSubType<Runtime, Runtime::BridgesGrandpaPalletInstance>
@@ -268,118 +310,69 @@ where
 		call.check_obsolete_call()?;
 		Ok(call)
 	}
-}
-
-impl<Runtime, Para, Msgs, Refund, Priority, Id> SignedExtension
-	for RefundBridgedParachainMessages<Runtime, Para, Msgs, Refund, Priority, Id>
-where
-	Self: 'static + Send + Sync,
-	Runtime: UtilityConfig<RuntimeCall = CallOf<Runtime>>
-		+ BoundedBridgeGrandpaConfig<Runtime::BridgesGrandpaPalletInstance>
-		+ ParachainsConfig<Para::Instance>
-		+ MessagesConfig<Msgs::Instance>
-		+ RelayersConfig,
-	Para: RefundableParachainId,
-	Msgs: RefundableMessagesLaneId,
-	Refund: RefundCalculator<Balance = Runtime::Reward>,
-	Priority: Get<TransactionPriority>,
-	Id: StaticStrProvider,
-	CallOf<Runtime>: Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo>
-		+ IsSubType<CallableCallFor<UtilityPallet<Runtime>, Runtime>>
-		+ GrandpaCallSubType<Runtime, Runtime::BridgesGrandpaPalletInstance>
-		+ ParachainsCallSubType<Runtime, Para::Instance>
-		+ MessagesCallSubType<Runtime, Msgs::Instance>,
-{
-	const IDENTIFIER: &'static str = Id::STR;
-	type AccountId = Runtime::AccountId;
-	type Call = CallOf<Runtime>;
-	type AdditionalSigned = ();
-	type Pre = Option<PreDispatchData<Runtime::AccountId>>;
-
-	fn additional_signed(&self) -> Result<(), TransactionValidityError> {
-		Ok(())
-	}
 
-	fn validate(
-		&self,
-		_who: &Self::AccountId,
-		call: &Self::Call,
-		_info: &DispatchInfoOf<Self::Call>,
-		_len: usize,
-	) -> TransactionValidity {
-		// this is the only relevant line of code for the `pre_dispatch`
-		//
-		// we're not calling `validato` from `pre_dispatch` directly because of performance
-		// reasons, so if you're adding some code that may fail here, please check if it needs
-		// to be added to the `pre_dispatch` as well
-		let parsed_call = self.parse_and_check_for_obsolete_call(call)?;
+	/// Given post-dispatch information, analyze the outcome of relayer call and return
+	/// actions that need to be performed on relayer account.
+	fn analyze_call_result(
+		pre: Option<Option<PreDispatchData<Runtime::AccountId>>>,
+		info: &DispatchInfo,
+		post_info: &PostDispatchInfo,
+		len: usize,
+		result: &DispatchResult,
+	) -> RelayerAccountAction<AccountIdOf<Runtime>, Runtime::Reward> {
+		let mut extra_weight = Weight::zero();
+		let mut extra_size = 0;
 
-		// the following code just plays with transaction priority and never returns an error
-		let mut valid_transaction = ValidTransactionBuilder::default();
-		if let Some(parsed_call) = parsed_call {
-			// we give delivery transactions some boost, that depends on number of messages inside
-			let messages_call_info = parsed_call.messages_call_info();
-			if let MessagesCallInfo::ReceiveMessagesProof(info) = messages_call_info {
-				// compute total number of messages in transaction
-				let bundled_messages = info.base.bundled_range.checked_len().unwrap_or(0);
-
-				// a quick check to avoid invalid high-priority transactions
-				if bundled_messages <= Runtime::MaxUnconfirmedMessagesAtInboundLane::get() {
-					let priority_boost = crate::priority_calculator::compute_priority_boost::<
-						Priority,
-					>(bundled_messages);
-					valid_transaction = valid_transaction.priority(priority_boost);
-				}
-			}
-		}
+		// We don't refund anything for transactions that we don't support.
+		let (relayer, call_info) = match pre {
+			Some(Some(pre)) => (pre.relayer, pre.call_info),
+			_ => return RelayerAccountAction::None,
+		};
 
-		valid_transaction.build()
-	}
+		// now we know that the relayer either needs to be rewarded, or slashed
+		// => let's prepare the correspondent account that pays reward/receives slashed amount
+		let reward_account_params = RewardsAccountParams::new(
+			Msgs::Id::get(),
+			Runtime::BridgedChainId::get(),
+			if call_info.is_receive_messages_proof_call() {
+				RewardsAccountOwner::ThisChain
+			} else {
+				RewardsAccountOwner::BridgedChain
+			},
+		);
 
-	fn pre_dispatch(
-		self,
-		who: &Self::AccountId,
-		call: &Self::Call,
-		_info: &DispatchInfoOf<Self::Call>,
-		_len: usize,
-	) -> Result<Self::Pre, TransactionValidityError> {
-		// this is a relevant piece of `validate` that we need here (in `pre_dispatch`)
-		let parsed_call = self.parse_and_check_for_obsolete_call(call)?;
+		// prepare return value for the case if the call has failed or it has not caused
+		// expected side effects (e.g. not all messages have been accepted)
+		//
+		// we are not checking if relayer is registered here - it happens during the slash attempt
+		//
+		// there are couple of edge cases here:
+		//
+		// - when the relayer becomes registered during message dispatch: this is unlikely + relayer
+		//   should be ready for slashing after registration;
+		//
+		// - when relayer is registered after `validate` is called and priority is not boosted:
+		//   relayer should be ready for slashing after registration.
+		let may_slash_relayer =
+			Self::bundled_messages_for_priority_boost(Some(&call_info)).is_some();
+		let slash_relayer_if_delivery_result = may_slash_relayer
+			.then(|| RelayerAccountAction::Slash(relayer.clone(), reward_account_params))
+			.unwrap_or(RelayerAccountAction::None);
 
-		Ok(parsed_call.map(|call_info| {
+		// We don't refund anything if the transaction has failed.
+		if let Err(e) = result {
 			log::trace!(
 				target: "runtime::bridge",
-				"{} from parachain {} via {:?} parsed bridge transaction in pre-dispatch: {:?}",
+				"{} from parachain {} via {:?}: relayer {:?} has submitted invalid messages transaction: {:?}",
 				Self::IDENTIFIER,
 				Para::Id::get(),
 				Msgs::Id::get(),
-				call_info,
+				relayer,
+				e,
 			);
-			PreDispatchData { relayer: who.clone(), call_info }
-		}))
-	}
-
-	fn post_dispatch(
-		pre: Option<Self::Pre>,
-		info: &DispatchInfoOf<Self::Call>,
-		post_info: &PostDispatchInfoOf<Self::Call>,
-		len: usize,
-		result: &DispatchResult,
-	) -> Result<(), TransactionValidityError> {
-		let mut extra_weight = Weight::zero();
-		let mut extra_size = 0;
-
-		// We don't refund anything if the transaction has failed.
-		if result.is_err() {
-			return Ok(())
+			return slash_relayer_if_delivery_result
 		}
 
-		// We don't refund anything for transactions that we don't support.
-		let (relayer, call_info) = match pre {
-			Some(Some(pre)) => (pre.relayer, pre.call_info),
-			_ => return Ok(()),
-		};
-
 		// check if relay chain state has been updated
 		if let Some(finality_proof_info) = call_info.submit_finality_proof_info() {
 			if !SubmitFinalityProofHelper::<Runtime, Runtime::BridgesGrandpaPalletInstance>::was_successful(
@@ -388,15 +381,13 @@ where
 				// we only refund relayer if all calls have updated chain state
 				log::trace!(
 					target: "runtime::bridge",
-					"{} from parachain {} via {:?}: failed to refund relayer {:?}, because \
-					relay chain finality proof has not been accepted",
+					"{} from parachain {} via {:?}: relayer {:?} has submitted invalid relay chain finality proof",
 					Self::IDENTIFIER,
 					Para::Id::get(),
 					Msgs::Id::get(),
 					relayer,
 				);
-
-				return Ok(())
+				return slash_relayer_if_delivery_result;
 			}
 
 			// there's a conflict between how bridge GRANDPA pallet works and a `utility.batchAll`
@@ -420,33 +411,29 @@ where
 				// we only refund relayer if all calls have updated chain state
 				log::trace!(
 					target: "runtime::bridge",
-					"{} from parachain {} via {:?}: failed to refund relayer {:?}, because \
-					parachain finality proof has not been accepted",
+					"{} from parachain {} via {:?}: relayer {:?} has submitted invalid parachain finality proof",
 					Self::IDENTIFIER,
 					Para::Id::get(),
 					Msgs::Id::get(),
 					relayer,
 				);
-
-				return Ok(())
+				return slash_relayer_if_delivery_result
 			}
 		}
 
-		// Check if the `ReceiveMessagesProof` call delivered all the messages that
+		// Check if the `ReceiveMessagesProof` call delivered at least some of the messages that
 		// it contained. If this happens, we consider the transaction "helpful" and refund it.
 		let msgs_call_info = call_info.messages_call_info();
 		if !MessagesCallHelper::<Runtime, Msgs::Instance>::was_successful(msgs_call_info) {
 			log::trace!(
 				target: "runtime::bridge",
-				"{} from parachain {} via {:?}: failed to refund relayer {:?}, because \
-				some of messages have not been accepted",
+				"{} from parachain {} via {:?}: relayer {:?} has submitted invalid messages call",
 				Self::IDENTIFIER,
 				Para::Id::get(),
 				Msgs::Id::get(),
 				relayer,
 			);
-
-			return Ok(())
+			return slash_relayer_if_delivery_result
 		}
 
 		// regarding the tip - refund that happens here (at this side of the bridge) isn't the whole
@@ -465,31 +452,172 @@ where
 		// compute the relayer refund
 		let refund = Refund::compute_refund(info, &post_info, post_info_len, tip);
 
-		// finally - register refund in relayers pallet
-		let rewards_account_owner = match msgs_call_info {
-			MessagesCallInfo::ReceiveMessagesProof(_) => RewardsAccountOwner::ThisChain,
-			MessagesCallInfo::ReceiveMessagesDeliveryProof(_) => RewardsAccountOwner::BridgedChain,
+		// we can finally reward relayer
+		RelayerAccountAction::Reward(relayer, reward_account_params, refund)
+	}
+
+	/// Returns number of bundled messages `Some(_)`, if the given call info is a:
+	///
+	/// - message delivery transaction;
+	///
+	/// - with reasonable bundled messages that may be accepted by the messages pallet.
+	///
+	/// This function is used to check whether the transaction priority should be
+	/// virtually boosted. The relayer registration (we only boost priority for registered
+	/// relayer transactions) must be checked outside.
+	fn bundled_messages_for_priority_boost(call_info: Option<&CallInfo>) -> Option<MessageNonce> {
+		// we only boost priority of message delivery transactions
+		let parsed_call = match call_info {
+			Some(parsed_call) if parsed_call.is_receive_messages_proof_call() => parsed_call,
+			_ => return None,
 		};
-		RelayersPallet::<Runtime>::register_relayer_reward(
-			RewardsAccountParams::new(
-				Msgs::Id::get(),
-				Runtime::BridgedChainId::get(),
-				rewards_account_owner,
-			),
-			&relayer,
-			refund,
-		);
+
+		// compute total number of messages in transaction
+		let bundled_messages =
+			parsed_call.messages_call_info().bundled_messages().checked_len().unwrap_or(0);
+
+		// a quick check to avoid invalid high-priority transactions
+		if bundled_messages > Runtime::MaxUnconfirmedMessagesAtInboundLane::get() {
+			return None
+		}
+
+		Some(bundled_messages)
+	}
+}
+
+impl<Runtime, Para, Msgs, Refund, Priority, Id> SignedExtension
+	for RefundBridgedParachainMessages<Runtime, Para, Msgs, Refund, Priority, Id>
+where
+	Self: 'static + Send + Sync,
+	Runtime: UtilityConfig<RuntimeCall = CallOf<Runtime>>
+		+ BoundedBridgeGrandpaConfig<Runtime::BridgesGrandpaPalletInstance>
+		+ ParachainsConfig<Para::Instance>
+		+ MessagesConfig<Msgs::Instance>
+		+ RelayersConfig,
+	Para: RefundableParachainId,
+	Msgs: RefundableMessagesLaneId,
+	Refund: RefundCalculator<Balance = Runtime::Reward>,
+	Priority: Get<TransactionPriority>,
+	Id: StaticStrProvider,
+	CallOf<Runtime>: Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo>
+		+ IsSubType<CallableCallFor<UtilityPallet<Runtime>, Runtime>>
+		+ GrandpaCallSubType<Runtime, Runtime::BridgesGrandpaPalletInstance>
+		+ ParachainsCallSubType<Runtime, Para::Instance>
+		+ MessagesCallSubType<Runtime, Msgs::Instance>,
+{
+	const IDENTIFIER: &'static str = Id::STR;
+	type AccountId = Runtime::AccountId;
+	type Call = CallOf<Runtime>;
+	type AdditionalSigned = ();
+	type Pre = Option<PreDispatchData<Runtime::AccountId>>;
+
+	fn additional_signed(&self) -> Result<(), TransactionValidityError> {
+		Ok(())
+	}
+
+	fn validate(
+		&self,
+		who: &Self::AccountId,
+		call: &Self::Call,
+		_info: &DispatchInfoOf<Self::Call>,
+		_len: usize,
+	) -> TransactionValidity {
+		// this is the only relevant line of code for the `pre_dispatch`
+		//
+		// we're not calling `validate` from `pre_dispatch` directly because of performance
+		// reasons, so if you're adding some code that may fail here, please check if it needs
+		// to be added to the `pre_dispatch` as well
+		let parsed_call = self.parse_and_check_for_obsolete_call(call)?;
+
+		// the following code just plays with transaction priority and never returns an error
+
+		// we only boost priority of presumably correct message delivery transactions
+		let bundled_messages = match Self::bundled_messages_for_priority_boost(parsed_call.as_ref())
+		{
+			Some(bundled_messages) => bundled_messages,
+			None => return Ok(Default::default()),
+		};
+
+		// we only boost priority if relayer has staked required balance
+		if !RelayersPallet::<Runtime>::is_registration_active(who) {
+			return Ok(Default::default())
+		}
+
+		// compute priority boost
+		let priority_boost =
+			crate::priority_calculator::compute_priority_boost::<Priority>(bundled_messages);
+		let valid_transaction = ValidTransactionBuilder::default().priority(priority_boost);
 
 		log::trace!(
 			target: "runtime::bridge",
-			"{} from parachain {} via {:?} has registered reward: {:?} for {:?}",
+			"{} from parachain {} via {:?} has boosted priority of message delivery transaction \
+			of relayer {:?}: {} messages -> {} priority",
 			Self::IDENTIFIER,
 			Para::Id::get(),
 			Msgs::Id::get(),
-			refund,
-			relayer,
+			who,
+			bundled_messages,
+			priority_boost,
 		);
 
+		valid_transaction.build()
+	}
+
+	fn pre_dispatch(
+		self,
+		who: &Self::AccountId,
+		call: &Self::Call,
+		_info: &DispatchInfoOf<Self::Call>,
+		_len: usize,
+	) -> Result<Self::Pre, TransactionValidityError> {
+		// this is a relevant piece of `validate` that we need here (in `pre_dispatch`)
+		let parsed_call = self.parse_and_check_for_obsolete_call(call)?;
+
+		Ok(parsed_call.map(|call_info| {
+			log::trace!(
+				target: "runtime::bridge",
+				"{} from parachain {} via {:?} parsed bridge transaction in pre-dispatch: {:?}",
+				Self::IDENTIFIER,
+				Para::Id::get(),
+				Msgs::Id::get(),
+				call_info,
+			);
+			PreDispatchData { relayer: who.clone(), call_info }
+		}))
+	}
+
+	fn post_dispatch(
+		pre: Option<Self::Pre>,
+		info: &DispatchInfoOf<Self::Call>,
+		post_info: &PostDispatchInfoOf<Self::Call>,
+		len: usize,
+		result: &DispatchResult,
+	) -> Result<(), TransactionValidityError> {
+		let call_result = Self::analyze_call_result(pre, info, post_info, len, result);
+
+		match call_result {
+			RelayerAccountAction::None => (),
+			RelayerAccountAction::Reward(relayer, reward_account, reward) => {
+				RelayersPallet::<Runtime>::register_relayer_reward(
+					reward_account,
+					&relayer,
+					reward,
+				);
+
+				log::trace!(
+					target: "runtime::bridge",
+					"{} from parachain {} via {:?} has registered reward: {:?} for {:?}",
+					Self::IDENTIFIER,
+					Para::Id::get(),
+					Msgs::Id::get(),
+					reward,
+					relayer,
+				);
+			},
+			RelayerAccountAction::Slash(relayer, slash_account) =>
+				RelayersPallet::<Runtime>::slash_and_deregister(&relayer, slash_account),
+		}
+
 		Ok(())
 	}
 }
@@ -509,10 +637,14 @@ mod tests {
 	};
 	use bp_messages::{InboundLaneData, MessageNonce, OutboundLaneData, UnrewardedRelayersState};
 	use bp_parachains::{BestParaHeadHash, ParaInfo};
-	use bp_polkadot_core::parachains::{ParaHash, ParaHeadsProof, ParaId};
+	use bp_polkadot_core::parachains::{ParaHeadsProof, ParaId};
 	use bp_runtime::HeaderId;
 	use bp_test_utils::{make_default_justification, test_keyring};
-	use frame_support::{assert_storage_noop, parameter_types, weights::Weight};
+	use frame_support::{
+		assert_storage_noop, parameter_types,
+		traits::{fungible::Mutate, ReservableCurrency},
+		weights::Weight,
+	};
 	use pallet_bridge_grandpa::{Call as GrandpaCall, StoredAuthoritySet};
 	use pallet_bridge_messages::Call as MessagesCall;
 	use pallet_bridge_parachains::{Call as ParachainsCall, RelayBlockHash};
@@ -547,6 +679,22 @@ mod tests {
 		StrTestExtension,
 	>;
 
+	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
 	}
@@ -558,7 +706,6 @@ mod tests {
 	fn initialize_environment(
 		best_relay_header_number: RelayBlockNumber,
 		parachain_head_at_relay_header_number: RelayBlockNumber,
-		parachain_head_hash: ParaHash,
 		best_message: MessageNonce,
 	) {
 		let authorities = test_keyring().into_iter().map(|(a, w)| (a.into(), w)).collect();
@@ -572,7 +719,7 @@ mod tests {
 		let para_info = ParaInfo {
 			best_head_hash: BestParaHeadHash {
 				at_relay_block_number: parachain_head_at_relay_header_number,
-				head_hash: parachain_head_hash,
+				head_hash: [parachain_head_at_relay_header_number as u8; 32].into(),
 			},
 			next_imported_hash_position: 0,
 		};
@@ -586,6 +733,14 @@ mod tests {
 		let out_lane_data =
 			OutboundLaneData { latest_received_nonce: best_message, ..Default::default() };
 		pallet_bridge_messages::OutboundLanes::<TestRuntime>::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: RelayBlockNumber) -> RuntimeCall {
@@ -609,7 +764,10 @@ mod tests {
 	) -> RuntimeCall {
 		RuntimeCall::BridgeParachains(ParachainsCall::submit_parachain_heads {
 			at_relay_block: (parachain_head_at_relay_header_number, RelayBlockHash::default()),
-			parachains: vec![(ParaId(TestParachain::get()), [1u8; 32].into())],
+			parachains: vec![(
+				ParaId(TestParachain::get()),
+				[parachain_head_at_relay_header_number as u8; 32].into(),
+			)],
 			parachain_heads_proof: ParaHeadsProof(vec![]),
 		})
 	}
@@ -711,7 +869,7 @@ mod tests {
 				SubmitParachainHeadsInfo {
 					at_relay_block_number: 200,
 					para_id: ParaId(TestParachain::get()),
-					para_head_hash: [1u8; 32].into(),
+					para_head_hash: [200u8; 32].into(),
 				},
 				MessagesCallInfo::ReceiveMessagesProof(ReceiveMessagesProofInfo {
 					base: BaseMessagesProofInfo {
@@ -740,7 +898,7 @@ mod tests {
 				SubmitParachainHeadsInfo {
 					at_relay_block_number: 200,
 					para_id: ParaId(TestParachain::get()),
-					para_head_hash: [1u8; 32].into(),
+					para_head_hash: [200u8; 32].into(),
 				},
 				MessagesCallInfo::ReceiveMessagesDeliveryProof(ReceiveMessagesDeliveryProofInfo(
 					BaseMessagesProofInfo {
@@ -760,7 +918,7 @@ mod tests {
 				SubmitParachainHeadsInfo {
 					at_relay_block_number: 200,
 					para_id: ParaId(TestParachain::get()),
-					para_head_hash: [1u8; 32].into(),
+					para_head_hash: [200u8; 32].into(),
 				},
 				MessagesCallInfo::ReceiveMessagesProof(ReceiveMessagesProofInfo {
 					base: BaseMessagesProofInfo {
@@ -784,7 +942,7 @@ mod tests {
 				SubmitParachainHeadsInfo {
 					at_relay_block_number: 200,
 					para_id: ParaId(TestParachain::get()),
-					para_head_hash: [1u8; 32].into(),
+					para_head_hash: [200u8; 32].into(),
 				},
 				MessagesCallInfo::ReceiveMessagesDeliveryProof(ReceiveMessagesDeliveryProofInfo(
 					BaseMessagesProofInfo {
@@ -829,8 +987,21 @@ mod tests {
 		}
 	}
 
-	fn run_test(test: impl FnOnce()) {
-		sp_io::TestExternalities::new(Default::default()).execute_with(test)
+	fn set_bundled_range_end(
+		mut pre_dispatch_data: PreDispatchData<ThisChainAccountId>,
+		end: MessageNonce,
+	) -> PreDispatchData<ThisChainAccountId> {
+		let msg_info = match pre_dispatch_data.call_info {
+			CallInfo::AllFinalityAndMsgs(_, _, ref mut info) => info,
+			CallInfo::ParachainFinalityAndMsgs(_, ref mut info) => info,
+			CallInfo::Msgs(ref mut info) => info,
+		};
+
+		if let MessagesCallInfo::ReceiveMessagesProof(ref mut msg_info) = msg_info {
+			msg_info.base.bundled_range = *msg_info.base.bundled_range.start()..=end
+		}
+
+		pre_dispatch_data
 	}
 
 	fn run_validate(call: RuntimeCall) -> TransactionValidity {
@@ -838,6 +1009,13 @@ mod tests {
 		extension.validate(&relayer_account_at_this_chain(), &call, &DispatchInfo::default(), 0)
 	}
 
+	fn run_validate_ignore_priority(call: RuntimeCall) -> TransactionValidity {
+		run_validate(call).map(|mut tx| {
+			tx.priority = 0;
+			tx
+		})
+	}
+
 	fn run_pre_dispatch(
 		call: RuntimeCall,
 	) -> Result<Option<PreDispatchData<ThisChainAccountId>>, TransactionValidityError> {
@@ -883,10 +1061,49 @@ mod tests {
 		)
 	}
 
+	#[test]
+	fn validate_doesnt_boost_transaction_priority_if_relayer_is_not_registered() {
+		run_test(|| {
+			initialize_environment(100, 100, 100);
+			Balances::set_balance(&relayer_account_at_this_chain(), ExistentialDeposit::get());
+
+			// message delivery is failing
+			assert_eq!(run_validate(message_delivery_call(200)), Ok(Default::default()),);
+			assert_eq!(
+				run_validate(parachain_finality_and_delivery_batch_call(200, 200)),
+				Ok(Default::default()),
+			);
+			assert_eq!(
+				run_validate(all_finality_and_delivery_batch_call(200, 200, 200)),
+				Ok(Default::default()),
+			);
+			// message confirmation validation is passing
+			assert_eq!(
+				run_validate_ignore_priority(message_confirmation_call(200)),
+				Ok(Default::default()),
+			);
+			assert_eq!(
+				run_validate_ignore_priority(parachain_finality_and_confirmation_batch_call(
+					200, 200
+				)),
+				Ok(Default::default()),
+			);
+			assert_eq!(
+				run_validate_ignore_priority(all_finality_and_confirmation_batch_call(
+					200, 200, 200
+				)),
+				Ok(Default::default()),
+			);
+		});
+	}
+
 	#[test]
 	fn validate_boosts_priority_of_message_delivery_transactons() {
 		run_test(|| {
-			initialize_environment(100, 100, Default::default(), 100);
+			initialize_environment(100, 100, 100);
+
+			BridgeRelayers::register(RuntimeOrigin::signed(relayer_account_at_this_chain()), 1000)
+				.unwrap();
 
 			let priority_of_100_messages_delivery =
 				run_validate(message_delivery_call(200)).unwrap().priority;
@@ -913,7 +1130,10 @@ mod tests {
 	#[test]
 	fn validate_does_not_boost_priority_of_message_delivery_transactons_with_too_many_messages() {
 		run_test(|| {
-			initialize_environment(100, 100, Default::default(), 100);
+			initialize_environment(100, 100, 100);
+
+			BridgeRelayers::register(RuntimeOrigin::signed(relayer_account_at_this_chain()), 1000)
+				.unwrap();
 
 			let priority_of_max_messages_delivery = run_validate(message_delivery_call(
 				100 + MaxUnconfirmedMessagesAtInboundLane::get(),
@@ -938,14 +1158,7 @@ mod tests {
 	#[test]
 	fn validate_allows_non_obsolete_transactions() {
 		run_test(|| {
-			initialize_environment(100, 100, Default::default(), 100);
-
-			fn run_validate_ignore_priority(call: RuntimeCall) -> TransactionValidity {
-				run_validate(call).map(|mut tx| {
-					tx.priority = 0;
-					tx
-				})
-			}
+			initialize_environment(100, 100, 100);
 
 			assert_eq!(
 				run_validate_ignore_priority(message_delivery_call(200)),
@@ -983,7 +1196,7 @@ mod tests {
 	#[test]
 	fn ext_rejects_batch_with_obsolete_relay_chain_header() {
 		run_test(|| {
-			initialize_environment(100, 100, Default::default(), 100);
+			initialize_environment(100, 100, 100);
 
 			assert_eq!(
 				run_pre_dispatch(all_finality_and_delivery_batch_call(100, 200, 200)),
@@ -1000,7 +1213,7 @@ mod tests {
 	#[test]
 	fn ext_rejects_batch_with_obsolete_parachain_head() {
 		run_test(|| {
-			initialize_environment(100, 100, Default::default(), 100);
+			initialize_environment(100, 100, 100);
 
 			assert_eq!(
 				run_pre_dispatch(all_finality_and_delivery_batch_call(101, 100, 200)),
@@ -1025,7 +1238,7 @@ mod tests {
 	#[test]
 	fn ext_rejects_batch_with_obsolete_messages() {
 		run_test(|| {
-			initialize_environment(100, 100, Default::default(), 100);
+			initialize_environment(100, 100, 100);
 
 			assert_eq!(
 				run_pre_dispatch(all_finality_and_delivery_batch_call(200, 200, 100)),
@@ -1068,7 +1281,7 @@ mod tests {
 	#[test]
 	fn pre_dispatch_parses_batch_with_relay_chain_and_parachain_headers() {
 		run_test(|| {
-			initialize_environment(100, 100, Default::default(), 100);
+			initialize_environment(100, 100, 100);
 
 			assert_eq!(
 				run_pre_dispatch(all_finality_and_delivery_batch_call(200, 200, 200)),
@@ -1084,7 +1297,7 @@ mod tests {
 	#[test]
 	fn pre_dispatch_parses_batch_with_parachain_header() {
 		run_test(|| {
-			initialize_environment(100, 100, Default::default(), 100);
+			initialize_environment(100, 100, 100);
 
 			assert_eq!(
 				run_pre_dispatch(parachain_finality_and_delivery_batch_call(200, 200)),
@@ -1100,7 +1313,7 @@ mod tests {
 	#[test]
 	fn pre_dispatch_fails_to_parse_batch_with_multiple_parachain_headers() {
 		run_test(|| {
-			initialize_environment(100, 100, Default::default(), 100);
+			initialize_environment(100, 100, 100);
 
 			let call = RuntimeCall::Utility(UtilityCall::batch_all {
 				calls: vec![
@@ -1123,7 +1336,7 @@ mod tests {
 	#[test]
 	fn pre_dispatch_parses_message_transaction() {
 		run_test(|| {
-			initialize_environment(100, 100, Default::default(), 100);
+			initialize_environment(100, 100, 100);
 
 			assert_eq!(
 				run_pre_dispatch(message_delivery_call(200)),
@@ -1156,7 +1369,7 @@ mod tests {
 	#[test]
 	fn post_dispatch_ignores_transaction_that_has_not_updated_relay_chain_state() {
 		run_test(|| {
-			initialize_environment(100, 200, Default::default(), 200);
+			initialize_environment(100, 200, 200);
 
 			assert_storage_noop!(run_post_dispatch(Some(all_finality_pre_dispatch_data()), Ok(())));
 		});
@@ -1165,7 +1378,7 @@ mod tests {
 	#[test]
 	fn post_dispatch_ignores_transaction_that_has_not_updated_parachain_state() {
 		run_test(|| {
-			initialize_environment(200, 100, Default::default(), 200);
+			initialize_environment(200, 100, 200);
 
 			assert_storage_noop!(run_post_dispatch(Some(all_finality_pre_dispatch_data()), Ok(())));
 			assert_storage_noop!(run_post_dispatch(
@@ -1178,7 +1391,7 @@ mod tests {
 	#[test]
 	fn post_dispatch_ignores_transaction_that_has_not_delivered_any_messages() {
 		run_test(|| {
-			initialize_environment(200, 200, Default::default(), 100);
+			initialize_environment(200, 200, 100);
 
 			assert_storage_noop!(run_post_dispatch(Some(all_finality_pre_dispatch_data()), Ok(())));
 			assert_storage_noop!(run_post_dispatch(
@@ -1202,7 +1415,7 @@ mod tests {
 	#[test]
 	fn post_dispatch_ignores_transaction_that_has_not_delivered_all_messages() {
 		run_test(|| {
-			initialize_environment(200, 200, Default::default(), 150);
+			initialize_environment(200, 200, 150);
 
 			assert_storage_noop!(run_post_dispatch(Some(all_finality_pre_dispatch_data()), Ok(())));
 			assert_storage_noop!(run_post_dispatch(
@@ -1226,7 +1439,7 @@ mod tests {
 	#[test]
 	fn post_dispatch_refunds_relayer_in_all_finality_batch_with_extra_weight() {
 		run_test(|| {
-			initialize_environment(200, 200, [1u8; 32].into(), 200);
+			initialize_environment(200, 200, 200);
 
 			let mut dispatch_info = dispatch_info();
 			dispatch_info.weight = Weight::from_parts(
@@ -1275,7 +1488,7 @@ mod tests {
 	#[test]
 	fn post_dispatch_refunds_relayer_in_all_finality_batch() {
 		run_test(|| {
-			initialize_environment(200, 200, [1u8; 32].into(), 200);
+			initialize_environment(200, 200, 200);
 
 			run_post_dispatch(Some(all_finality_pre_dispatch_data()), Ok(()));
 			assert_eq!(
@@ -1300,7 +1513,7 @@ mod tests {
 	#[test]
 	fn post_dispatch_refunds_relayer_in_parachain_finality_batch() {
 		run_test(|| {
-			initialize_environment(200, 200, [1u8; 32].into(), 200);
+			initialize_environment(200, 200, 200);
 
 			run_post_dispatch(Some(parachain_finality_pre_dispatch_data()), Ok(()));
 			assert_eq!(
@@ -1325,7 +1538,7 @@ mod tests {
 	#[test]
 	fn post_dispatch_refunds_relayer_in_message_transaction() {
 		run_test(|| {
-			initialize_environment(200, 200, Default::default(), 200);
+			initialize_environment(200, 200, 200);
 
 			run_post_dispatch(Some(delivery_pre_dispatch_data()), Ok(()));
 			assert_eq!(
@@ -1346,4 +1559,149 @@ mod tests {
 			);
 		});
 	}
+
+	#[test]
+	fn post_dispatch_slashing_relayer_stake() {
+		run_test(|| {
+			initialize_environment(200, 200, 100);
+
+			let delivery_rewards_account_balance =
+				Balances::free_balance(delivery_rewards_account());
+
+			let test_stake: ThisChainBalance = TestStake::get();
+			Balances::set_balance(
+				&relayer_account_at_this_chain(),
+				ExistentialDeposit::get() + test_stake * 10,
+			);
+
+			// slashing works for message delivery calls
+			BridgeRelayers::register(RuntimeOrigin::signed(relayer_account_at_this_chain()), 1000)
+				.unwrap();
+			assert_eq!(Balances::reserved_balance(relayer_account_at_this_chain()), test_stake);
+			run_post_dispatch(Some(delivery_pre_dispatch_data()), Ok(()));
+			assert_eq!(Balances::reserved_balance(relayer_account_at_this_chain()), 0);
+			assert_eq!(
+				delivery_rewards_account_balance + test_stake,
+				Balances::free_balance(delivery_rewards_account())
+			);
+
+			BridgeRelayers::register(RuntimeOrigin::signed(relayer_account_at_this_chain()), 1000)
+				.unwrap();
+			assert_eq!(Balances::reserved_balance(relayer_account_at_this_chain()), test_stake);
+			run_post_dispatch(Some(parachain_finality_pre_dispatch_data()), Ok(()));
+			assert_eq!(Balances::reserved_balance(relayer_account_at_this_chain()), 0);
+			assert_eq!(
+				delivery_rewards_account_balance + test_stake * 2,
+				Balances::free_balance(delivery_rewards_account())
+			);
+
+			BridgeRelayers::register(RuntimeOrigin::signed(relayer_account_at_this_chain()), 1000)
+				.unwrap();
+			assert_eq!(Balances::reserved_balance(relayer_account_at_this_chain()), test_stake);
+			run_post_dispatch(Some(all_finality_pre_dispatch_data()), Ok(()));
+			assert_eq!(Balances::reserved_balance(relayer_account_at_this_chain()), 0);
+			assert_eq!(
+				delivery_rewards_account_balance + test_stake * 3,
+				Balances::free_balance(delivery_rewards_account())
+			);
+
+			// reserve doesn't work for message confirmation calls
+			let confirmation_rewards_account_balance =
+				Balances::free_balance(confirmation_rewards_account());
+
+			Balances::reserve(&relayer_account_at_this_chain(), test_stake).unwrap();
+			assert_eq!(Balances::reserved_balance(relayer_account_at_this_chain()), test_stake);
+
+			assert_eq!(
+				confirmation_rewards_account_balance,
+				Balances::free_balance(confirmation_rewards_account())
+			);
+			run_post_dispatch(Some(confirmation_pre_dispatch_data()), Ok(()));
+			assert_eq!(Balances::reserved_balance(relayer_account_at_this_chain()), test_stake);
+
+			run_post_dispatch(Some(parachain_finality_confirmation_pre_dispatch_data()), Ok(()));
+			assert_eq!(Balances::reserved_balance(relayer_account_at_this_chain()), test_stake);
+
+			run_post_dispatch(Some(all_finality_confirmation_pre_dispatch_data()), Ok(()));
+			assert_eq!(Balances::reserved_balance(relayer_account_at_this_chain()), test_stake);
+
+			// check that unreserve has happened, not slashing
+			assert_eq!(
+				delivery_rewards_account_balance + test_stake * 3,
+				Balances::free_balance(delivery_rewards_account())
+			);
+			assert_eq!(
+				confirmation_rewards_account_balance,
+				Balances::free_balance(confirmation_rewards_account())
+			);
+		});
+	}
+
+	fn run_analyze_call_result(
+		pre_dispatch_data: PreDispatchData<ThisChainAccountId>,
+		dispatch_result: DispatchResult,
+	) -> RelayerAccountAction<ThisChainAccountId, ThisChainBalance> {
+		TestExtension::analyze_call_result(
+			Some(Some(pre_dispatch_data)),
+			&dispatch_info(),
+			&post_dispatch_info(),
+			1024,
+			&dispatch_result,
+		)
+	}
+
+	#[test]
+	fn analyze_call_result_shall_not_slash_for_transactions_with_too_many_messages() {
+		run_test(|| {
+			initialize_environment(100, 100, 100);
+
+			// the `analyze_call_result` should return slash if number of bundled messages is
+			// within reasonable limits
+			assert_eq!(
+				run_analyze_call_result(all_finality_pre_dispatch_data(), Ok(())),
+				RelayerAccountAction::Slash(
+					relayer_account_at_this_chain(),
+					MsgProofsRewardsAccount::get()
+				),
+			);
+			assert_eq!(
+				run_analyze_call_result(parachain_finality_pre_dispatch_data(), Ok(())),
+				RelayerAccountAction::Slash(
+					relayer_account_at_this_chain(),
+					MsgProofsRewardsAccount::get()
+				),
+			);
+			assert_eq!(
+				run_analyze_call_result(delivery_pre_dispatch_data(), Ok(())),
+				RelayerAccountAction::Slash(
+					relayer_account_at_this_chain(),
+					MsgProofsRewardsAccount::get()
+				),
+			);
+
+			// the `analyze_call_result` should not return slash if number of bundled messages is
+			// larger than the
+			assert_eq!(
+				run_analyze_call_result(
+					set_bundled_range_end(all_finality_pre_dispatch_data(), 1_000_000),
+					Ok(())
+				),
+				RelayerAccountAction::None,
+			);
+			assert_eq!(
+				run_analyze_call_result(
+					set_bundled_range_end(parachain_finality_pre_dispatch_data(), 1_000_000),
+					Ok(())
+				),
+				RelayerAccountAction::None,
+			);
+			assert_eq!(
+				run_analyze_call_result(
+					set_bundled_range_end(delivery_pre_dispatch_data(), 1_000_000),
+					Ok(())
+				),
+				RelayerAccountAction::None,
+			);
+		});
+	}
 }
diff --git a/bridges/modules/relayers/src/lib.rs b/bridges/modules/relayers/src/lib.rs
index bd33b811b30..14e44d30f89 100644
--- a/bridges/modules/relayers/src/lib.rs
+++ b/bridges/modules/relayers/src/lib.rs
@@ -20,20 +20,25 @@
 #![cfg_attr(not(feature = "std"), no_std)]
 #![warn(missing_docs)]
 
-use bp_relayers::{PaymentProcedure, RelayerRewardsKeyProvider, RewardsAccountParams};
+use bp_relayers::{
+	PaymentProcedure, Registration, RelayerRewardsKeyProvider, RewardsAccountParams, StakeAndSlash,
+};
 use bp_runtime::StorageDoubleMapKeyProvider;
-use frame_support::sp_runtime::Saturating;
+use frame_support::fail;
 use sp_arithmetic::traits::{AtLeast32BitUnsigned, Zero};
+use sp_runtime::{traits::CheckedSub, Saturating};
 use sp_std::marker::PhantomData;
 
 pub use pallet::*;
 pub use payment_adapter::DeliveryConfirmationPaymentsAdapter;
+pub use stake_adapter::StakeAndSlashNamed;
 pub use weights::WeightInfo;
 
 pub mod benchmarking;
 
 mod mock;
 mod payment_adapter;
+mod stake_adapter;
 
 pub mod weights;
 
@@ -56,8 +61,10 @@ pub mod pallet {
 		type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
 		/// Type of relayer reward.
 		type Reward: AtLeast32BitUnsigned + Copy + Parameter + MaxEncodedLen;
-		/// Pay rewards adapter.
+		/// Pay rewards scheme.
 		type PaymentProcedure: PaymentProcedure<Self::AccountId, Self::Reward>;
+		/// Stake and slash scheme.
+		type StakeAndSlash: StakeAndSlash<Self::AccountId, Self::BlockNumber, Self::Reward>;
 		/// Pallet call weights.
 		type WeightInfo: WeightInfo;
 	}
@@ -102,9 +109,194 @@ pub mod pallet {
 				},
 			)
 		}
+
+		/// Register relayer or update its registration.
+		///
+		/// Registration allows relayer to get priority boost for its message delivery transactions.
+		#[pallet::call_index(1)]
+		#[pallet::weight(Weight::zero())] // TODO: https://github.com/paritytech/parity-bridges-common/issues/2033
+		pub fn register(origin: OriginFor<T>, valid_till: T::BlockNumber) -> DispatchResult {
+			let relayer = ensure_signed(origin)?;
+
+			// valid till must be larger than the current block number and the lease must be larger
+			// than the `RequiredRegistrationLease`
+			let lease = valid_till.saturating_sub(frame_system::Pallet::<T>::block_number());
+			ensure!(
+				lease > Pallet::<T>::required_registration_lease(),
+				Error::<T>::InvalidRegistrationLease
+			);
+
+			RegisteredRelayers::<T>::try_mutate(&relayer, |maybe_registration| -> DispatchResult {
+				let mut registration = maybe_registration
+					.unwrap_or_else(|| Registration { valid_till, stake: Zero::zero() });
+
+				// new `valid_till` must be larger (or equal) than the old one
+				ensure!(
+					valid_till >= registration.valid_till,
+					Error::<T>::CannotReduceRegistrationLease,
+				);
+				registration.valid_till = valid_till;
+
+				// regarding stake, there are three options:
+				// - if relayer stake is larger than required stake, we may do unreserve
+				// - if relayer stake equals to required stake, we do nothing
+				// - if relayer stake is smaller than required stake, we do additional reserve
+				let required_stake = Pallet::<T>::required_stake();
+				if let Some(to_unreserve) = registration.stake.checked_sub(&required_stake) {
+					Self::do_unreserve(&relayer, to_unreserve)?;
+				} else if let Some(to_reserve) = required_stake.checked_sub(&registration.stake) {
+					T::StakeAndSlash::reserve(&relayer, to_reserve).map_err(|e| {
+						log::trace!(
+							target: LOG_TARGET,
+							"Failed to reserve {:?} on relayer {:?} account: {:?}",
+							to_reserve,
+							relayer,
+							e,
+						);
+
+						Error::<T>::FailedToReserve
+					})?;
+				}
+				registration.stake = required_stake;
+
+				Self::deposit_event(Event::<T>::RegistrationUpdated {
+					relayer: relayer.clone(),
+					registration,
+				});
+
+				*maybe_registration = Some(registration);
+
+				Ok(())
+			})
+		}
+
+		/// `Deregister` relayer.
+		///
+		/// After this call, message delivery transactions of the relayer won't get any priority
+		/// boost.
+		#[pallet::call_index(2)]
+		#[pallet::weight(Weight::zero())] // TODO: https://github.com/paritytech/parity-bridges-common/issues/2033
+		pub fn deregister(origin: OriginFor<T>) -> DispatchResult {
+			let relayer = ensure_signed(origin)?;
+
+			RegisteredRelayers::<T>::try_mutate(&relayer, |maybe_registration| -> DispatchResult {
+				let registration = match maybe_registration.take() {
+					Some(registration) => registration,
+					None => fail!(Error::<T>::NotRegistered),
+				};
+
+				// we can't deregister until `valid_till + 1`
+				ensure!(
+					registration.valid_till < frame_system::Pallet::<T>::block_number(),
+					Error::<T>::RegistrationIsStillActive,
+				);
+
+				// if stake is non-zero, we should do unreserve
+				if !registration.stake.is_zero() {
+					Self::do_unreserve(&relayer, registration.stake)?;
+				}
+
+				Self::deposit_event(Event::<T>::Deregistered { relayer: relayer.clone() });
+
+				*maybe_registration = None;
+
+				Ok(())
+			})
+		}
 	}
 
 	impl<T: Config> Pallet<T> {
+		/// Returns true if given relayer registration is active at current block.
+		///
+		/// This call respects both `RequiredStake` and `RequiredRegistrationLease`, meaning that
+		/// it'll return false if registered stake is lower than required or if remaining lease
+		/// is less than `RequiredRegistrationLease`.
+		pub fn is_registration_active(relayer: &T::AccountId) -> bool {
+			let registration = match Self::registered_relayer(relayer) {
+				Some(registration) => registration,
+				None => return false,
+			};
+
+			// registration is inactive if relayer stake is less than required
+			if registration.stake < Self::required_stake() {
+				return false
+			}
+
+			// registration is inactive if it ends soon
+			let remaining_lease = registration
+				.valid_till
+				.saturating_sub(frame_system::Pallet::<T>::block_number());
+			if remaining_lease <= Self::required_registration_lease() {
+				return false
+			}
+
+			true
+		}
+
+		/// Slash and `deregister` relayer. This function slashes all staked balance.
+		///
+		/// It may fail inside, but error is swallowed and we only log it.
+		pub fn slash_and_deregister(
+			relayer: &T::AccountId,
+			slash_destination: RewardsAccountParams,
+		) {
+			let registration = match RegisteredRelayers::<T>::take(relayer) {
+				Some(registration) => registration,
+				None => {
+					log::trace!(
+						target: crate::LOG_TARGET,
+						"Cannot slash unregistered relayer {:?}",
+						relayer,
+					);
+
+					return
+				},
+			};
+
+			match T::StakeAndSlash::repatriate_reserved(
+				relayer,
+				slash_destination,
+				registration.stake,
+			) {
+				Ok(failed_to_slash) if failed_to_slash.is_zero() => {
+					log::trace!(
+						target: crate::LOG_TARGET,
+						"Relayer account {:?} has been slashed for {:?}. Funds were deposited to {:?}",
+						relayer,
+						registration.stake,
+						slash_destination,
+					);
+				},
+				Ok(failed_to_slash) => {
+					log::trace!(
+						target: crate::LOG_TARGET,
+						"Relayer account {:?} has been partially slashed for {:?}. Funds were deposited to {:?}. \
+						Failed to slash: {:?}",
+						relayer,
+						registration.stake,
+						slash_destination,
+						failed_to_slash,
+					);
+				},
+				Err(e) => {
+					// TODO: document this. Where?
+
+					// it may fail if there's no beneficiary account. For us it means that this
+					// account must exists before we'll deploy the bridge
+					log::debug!(
+						target: crate::LOG_TARGET,
+						"Failed to slash relayer account {:?}: {:?}. Maybe beneficiary account doesn't exist? \
+						Beneficiary: {:?}, amount: {:?}, failed to slash: {:?}",
+						relayer,
+						e,
+						slash_destination,
+						registration.stake,
+						registration.stake,
+					);
+				},
+			}
+		}
+
 		/// Register reward for given relayer.
 		pub fn register_relayer_reward(
 			rewards_account_params: RewardsAccountParams,
@@ -132,6 +324,42 @@ pub mod pallet {
 				},
 			);
 		}
+
+		/// Return required registration lease.
+		fn required_registration_lease() -> T::BlockNumber {
+			<T::StakeAndSlash as StakeAndSlash<
+				T::AccountId,
+				T::BlockNumber,
+				T::Reward,
+			>>::RequiredRegistrationLease::get()
+		}
+
+		/// Return required stake.
+		fn required_stake() -> T::Reward {
+			<T::StakeAndSlash as StakeAndSlash<
+				T::AccountId,
+				T::BlockNumber,
+				T::Reward,
+			>>::RequiredStake::get()
+		}
+
+		/// `Unreserve` given amount on relayer account.
+		fn do_unreserve(relayer: &T::AccountId, amount: T::Reward) -> DispatchResult {
+			let failed_to_unreserve = T::StakeAndSlash::unreserve(relayer, amount);
+			if !failed_to_unreserve.is_zero() {
+				log::trace!(
+					target: LOG_TARGET,
+					"Failed to unreserve {:?}/{:?} on relayer {:?} account",
+					failed_to_unreserve,
+					amount,
+					relayer,
+				);
+
+				fail!(Error::<T>::FailedToUnreserve)
+			}
+
+			Ok(())
+		}
 	}
 
 	#[pallet::event]
@@ -146,6 +374,25 @@ pub mod pallet {
 			/// Reward amount.
 			reward: T::Reward,
 		},
+		/// Relayer registration has been added or updated.
+		RegistrationUpdated {
+			/// Relayer account that has been registered.
+			relayer: T::AccountId,
+			/// Relayer registration.
+			registration: Registration<T::BlockNumber, T::Reward>,
+		},
+		/// Relayer has been `deregistered`.
+		Deregistered {
+			/// Relayer account that has been `deregistered`.
+			relayer: T::AccountId,
+		},
+		/// Relayer has been slashed and `deregistered`.
+		SlashedAndDeregistered {
+			/// Relayer account that has been `deregistered`.
+			relayer: T::AccountId,
+			/// Registration that was removed.
+			registration: Registration<T::BlockNumber, T::Reward>,
+		},
 	}
 
 	#[pallet::error]
@@ -154,6 +401,19 @@ pub mod pallet {
 		NoRewardForRelayer,
 		/// Reward payment procedure has failed.
 		FailedToPayReward,
+		/// The relayer has tried to register for past block or registration lease
+		/// is too short.
+		InvalidRegistrationLease,
+		/// New registration lease is less than the previous one.
+		CannotReduceRegistrationLease,
+		/// Failed to reserve enough funds on relayer account.
+		FailedToReserve,
+		/// Failed to `unreserve` enough funds on relayer account.
+		FailedToUnreserve,
+		/// Cannot `deregister` if not registered.
+		NotRegistered,
+		/// Failed to `deregister` relayer, because lease is still active.
+		RegistrationIsStillActive,
 	}
 
 	/// Map of the relayer => accumulated reward.
@@ -168,6 +428,22 @@ pub mod pallet {
 		<RelayerRewardsKeyProviderOf<T> as StorageDoubleMapKeyProvider>::Value,
 		OptionQuery,
 	>;
+
+	/// Relayers that have reserved some of their balance to get free priority boost
+	/// for their message delivery transactions.
+	///
+	/// Other relayers may submit transactions as well, but they will have default
+	/// priority and will be rejected (without significant tip) in case if registered
+	/// relayer is present.
+	#[pallet::storage]
+	#[pallet::getter(fn registered_relayer)]
+	pub type RegisteredRelayers<T: Config> = StorageMap<
+		_,
+		Blake2_128Concat,
+		T::AccountId,
+		Registration<T::BlockNumber, T::Reward>,
+		OptionQuery,
+	>;
 }
 
 #[cfg(test)]
@@ -253,10 +529,10 @@ mod tests {
 				None
 			);
 
-			//Check if the `RewardPaid` event was emitted.
+			// Check if the `RewardPaid` event was emitted.
 			assert_eq!(
-				System::<TestRuntime>::events(),
-				vec![EventRecord {
+				System::<TestRuntime>::events().last(),
+				Some(&EventRecord {
 					phase: Phase::Initialization,
 					event: TestEvent::Relayers(RewardPaid {
 						relayer: REGULAR_RELAYER,
@@ -264,7 +540,7 @@ mod tests {
 						reward: 100
 					}),
 					topics: vec![],
-				}],
+				}),
 			);
 		});
 	}
@@ -306,4 +582,295 @@ mod tests {
 			assert_eq!(Balances::balance(&1), 200);
 		});
 	}
+
+	#[test]
+	fn register_fails_if_valid_till_is_a_past_block() {
+		run_test(|| {
+			System::<TestRuntime>::set_block_number(100);
+
+			assert_noop!(
+				Pallet::<TestRuntime>::register(RuntimeOrigin::signed(REGISTER_RELAYER), 50),
+				Error::<TestRuntime>::InvalidRegistrationLease,
+			);
+		});
+	}
+
+	#[test]
+	fn register_fails_if_valid_till_lease_is_less_than_required() {
+		run_test(|| {
+			System::<TestRuntime>::set_block_number(100);
+
+			assert_noop!(
+				Pallet::<TestRuntime>::register(
+					RuntimeOrigin::signed(REGISTER_RELAYER),
+					99 + Lease::get()
+				),
+				Error::<TestRuntime>::InvalidRegistrationLease,
+			);
+		});
+	}
+
+	#[test]
+	fn register_works() {
+		run_test(|| {
+			get_ready_for_events();
+
+			assert_ok!(Pallet::<TestRuntime>::register(
+				RuntimeOrigin::signed(REGISTER_RELAYER),
+				150
+			));
+			assert_eq!(Balances::reserved_balance(REGISTER_RELAYER), Stake::get());
+			assert_eq!(
+				Pallet::<TestRuntime>::registered_relayer(REGISTER_RELAYER),
+				Some(Registration { valid_till: 150, stake: Stake::get() }),
+			);
+
+			assert_eq!(
+				System::<TestRuntime>::events().last(),
+				Some(&EventRecord {
+					phase: Phase::Initialization,
+					event: TestEvent::Relayers(Event::RegistrationUpdated {
+						relayer: REGISTER_RELAYER,
+						registration: Registration { valid_till: 150, stake: Stake::get() },
+					}),
+					topics: vec![],
+				}),
+			);
+		});
+	}
+
+	#[test]
+	fn register_fails_if_new_valid_till_is_lesser_than_previous() {
+		run_test(|| {
+			assert_ok!(Pallet::<TestRuntime>::register(
+				RuntimeOrigin::signed(REGISTER_RELAYER),
+				150
+			));
+
+			assert_noop!(
+				Pallet::<TestRuntime>::register(RuntimeOrigin::signed(REGISTER_RELAYER), 125),
+				Error::<TestRuntime>::CannotReduceRegistrationLease,
+			);
+		});
+	}
+
+	#[test]
+	fn register_fails_if_it_cant_unreserve_some_balance_if_required_stake_decreases() {
+		run_test(|| {
+			RegisteredRelayers::<TestRuntime>::insert(
+				REGISTER_RELAYER,
+				Registration { valid_till: 150, stake: Stake::get() + 1 },
+			);
+
+			assert_noop!(
+				Pallet::<TestRuntime>::register(RuntimeOrigin::signed(REGISTER_RELAYER), 150),
+				Error::<TestRuntime>::FailedToUnreserve,
+			);
+		});
+	}
+
+	#[test]
+	fn register_unreserves_some_balance_if_required_stake_decreases() {
+		run_test(|| {
+			get_ready_for_events();
+
+			RegisteredRelayers::<TestRuntime>::insert(
+				REGISTER_RELAYER,
+				Registration { valid_till: 150, stake: Stake::get() + 1 },
+			);
+			TestStakeAndSlash::reserve(&REGISTER_RELAYER, Stake::get() + 1).unwrap();
+			assert_eq!(Balances::reserved_balance(REGISTER_RELAYER), Stake::get() + 1);
+			let free_balance = Balances::free_balance(REGISTER_RELAYER);
+
+			assert_ok!(Pallet::<TestRuntime>::register(
+				RuntimeOrigin::signed(REGISTER_RELAYER),
+				150
+			));
+			assert_eq!(Balances::reserved_balance(REGISTER_RELAYER), Stake::get());
+			assert_eq!(Balances::free_balance(REGISTER_RELAYER), free_balance + 1);
+			assert_eq!(
+				Pallet::<TestRuntime>::registered_relayer(REGISTER_RELAYER),
+				Some(Registration { valid_till: 150, stake: Stake::get() }),
+			);
+
+			assert_eq!(
+				System::<TestRuntime>::events().last(),
+				Some(&EventRecord {
+					phase: Phase::Initialization,
+					event: TestEvent::Relayers(Event::RegistrationUpdated {
+						relayer: REGISTER_RELAYER,
+						registration: Registration { valid_till: 150, stake: Stake::get() }
+					}),
+					topics: vec![],
+				}),
+			);
+		});
+	}
+
+	#[test]
+	fn register_fails_if_it_cant_reserve_some_balance() {
+		run_test(|| {
+			Balances::set_balance(&REGISTER_RELAYER, 0);
+			assert_noop!(
+				Pallet::<TestRuntime>::register(RuntimeOrigin::signed(REGISTER_RELAYER), 150),
+				Error::<TestRuntime>::FailedToReserve,
+			);
+		});
+	}
+
+	#[test]
+	fn register_fails_if_it_cant_reserve_some_balance_if_required_stake_increases() {
+		run_test(|| {
+			RegisteredRelayers::<TestRuntime>::insert(
+				REGISTER_RELAYER,
+				Registration { valid_till: 150, stake: Stake::get() - 1 },
+			);
+			Balances::set_balance(&REGISTER_RELAYER, 0);
+
+			assert_noop!(
+				Pallet::<TestRuntime>::register(RuntimeOrigin::signed(REGISTER_RELAYER), 150),
+				Error::<TestRuntime>::FailedToReserve,
+			);
+		});
+	}
+
+	#[test]
+	fn register_reserves_some_balance_if_required_stake_increases() {
+		run_test(|| {
+			get_ready_for_events();
+
+			RegisteredRelayers::<TestRuntime>::insert(
+				REGISTER_RELAYER,
+				Registration { valid_till: 150, stake: Stake::get() - 1 },
+			);
+			TestStakeAndSlash::reserve(&REGISTER_RELAYER, Stake::get() - 1).unwrap();
+
+			let free_balance = Balances::free_balance(REGISTER_RELAYER);
+			assert_ok!(Pallet::<TestRuntime>::register(
+				RuntimeOrigin::signed(REGISTER_RELAYER),
+				150
+			));
+			assert_eq!(Balances::reserved_balance(REGISTER_RELAYER), Stake::get());
+			assert_eq!(Balances::free_balance(REGISTER_RELAYER), free_balance - 1);
+			assert_eq!(
+				Pallet::<TestRuntime>::registered_relayer(REGISTER_RELAYER),
+				Some(Registration { valid_till: 150, stake: Stake::get() }),
+			);
+
+			assert_eq!(
+				System::<TestRuntime>::events().last(),
+				Some(&EventRecord {
+					phase: Phase::Initialization,
+					event: TestEvent::Relayers(Event::RegistrationUpdated {
+						relayer: REGISTER_RELAYER,
+						registration: Registration { valid_till: 150, stake: Stake::get() }
+					}),
+					topics: vec![],
+				}),
+			);
+		});
+	}
+
+	#[test]
+	fn deregister_fails_if_not_registered() {
+		run_test(|| {
+			assert_noop!(
+				Pallet::<TestRuntime>::deregister(RuntimeOrigin::signed(REGISTER_RELAYER)),
+				Error::<TestRuntime>::NotRegistered,
+			);
+		});
+	}
+
+	#[test]
+	fn deregister_fails_if_registration_is_still_active() {
+		run_test(|| {
+			assert_ok!(Pallet::<TestRuntime>::register(
+				RuntimeOrigin::signed(REGISTER_RELAYER),
+				150
+			));
+
+			System::<TestRuntime>::set_block_number(100);
+
+			assert_noop!(
+				Pallet::<TestRuntime>::deregister(RuntimeOrigin::signed(REGISTER_RELAYER)),
+				Error::<TestRuntime>::RegistrationIsStillActive,
+			);
+		});
+	}
+
+	#[test]
+	fn deregister_works() {
+		run_test(|| {
+			get_ready_for_events();
+
+			assert_ok!(Pallet::<TestRuntime>::register(
+				RuntimeOrigin::signed(REGISTER_RELAYER),
+				150
+			));
+
+			System::<TestRuntime>::set_block_number(151);
+
+			let reserved_balance = Balances::reserved_balance(REGISTER_RELAYER);
+			let free_balance = Balances::free_balance(REGISTER_RELAYER);
+			assert_ok!(Pallet::<TestRuntime>::deregister(RuntimeOrigin::signed(REGISTER_RELAYER)));
+			assert_eq!(
+				Balances::reserved_balance(REGISTER_RELAYER),
+				reserved_balance - Stake::get()
+			);
+			assert_eq!(Balances::free_balance(REGISTER_RELAYER), free_balance + Stake::get());
+
+			assert_eq!(
+				System::<TestRuntime>::events().last(),
+				Some(&EventRecord {
+					phase: Phase::Initialization,
+					event: TestEvent::Relayers(Event::Deregistered { relayer: REGISTER_RELAYER }),
+					topics: vec![],
+				}),
+			);
+		});
+	}
+
+	#[test]
+	fn is_registration_active_is_false_for_unregistered_relayer() {
+		run_test(|| {
+			assert!(!Pallet::<TestRuntime>::is_registration_active(&REGISTER_RELAYER));
+		});
+	}
+
+	#[test]
+	fn is_registration_active_is_false_when_stake_is_too_low() {
+		run_test(|| {
+			RegisteredRelayers::<TestRuntime>::insert(
+				REGISTER_RELAYER,
+				Registration { valid_till: 150, stake: Stake::get() - 1 },
+			);
+			assert!(!Pallet::<TestRuntime>::is_registration_active(&REGISTER_RELAYER));
+		});
+	}
+
+	#[test]
+	fn is_registration_active_is_false_when_remaining_lease_is_too_low() {
+		run_test(|| {
+			System::<TestRuntime>::set_block_number(150 - Lease::get());
+
+			RegisteredRelayers::<TestRuntime>::insert(
+				REGISTER_RELAYER,
+				Registration { valid_till: 150, stake: Stake::get() },
+			);
+			assert!(!Pallet::<TestRuntime>::is_registration_active(&REGISTER_RELAYER));
+		});
+	}
+
+	#[test]
+	fn is_registration_active_is_true_when_relayer_is_properly_registeered() {
+		run_test(|| {
+			System::<TestRuntime>::set_block_number(150 - Lease::get());
+
+			RegisteredRelayers::<TestRuntime>::insert(
+				REGISTER_RELAYER,
+				Registration { valid_till: 151, stake: Stake::get() },
+			);
+			assert!(Pallet::<TestRuntime>::is_registration_active(&REGISTER_RELAYER));
+		});
+	}
 }
diff --git a/bridges/modules/relayers/src/mock.rs b/bridges/modules/relayers/src/mock.rs
index fe8c586eecc..406a365f350 100644
--- a/bridges/modules/relayers/src/mock.rs
+++ b/bridges/modules/relayers/src/mock.rs
@@ -19,8 +19,10 @@
 use crate as pallet_bridge_relayers;
 
 use bp_messages::LaneId;
-use bp_relayers::{PaymentProcedure, RewardsAccountOwner, RewardsAccountParams};
-use frame_support::{parameter_types, weights::RuntimeDbWeight};
+use bp_relayers::{
+	PayRewardFromAccount, PaymentProcedure, RewardsAccountOwner, RewardsAccountParams,
+};
+use frame_support::{parameter_types, traits::fungible::Mutate, weights::RuntimeDbWeight};
 use sp_core::H256;
 use sp_runtime::{
 	testing::Header as SubstrateHeader,
@@ -29,6 +31,16 @@ use sp_runtime::{
 
 pub type AccountId = u64;
 pub type Balance = u64;
+pub type BlockNumber = u64;
+
+pub type TestStakeAndSlash = pallet_bridge_relayers::StakeAndSlashNamed<
+	AccountId,
+	BlockNumber,
+	Balances,
+	ReserveId,
+	Stake,
+	Lease,
+>;
 
 type Block = frame_system::mocking::MockBlock<TestRuntime>;
 type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic<TestRuntime>;
@@ -47,13 +59,17 @@ frame_support::construct_runtime! {
 
 parameter_types! {
 	pub const DbWeight: RuntimeDbWeight = RuntimeDbWeight { read: 1, write: 2 };
+	pub const ExistentialDeposit: Balance = 1;
+	pub const ReserveId: [u8; 8] = *b"brdgrlrs";
+	pub const Stake: Balance = 1_000;
+	pub const Lease: BlockNumber = 8;
 }
 
 impl frame_system::Config for TestRuntime {
 	type RuntimeOrigin = RuntimeOrigin;
 	type Index = u64;
 	type RuntimeCall = RuntimeCall;
-	type BlockNumber = u64;
+	type BlockNumber = BlockNumber;
 	type Hash = H256;
 	type Hashing = BlakeTwo256;
 	type AccountId = AccountId;
@@ -81,11 +97,11 @@ impl pallet_balances::Config for TestRuntime {
 	type Balance = Balance;
 	type DustRemoval = ();
 	type RuntimeEvent = RuntimeEvent;
-	type ExistentialDeposit = frame_support::traits::ConstU64<1>;
+	type ExistentialDeposit = ExistentialDeposit;
 	type AccountStore = frame_system::Pallet<TestRuntime>;
 	type WeightInfo = ();
-	type MaxReserves = ();
-	type ReserveIdentifier = ();
+	type MaxReserves = ConstU32<1>;
+	type ReserveIdentifier = [u8; 8];
 	type HoldIdentifier = ();
 	type FreezeIdentifier = ();
 	type MaxHolds = ConstU32<0>;
@@ -96,6 +112,7 @@ impl pallet_bridge_relayers::Config for TestRuntime {
 	type RuntimeEvent = RuntimeEvent;
 	type Reward = Balance;
 	type PaymentProcedure = TestPaymentProcedure;
+	type StakeAndSlash = TestStakeAndSlash;
 	type WeightInfo = ();
 }
 
@@ -121,9 +138,18 @@ pub const REGULAR_RELAYER: AccountId = 1;
 /// Relayer that can't receive rewards.
 pub const FAILING_RELAYER: AccountId = 2;
 
+/// Relayer that is able to register.
+pub const REGISTER_RELAYER: AccountId = 42;
+
 /// Payment procedure that rejects payments to the `FAILING_RELAYER`.
 pub struct TestPaymentProcedure;
 
+impl TestPaymentProcedure {
+	pub fn rewards_account(params: RewardsAccountParams) -> AccountId {
+		PayRewardFromAccount::<(), AccountId>::rewards_account(params)
+	}
+}
+
 impl PaymentProcedure<AccountId, Balance> for TestPaymentProcedure {
 	type Error = ();
 
@@ -147,5 +173,10 @@ pub fn new_test_ext() -> sp_io::TestExternalities {
 
 /// Run pallet test.
 pub fn run_test<T>(test: impl FnOnce() -> T) -> T {
-	new_test_ext().execute_with(test)
+	new_test_ext().execute_with(|| {
+		Balances::mint_into(&REGISTER_RELAYER, ExistentialDeposit::get() + 10 * Stake::get())
+			.unwrap();
+
+		test()
+	})
 }
diff --git a/bridges/modules/relayers/src/stake_adapter.rs b/bridges/modules/relayers/src/stake_adapter.rs
new file mode 100644
index 00000000000..055b6a111ec
--- /dev/null
+++ b/bridges/modules/relayers/src/stake_adapter.rs
@@ -0,0 +1,186 @@
+// Copyright 2019-2021 Parity Technologies (UK) Ltd.
+// This file is part of Parity Bridges Common.
+
+// Parity Bridges Common is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Parity Bridges Common is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Parity Bridges Common.  If not, see <http://www.gnu.org/licenses/>.
+
+//! Code that allows `NamedReservableCurrency` to be used as a `StakeAndSlash`
+//! mechanism of the relayers pallet.
+
+use bp_relayers::{PayRewardFromAccount, RewardsAccountParams, StakeAndSlash};
+use codec::Codec;
+use frame_support::traits::{tokens::BalanceStatus, NamedReservableCurrency};
+use sp_runtime::{traits::Get, DispatchError, DispatchResult};
+use sp_std::{fmt::Debug, marker::PhantomData};
+
+/// `StakeAndSlash` that works with `NamedReservableCurrency` and uses named
+/// reservations.
+///
+/// **WARNING**: this implementation assumes that the relayers pallet is configured to
+/// use the [`bp_relayers::PayRewardFromAccount`] as its relayers payment scheme.
+pub struct StakeAndSlashNamed<AccountId, BlockNumber, Currency, ReserveId, Stake, Lease>(
+	PhantomData<(AccountId, BlockNumber, Currency, ReserveId, Stake, Lease)>,
+);
+
+impl<AccountId, BlockNumber, Currency, ReserveId, Stake, Lease>
+	StakeAndSlash<AccountId, BlockNumber, Currency::Balance>
+	for StakeAndSlashNamed<AccountId, BlockNumber, Currency, ReserveId, Stake, Lease>
+where
+	AccountId: Codec + Debug,
+	Currency: NamedReservableCurrency<AccountId>,
+	ReserveId: Get<Currency::ReserveIdentifier>,
+	Stake: Get<Currency::Balance>,
+	Lease: Get<BlockNumber>,
+{
+	type RequiredStake = Stake;
+	type RequiredRegistrationLease = Lease;
+
+	fn reserve(relayer: &AccountId, amount: Currency::Balance) -> DispatchResult {
+		Currency::reserve_named(&ReserveId::get(), relayer, amount)
+	}
+
+	fn unreserve(relayer: &AccountId, amount: Currency::Balance) -> Currency::Balance {
+		Currency::unreserve_named(&ReserveId::get(), relayer, amount)
+	}
+
+	fn repatriate_reserved(
+		relayer: &AccountId,
+		beneficiary: RewardsAccountParams,
+		amount: Currency::Balance,
+	) -> Result<Currency::Balance, DispatchError> {
+		let beneficiary_account =
+			PayRewardFromAccount::<(), AccountId>::rewards_account(beneficiary);
+		Currency::repatriate_reserved_named(
+			&ReserveId::get(),
+			relayer,
+			&beneficiary_account,
+			amount,
+			BalanceStatus::Free,
+		)
+	}
+}
+
+#[cfg(test)]
+mod tests {
+	use super::*;
+	use crate::mock::*;
+
+	use frame_support::traits::fungible::Mutate;
+
+	fn test_stake() -> Balance {
+		Stake::get()
+	}
+
+	#[test]
+	fn reserve_works() {
+		run_test(|| {
+			assert!(TestStakeAndSlash::reserve(&1, test_stake()).is_err());
+			assert_eq!(Balances::free_balance(1), 0);
+			assert_eq!(Balances::reserved_balance(1), 0);
+
+			Balances::mint_into(&2, test_stake() - 1).unwrap();
+			assert!(TestStakeAndSlash::reserve(&2, test_stake()).is_err());
+			assert_eq!(Balances::free_balance(2), test_stake() - 1);
+			assert_eq!(Balances::reserved_balance(2), 0);
+
+			Balances::mint_into(&3, test_stake() * 2).unwrap();
+			assert_eq!(TestStakeAndSlash::reserve(&3, test_stake()), Ok(()));
+			assert_eq!(Balances::free_balance(3), test_stake());
+			assert_eq!(Balances::reserved_balance(3), test_stake());
+		})
+	}
+
+	#[test]
+	fn unreserve_works() {
+		run_test(|| {
+			assert_eq!(TestStakeAndSlash::unreserve(&1, test_stake()), test_stake());
+			assert_eq!(Balances::free_balance(1), 0);
+			assert_eq!(Balances::reserved_balance(1), 0);
+
+			Balances::mint_into(&2, test_stake() * 2).unwrap();
+			TestStakeAndSlash::reserve(&2, test_stake() / 3).unwrap();
+			assert_eq!(
+				TestStakeAndSlash::unreserve(&2, test_stake()),
+				test_stake() - test_stake() / 3
+			);
+			assert_eq!(Balances::free_balance(2), test_stake() * 2);
+			assert_eq!(Balances::reserved_balance(2), 0);
+
+			Balances::mint_into(&3, test_stake() * 2).unwrap();
+			TestStakeAndSlash::reserve(&3, test_stake()).unwrap();
+			assert_eq!(TestStakeAndSlash::unreserve(&3, test_stake()), 0);
+			assert_eq!(Balances::free_balance(3), test_stake() * 2);
+			assert_eq!(Balances::reserved_balance(3), 0);
+		})
+	}
+
+	#[test]
+	fn repatriate_reserved_works() {
+		run_test(|| {
+			let beneficiary = TEST_REWARDS_ACCOUNT_PARAMS;
+			let beneficiary_account = TestPaymentProcedure::rewards_account(beneficiary);
+
+			let mut expected_balance = ExistentialDeposit::get();
+			Balances::mint_into(&beneficiary_account, expected_balance).unwrap();
+
+			assert_eq!(
+				TestStakeAndSlash::repatriate_reserved(&1, beneficiary, test_stake()),
+				Ok(test_stake())
+			);
+			assert_eq!(Balances::free_balance(1), 0);
+			assert_eq!(Balances::reserved_balance(1), 0);
+			assert_eq!(Balances::free_balance(beneficiary_account), expected_balance);
+			assert_eq!(Balances::reserved_balance(beneficiary_account), 0);
+
+			expected_balance += test_stake() / 3;
+			Balances::mint_into(&2, test_stake() * 2).unwrap();
+			TestStakeAndSlash::reserve(&2, test_stake() / 3).unwrap();
+			assert_eq!(
+				TestStakeAndSlash::repatriate_reserved(&2, beneficiary, test_stake()),
+				Ok(test_stake() - test_stake() / 3)
+			);
+			assert_eq!(Balances::free_balance(2), test_stake() * 2 - test_stake() / 3);
+			assert_eq!(Balances::reserved_balance(2), 0);
+			assert_eq!(Balances::free_balance(beneficiary_account), expected_balance);
+			assert_eq!(Balances::reserved_balance(beneficiary_account), 0);
+
+			expected_balance += test_stake();
+			Balances::mint_into(&3, test_stake() * 2).unwrap();
+			TestStakeAndSlash::reserve(&3, test_stake()).unwrap();
+			assert_eq!(
+				TestStakeAndSlash::repatriate_reserved(&3, beneficiary, test_stake()),
+				Ok(0)
+			);
+			assert_eq!(Balances::free_balance(3), test_stake());
+			assert_eq!(Balances::reserved_balance(3), 0);
+			assert_eq!(Balances::free_balance(beneficiary_account), expected_balance);
+			assert_eq!(Balances::reserved_balance(beneficiary_account), 0);
+		})
+	}
+
+	#[test]
+	fn repatriate_reserved_doesnt_work_when_beneficiary_account_is_missing() {
+		run_test(|| {
+			let beneficiary = TEST_REWARDS_ACCOUNT_PARAMS;
+			let beneficiary_account = TestPaymentProcedure::rewards_account(beneficiary);
+
+			Balances::mint_into(&3, test_stake() * 2).unwrap();
+			TestStakeAndSlash::reserve(&3, test_stake()).unwrap();
+			assert!(TestStakeAndSlash::repatriate_reserved(&3, beneficiary, test_stake()).is_err());
+			assert_eq!(Balances::free_balance(3), test_stake());
+			assert_eq!(Balances::reserved_balance(3), test_stake());
+			assert_eq!(Balances::free_balance(beneficiary_account), 0);
+			assert_eq!(Balances::reserved_balance(beneficiary_account), 0);
+		});
+	}
+}
diff --git a/bridges/primitives/relayers/src/lib.rs b/bridges/primitives/relayers/src/lib.rs
index f14b841fa9e..21f66a2ffa1 100644
--- a/bridges/primitives/relayers/src/lib.rs
+++ b/bridges/primitives/relayers/src/lib.rs
@@ -19,6 +19,8 @@
 #![warn(missing_docs)]
 #![cfg_attr(not(feature = "std"), no_std)]
 
+pub use registration::{Registration, StakeAndSlash};
+
 use bp_messages::LaneId;
 use bp_runtime::{ChainId, StorageDoubleMapKeyProvider};
 use frame_support::{traits::tokens::Preservation, Blake2_128Concat, Identity};
@@ -30,6 +32,8 @@ use sp_runtime::{
 };
 use sp_std::{fmt::Debug, marker::PhantomData};
 
+mod registration;
+
 /// The owner of the sovereign account that should pay the rewards.
 ///
 /// Each of the 2 final points connected by a bridge owns a sovereign account at each end of the
diff --git a/bridges/primitives/relayers/src/registration.rs b/bridges/primitives/relayers/src/registration.rs
new file mode 100644
index 00000000000..da64bdde379
--- /dev/null
+++ b/bridges/primitives/relayers/src/registration.rs
@@ -0,0 +1,121 @@
+// Copyright 2021 Parity Technologies (UK) Ltd.
+// This file is part of Parity Bridges Common.
+
+// Parity Bridges Common is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Parity Bridges Common is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Parity Bridges Common.  If not, see <http://www.gnu.org/licenses/>.
+
+//! Bridge relayers registration and slashing scheme.
+//!
+//! There is an option to add a refund-relayer signed extension that will compensate
+//! relayer costs of the message delivery and confirmation transactions (as well as
+//! required finality proofs). This extension boosts priority of message delivery
+//! transactions, based on the number of bundled messages. So transaction with more
+//! messages has larger priority than the transaction with less messages.
+//! See [`bridge_runtime_common::priority_calculator`] for details;
+//!
+//! This encourages relayers to include more messages to their delivery transactions.
+//! At the same time, we are not verifying storage proofs before boosting
+//! priority. Instead, we simply trust relayer, when it says that transaction delivers
+//! `N` messages.
+//!
+//! This allows relayers to submit transactions which declare large number of bundled
+//! transactions to receive priority boost for free, potentially pushing actual delivery
+//! transactions from the block (or even transaction queue). Such transactions are
+//! not free, but their cost is relatively small.
+//!
+//! To alleviate that, we only boost transactions of relayers that have some stake
+//! that guarantees that their transactions are valid. Such relayers get priority
+//! for free, but they risk to lose their stake.
+
+use crate::RewardsAccountParams;
+
+use codec::{Decode, Encode, MaxEncodedLen};
+use scale_info::TypeInfo;
+use sp_runtime::{
+	traits::{Get, Zero},
+	DispatchError, DispatchResult,
+};
+
+/// Relayer registration.
+#[derive(Copy, Clone, Debug, Decode, Encode, Eq, PartialEq, TypeInfo, MaxEncodedLen)]
+pub struct Registration<BlockNumber, Balance> {
+	/// The last block number, where this registration is considered active.
+	///
+	/// Relayer has an option to renew his registration (this may be done before it
+	/// is spoiled as well). Starting from block `valid_till + 1`, relayer may `deregister`
+	/// himself and get his stake back.
+	///
+	/// Please keep in mind that priority boost stops working some blocks before the
+	/// registration ends (see [`StakeAndSlash::RequiredRegistrationLease`]).
+	pub valid_till: BlockNumber,
+	/// Active relayer stake, which is mapped to the relayer reserved balance.
+	///
+	/// If `stake` is less than the [`StakeAndSlash::RequiredStake`], the registration
+	/// is considered inactive even if `valid_till + 1` is not yet reached.
+	pub stake: Balance,
+}
+
+/// Relayer stake-and-slash mechanism.
+pub trait StakeAndSlash<AccountId, BlockNumber, Balance> {
+	/// The stake that the relayer must have to have its transactions boosted.
+	type RequiredStake: Get<Balance>;
+	/// Required **remaining** registration lease to be able to get transaction priority boost.
+	///
+	/// If the difference between registration's `valid_till` and the current block number
+	/// is less than the `RequiredRegistrationLease`, it becomes inactive and relayer transaction
+	/// won't get priority boost. This period exists, because priority is calculated when
+	/// transaction is placed to the queue (and it is reevaluated periodically) and then some time
+	/// may pass before transaction will be included into the block.
+	type RequiredRegistrationLease: Get<BlockNumber>;
+
+	/// Reserve the given amount at relayer account.
+	fn reserve(relayer: &AccountId, amount: Balance) -> DispatchResult;
+	/// `Unreserve` the given amount from relayer account.
+	///
+	/// Returns amount that we have failed to `unreserve`.
+	fn unreserve(relayer: &AccountId, amount: Balance) -> Balance;
+	/// Slash up to `amount` from reserved balance of account `relayer` and send funds to given
+	/// `beneficiary`.
+	///
+	/// Returns `Ok(_)` with non-zero balance if we have failed to repatriate some portion of stake.
+	fn repatriate_reserved(
+		relayer: &AccountId,
+		beneficiary: RewardsAccountParams,
+		amount: Balance,
+	) -> Result<Balance, DispatchError>;
+}
+
+impl<AccountId, BlockNumber, Balance> StakeAndSlash<AccountId, BlockNumber, Balance> for ()
+where
+	Balance: Default + Zero,
+	BlockNumber: Default,
+{
+	type RequiredStake = ();
+	type RequiredRegistrationLease = ();
+
+	fn reserve(_relayer: &AccountId, _amount: Balance) -> DispatchResult {
+		Ok(())
+	}
+
+	fn unreserve(_relayer: &AccountId, _amount: Balance) -> Balance {
+		Zero::zero()
+	}
+
+	fn repatriate_reserved(
+		_relayer: &AccountId,
+		_beneficiary: RewardsAccountParams,
+		_amount: Balance,
+	) -> Result<Balance, DispatchError> {
+		Ok(Zero::zero())
+	}
+}
-- 
GitLab