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>
This commit is contained in:
Svyatoslav Nikolsky
2023-04-25 16:24:13 +03:00
committed by Bastian Köcher
parent 3b47f957db
commit 53e1b7e264
12 changed files with 1497 additions and 185 deletions
+9
View File
@@ -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 = ();
}
@@ -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 = ();
}
+1
View File
@@ -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 = ();
}
+2
View File
@@ -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",
@@ -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)>,
+23 -1
View File
@@ -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)
}
@@ -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,6 +310,179 @@ where
call.check_obsolete_call()?;
Ok(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;
// 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,
};
// 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
},
);
// 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);
// We don't refund anything if the transaction has failed.
if let Err(e) = result {
log::trace!(
target: "runtime::bridge",
"{} from parachain {} via {:?}: relayer {:?} has submitted invalid messages transaction: {:?}",
Self::IDENTIFIER,
Para::Id::get(),
Msgs::Id::get(),
relayer,
e,
);
return slash_relayer_if_delivery_result
}
// 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(
finality_proof_info.block_number,
) {
// we only refund relayer if all calls have updated chain state
log::trace!(
target: "runtime::bridge",
"{} from parachain {} via {:?}: relayer {:?} has submitted invalid relay chain finality proof",
Self::IDENTIFIER,
Para::Id::get(),
Msgs::Id::get(),
relayer,
);
return slash_relayer_if_delivery_result;
}
// there's a conflict between how bridge GRANDPA pallet works and a `utility.batchAll`
// transaction. If relay chain header is mandatory, the GRANDPA pallet returns
// `Pays::No`, because such transaction is mandatory for operating the bridge. But
// `utility.batchAll` transaction always requires payment. But in both cases we'll
// refund relayer - either explicitly here, or using `Pays::No` if he's choosing
// to submit dedicated transaction.
// submitter has means to include extra weight/bytes in the `submit_finality_proof`
// call, so let's subtract extra weight/size to avoid refunding for this extra stuff
extra_weight = finality_proof_info.extra_weight;
extra_size = finality_proof_info.extra_size;
}
// check if parachain state has been updated
if let Some(para_proof_info) = call_info.submit_parachain_heads_info() {
if !SubmitParachainHeadsHelper::<Runtime, Para::Instance>::was_successful(
para_proof_info,
) {
// we only refund relayer if all calls have updated chain state
log::trace!(
target: "runtime::bridge",
"{} from parachain {} via {:?}: relayer {:?} has submitted invalid parachain finality proof",
Self::IDENTIFIER,
Para::Id::get(),
Msgs::Id::get(),
relayer,
);
return slash_relayer_if_delivery_result
}
}
// 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 {:?}: relayer {:?} has submitted invalid messages call",
Self::IDENTIFIER,
Para::Id::get(),
Msgs::Id::get(),
relayer,
);
return slash_relayer_if_delivery_result
}
// regarding the tip - refund that happens here (at this side of the bridge) isn't the whole
// relayer compensation. He'll receive some amount at the other side of the bridge. It shall
// (in theory) cover the tip there. Otherwise, if we'll be compensating tip here, some
// malicious relayer may use huge tips, effectively depleting account that pay rewards. The
// cost of this attack is nothing. Hence we use zero as tip here.
let tip = Zero::zero();
// decrease post-dispatch weight/size using extra weight/size that we know now
let post_info_len = len.saturating_sub(extra_size as usize);
let mut post_info = *post_info;
post_info.actual_weight =
Some(post_info.actual_weight.unwrap_or(info.weight).saturating_sub(extra_weight));
// compute the relayer refund
let refund = Refund::compute_refund(info, &post_info, post_info_len, tip);
// 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,
};
// 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
@@ -302,37 +517,49 @@ where
fn validate(
&self,
_who: &Self::AccountId,
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
// 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
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 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 boosted priority of message delivery transaction \
of relayer {:?}: {} messages -> {} priority",
Self::IDENTIFIER,
Para::Id::get(),
Msgs::Id::get(),
who,
bundled_messages,
priority_boost,
);
valid_transaction.build()
}
@@ -366,118 +593,15 @@ where
len: usize,
result: &DispatchResult,
) -> Result<(), TransactionValidityError> {
let mut extra_weight = Weight::zero();
let mut extra_size = 0;
let call_result = Self::analyze_call_result(pre, info, post_info, len, result);
// We don't refund anything if the transaction has failed.
if result.is_err() {
return Ok(())
}
// 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(
finality_proof_info.block_number,
) {
// 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",
Self::IDENTIFIER,
Para::Id::get(),
Msgs::Id::get(),
relayer,
);
return Ok(())
}
// there's a conflict between how bridge GRANDPA pallet works and a `utility.batchAll`
// transaction. If relay chain header is mandatory, the GRANDPA pallet returns
// `Pays::No`, because such transaction is mandatory for operating the bridge. But
// `utility.batchAll` transaction always requires payment. But in both cases we'll
// refund relayer - either explicitly here, or using `Pays::No` if he's choosing
// to submit dedicated transaction.
// submitter has means to include extra weight/bytes in the `submit_finality_proof`
// call, so let's subtract extra weight/size to avoid refunding for this extra stuff
extra_weight = finality_proof_info.extra_weight;
extra_size = finality_proof_info.extra_size;
}
// check if parachain state has been updated
if let Some(para_proof_info) = call_info.submit_parachain_heads_info() {
if !SubmitParachainHeadsHelper::<Runtime, Para::Instance>::was_successful(
para_proof_info,
) {
// 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",
Self::IDENTIFIER,
Para::Id::get(),
Msgs::Id::get(),
relayer,
);
return Ok(())
}
}
// Check if the `ReceiveMessagesProof` call delivered all 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",
Self::IDENTIFIER,
Para::Id::get(),
Msgs::Id::get(),
relayer,
);
return Ok(())
}
// regarding the tip - refund that happens here (at this side of the bridge) isn't the whole
// relayer compensation. He'll receive some amount at the other side of the bridge. It shall
// (in theory) cover the tip there. Otherwise, if we'll be compensating tip here, some
// malicious relayer may use huge tips, effectively depleting account that pay rewards. The
// cost of this attack is nothing. Hence we use zero as tip here.
let tip = Zero::zero();
// decrease post-dispatch weight/size using extra weight/size that we know now
let post_info_len = len.saturating_sub(extra_size as usize);
let mut post_info = *post_info;
post_info.actual_weight =
Some(post_info.actual_weight.unwrap_or(info.weight).saturating_sub(extra_weight));
// 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,
};
match call_result {
RelayerAccountAction::None => (),
RelayerAccountAction::Reward(relayer, reward_account, reward) => {
RelayersPallet::<Runtime>::register_relayer_reward(
RewardsAccountParams::new(
Msgs::Id::get(),
Runtime::BridgedChainId::get(),
rewards_account_owner,
),
reward_account,
&relayer,
refund,
reward,
);
log::trace!(
@@ -486,9 +610,13 @@ where
Self::IDENTIFIER,
Para::Id::get(),
Msgs::Id::get(),
refund,
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,
);
});
}
}
+573 -6
View File
@@ -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)]
@@ -255,8 +531,8 @@ mod tests {
// 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));
});
}
}
+38 -7
View File
@@ -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()
})
}
@@ -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);
});
}
}
+4
View File
@@ -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
@@ -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())
}
}