mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-05-31 06:21:02 +00:00
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:
committed by
Bastian Köcher
parent
3b47f957db
commit
53e1b7e264
@@ -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 = ();
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = ();
|
||||
}
|
||||
|
||||
|
||||
@@ -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)>,
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(®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::<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(®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::<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(®ISTER_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(®ISTER_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(®ISTER_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(®ISTER_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(®ISTER_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(®ISTER_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(®ISTER_RELAYER));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(®ISTER_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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user