From 53e1b7e2641866fd95cbb75303c1c92aa3d470ed Mon Sep 17 00:00:00 2001 From: Svyatoslav Nikolsky 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 * Update primitives/relayers/src/registration.rs Co-authored-by: Adrian Catangiu * Update bin/runtime-common/src/refund_relayer_extension.rs Co-authored-by: Adrian Catangiu * Update bin/runtime-common/src/refund_relayer_extension.rs Co-authored-by: Adrian Catangiu * Update bin/runtime-common/src/refund_relayer_extension.rs Co-authored-by: Adrian Catangiu * Update modules/relayers/src/lib.rs Co-authored-by: Adrian Catangiu * Update primitives/relayers/src/registration.rs Co-authored-by: Adrian Catangiu * benificiary -> beneficiary --------- Co-authored-by: Adrian Catangiu --- 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 4e6f1e43e8c6..dccd75a5b001 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, 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 cd4e256f4203..cd5c45ec4ba9 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, AccountId>; + type StakeAndSlash = (); type WeightInfo = (); } diff --git a/bridges/bin/rialto/runtime/src/lib.rs b/bridges/bin/rialto/runtime/src/lib.rs index b325332acba5..0d2c667efa53 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, AccountId>; + type StakeAndSlash = (); type WeightInfo = (); } diff --git a/bridges/bin/runtime-common/Cargo.toml b/bridges/bin/runtime-common/Cargo.toml index 3db4ae9abca6..e7cd39da90b1 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 f3665a8d93b5..3f48ce583f9c 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 { + 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, 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 036813f6fd51..c17671996761 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; +/// Rewards payment procedure. +pub type TestPaymentProcedure = PayRewardFromAccount; +/// 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 925fea2a7434..7d65263e9fd0 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 = ::AccountId; // without this typedef rustfmt fails with internal err type BalanceOf = <::OnChargeTransaction as OnChargeTransaction>::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> { 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 { + /// 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( - 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 @@ -215,9 +253,13 @@ where Runtime: UtilityConfig> + BoundedBridgeGrandpaConfig + ParachainsConfig - + MessagesConfig, + + MessagesConfig + + RelayersConfig, Para: RefundableParachainId, Msgs: RefundableMessagesLaneId, + Refund: RefundCalculator, + Priority: Get, + Id: StaticStrProvider, CallOf: Dispatchable + IsSubType, Runtime>> + GrandpaCallSubType @@ -268,118 +310,69 @@ where call.check_obsolete_call()?; Ok(call) } -} - -impl SignedExtension - for RefundBridgedParachainMessages -where - Self: 'static + Send + Sync, - Runtime: UtilityConfig> - + BoundedBridgeGrandpaConfig - + ParachainsConfig - + MessagesConfig - + RelayersConfig, - Para: RefundableParachainId, - Msgs: RefundableMessagesLaneId, - Refund: RefundCalculator, - Priority: Get, - Id: StaticStrProvider, - CallOf: Dispatchable - + IsSubType, Runtime>> - + GrandpaCallSubType - + ParachainsCallSubType - + MessagesCallSubType, -{ - const IDENTIFIER: &'static str = Id::STR; - type AccountId = Runtime::AccountId; - type Call = CallOf; - type AdditionalSigned = (); - type Pre = Option>; - - fn additional_signed(&self) -> Result<(), TransactionValidityError> { - Ok(()) - } - fn validate( - &self, - _who: &Self::AccountId, - call: &Self::Call, - _info: &DispatchInfoOf, - _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>>, + info: &DispatchInfo, + post_info: &PostDispatchInfo, + len: usize, + result: &DispatchResult, + ) -> RelayerAccountAction, 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, - _len: usize, - ) -> Result { - // 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, - info: &DispatchInfoOf, - post_info: &PostDispatchInfoOf, - 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::::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::::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 { + // 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::::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 SignedExtension + for RefundBridgedParachainMessages +where + Self: 'static + Send + Sync, + Runtime: UtilityConfig> + + BoundedBridgeGrandpaConfig + + ParachainsConfig + + MessagesConfig + + RelayersConfig, + Para: RefundableParachainId, + Msgs: RefundableMessagesLaneId, + Refund: RefundCalculator, + Priority: Get, + Id: StaticStrProvider, + CallOf: Dispatchable + + IsSubType, Runtime>> + + GrandpaCallSubType + + ParachainsCallSubType + + MessagesCallSubType, +{ + const IDENTIFIER: &'static str = Id::STR; + type AccountId = Runtime::AccountId; + type Call = CallOf; + type AdditionalSigned = (); + type Pre = Option>; + + fn additional_signed(&self) -> Result<(), TransactionValidityError> { + Ok(()) + } + + fn validate( + &self, + who: &Self::AccountId, + call: &Self::Call, + _info: &DispatchInfoOf, + _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::::is_registration_active(who) { + return Ok(Default::default()) + } + + // compute priority boost + let priority_boost = + crate::priority_calculator::compute_priority_boost::(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, + _len: usize, + ) -> Result { + // 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, + info: &DispatchInfoOf, + post_info: &PostDispatchInfoOf, + 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::::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::::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::::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, + end: MessageNonce, + ) -> PreDispatchData { + 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>, 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, + dispatch_result: DispatchResult, + ) -> RelayerAccountAction { + 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 bd33b811b304..14e44d30f89e 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> + IsType<::RuntimeEvent>; /// Type of relayer reward. type Reward: AtLeast32BitUnsigned + Copy + Parameter + MaxEncodedLen; - /// Pay rewards adapter. + /// Pay rewards scheme. type PaymentProcedure: PaymentProcedure; + /// Stake and slash scheme. + type StakeAndSlash: StakeAndSlash; /// 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, 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::::block_number()); + ensure!( + lease > Pallet::::required_registration_lease(), + Error::::InvalidRegistrationLease + ); + + RegisteredRelayers::::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::::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::::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(®istration.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::::FailedToReserve + })?; + } + registration.stake = required_stake; + + Self::deposit_event(Event::::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) -> DispatchResult { + let relayer = ensure_signed(origin)?; + + RegisteredRelayers::::try_mutate(&relayer, |maybe_registration| -> DispatchResult { + let registration = match maybe_registration.take() { + Some(registration) => registration, + None => fail!(Error::::NotRegistered), + }; + + // we can't deregister until `valid_till + 1` + ensure!( + registration.valid_till < frame_system::Pallet::::block_number(), + Error::::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::::Deregistered { relayer: relayer.clone() }); + + *maybe_registration = None; + + Ok(()) + }) + } } impl Pallet { + /// 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::::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::::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 { + >::RequiredRegistrationLease::get() + } + + /// Return required stake. + fn required_stake() -> 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::::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, + }, + /// 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, + }, } #[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 { 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 = StorageMap< + _, + Blake2_128Concat, + T::AccountId, + Registration, + 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::::events(), - vec![EventRecord { + System::::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::::set_block_number(100); + + assert_noop!( + Pallet::::register(RuntimeOrigin::signed(REGISTER_RELAYER), 50), + Error::::InvalidRegistrationLease, + ); + }); + } + + #[test] + fn register_fails_if_valid_till_lease_is_less_than_required() { + run_test(|| { + System::::set_block_number(100); + + assert_noop!( + Pallet::::register( + RuntimeOrigin::signed(REGISTER_RELAYER), + 99 + Lease::get() + ), + Error::::InvalidRegistrationLease, + ); + }); + } + + #[test] + fn register_works() { + run_test(|| { + get_ready_for_events(); + + assert_ok!(Pallet::::register( + RuntimeOrigin::signed(REGISTER_RELAYER), + 150 + )); + assert_eq!(Balances::reserved_balance(REGISTER_RELAYER), Stake::get()); + assert_eq!( + Pallet::::registered_relayer(REGISTER_RELAYER), + Some(Registration { valid_till: 150, stake: Stake::get() }), + ); + + assert_eq!( + System::::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::::register( + RuntimeOrigin::signed(REGISTER_RELAYER), + 150 + )); + + assert_noop!( + Pallet::::register(RuntimeOrigin::signed(REGISTER_RELAYER), 125), + Error::::CannotReduceRegistrationLease, + ); + }); + } + + #[test] + fn register_fails_if_it_cant_unreserve_some_balance_if_required_stake_decreases() { + run_test(|| { + RegisteredRelayers::::insert( + REGISTER_RELAYER, + Registration { valid_till: 150, stake: Stake::get() + 1 }, + ); + + assert_noop!( + Pallet::::register(RuntimeOrigin::signed(REGISTER_RELAYER), 150), + Error::::FailedToUnreserve, + ); + }); + } + + #[test] + fn register_unreserves_some_balance_if_required_stake_decreases() { + run_test(|| { + get_ready_for_events(); + + RegisteredRelayers::::insert( + REGISTER_RELAYER, + Registration { valid_till: 150, stake: Stake::get() + 1 }, + ); + TestStakeAndSlash::reserve(®ISTER_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::::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::::registered_relayer(REGISTER_RELAYER), + Some(Registration { valid_till: 150, stake: Stake::get() }), + ); + + assert_eq!( + System::::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(®ISTER_RELAYER, 0); + assert_noop!( + Pallet::::register(RuntimeOrigin::signed(REGISTER_RELAYER), 150), + Error::::FailedToReserve, + ); + }); + } + + #[test] + fn register_fails_if_it_cant_reserve_some_balance_if_required_stake_increases() { + run_test(|| { + RegisteredRelayers::::insert( + REGISTER_RELAYER, + Registration { valid_till: 150, stake: Stake::get() - 1 }, + ); + Balances::set_balance(®ISTER_RELAYER, 0); + + assert_noop!( + Pallet::::register(RuntimeOrigin::signed(REGISTER_RELAYER), 150), + Error::::FailedToReserve, + ); + }); + } + + #[test] + fn register_reserves_some_balance_if_required_stake_increases() { + run_test(|| { + get_ready_for_events(); + + RegisteredRelayers::::insert( + REGISTER_RELAYER, + Registration { valid_till: 150, stake: Stake::get() - 1 }, + ); + TestStakeAndSlash::reserve(®ISTER_RELAYER, Stake::get() - 1).unwrap(); + + let free_balance = Balances::free_balance(REGISTER_RELAYER); + assert_ok!(Pallet::::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::::registered_relayer(REGISTER_RELAYER), + Some(Registration { valid_till: 150, stake: Stake::get() }), + ); + + assert_eq!( + System::::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::::deregister(RuntimeOrigin::signed(REGISTER_RELAYER)), + Error::::NotRegistered, + ); + }); + } + + #[test] + fn deregister_fails_if_registration_is_still_active() { + run_test(|| { + assert_ok!(Pallet::::register( + RuntimeOrigin::signed(REGISTER_RELAYER), + 150 + )); + + System::::set_block_number(100); + + assert_noop!( + Pallet::::deregister(RuntimeOrigin::signed(REGISTER_RELAYER)), + Error::::RegistrationIsStillActive, + ); + }); + } + + #[test] + fn deregister_works() { + run_test(|| { + get_ready_for_events(); + + assert_ok!(Pallet::::register( + RuntimeOrigin::signed(REGISTER_RELAYER), + 150 + )); + + System::::set_block_number(151); + + let reserved_balance = Balances::reserved_balance(REGISTER_RELAYER); + let free_balance = Balances::free_balance(REGISTER_RELAYER); + assert_ok!(Pallet::::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::::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::::is_registration_active(®ISTER_RELAYER)); + }); + } + + #[test] + fn is_registration_active_is_false_when_stake_is_too_low() { + run_test(|| { + RegisteredRelayers::::insert( + REGISTER_RELAYER, + Registration { valid_till: 150, stake: Stake::get() - 1 }, + ); + assert!(!Pallet::::is_registration_active(®ISTER_RELAYER)); + }); + } + + #[test] + fn is_registration_active_is_false_when_remaining_lease_is_too_low() { + run_test(|| { + System::::set_block_number(150 - Lease::get()); + + RegisteredRelayers::::insert( + REGISTER_RELAYER, + Registration { valid_till: 150, stake: Stake::get() }, + ); + assert!(!Pallet::::is_registration_active(®ISTER_RELAYER)); + }); + } + + #[test] + fn is_registration_active_is_true_when_relayer_is_properly_registeered() { + run_test(|| { + System::::set_block_number(150 - Lease::get()); + + RegisteredRelayers::::insert( + REGISTER_RELAYER, + Registration { valid_till: 151, stake: Stake::get() }, + ); + assert!(Pallet::::is_registration_active(®ISTER_RELAYER)); + }); + } } diff --git a/bridges/modules/relayers/src/mock.rs b/bridges/modules/relayers/src/mock.rs index fe8c586eecc5..406a365f3509 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; type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; @@ -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; 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 for TestPaymentProcedure { type Error = (); @@ -147,5 +173,10 @@ pub fn new_test_ext() -> sp_io::TestExternalities { /// Run pallet test. pub fn run_test(test: impl FnOnce() -> T) -> T { - new_test_ext().execute_with(test) + new_test_ext().execute_with(|| { + Balances::mint_into(®ISTER_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 000000000000..055b6a111ec7 --- /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 . + +//! 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( + PhantomData<(AccountId, BlockNumber, Currency, ReserveId, Stake, Lease)>, +); + +impl + StakeAndSlash + for StakeAndSlashNamed +where + AccountId: Codec + Debug, + Currency: NamedReservableCurrency, + ReserveId: Get, + Stake: Get, + Lease: Get, +{ + 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 { + 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 f14b841fa9eb..21f66a2ffa10 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 000000000000..da64bdde3793 --- /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 . + +//! 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 { + /// 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 { + /// The stake that the relayer must have to have its transactions boosted. + type RequiredStake: Get; + /// 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; + + /// 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; +} + +impl StakeAndSlash 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 { + Ok(Zero::zero()) + } +}