From 5a77f3c5afbfb2a7bc791ad282695091305bad84 Mon Sep 17 00:00:00 2001 From: Svyatoslav Nikolsky Date: Fri, 9 Dec 2022 12:15:27 +0300 Subject: [PATCH] Signed extension to refund relayer at the target chain (#1657) * add utlity pallet to the Millau runtime * RefundRelayerForMessagesDeliveryFromParachain prototype * done with RefundRelayerForMessagesDeliveryFromParachain::post_dispatch * parse calls * check batch for obsolete headers/messages * fmt * shorten generic arg names + add parachain id generic arg * check lane_id * impl all state read functions * fix typos from review * renamed extension + reference issue from TODO * tests for pre-dispaytch * renamed extension source file * tests for post-dispatch * abstract fee calculation * clippy * actually fix clippy * Update bin/runtime-common/src/refund_relayer_extension.rs Co-authored-by: Adrian Catangiu * Update bin/runtime-common/src/refund_relayer_extension.rs Co-authored-by: Adrian Catangiu * Update bin/runtime-common/src/refund_relayer_extension.rs Co-authored-by: Adrian Catangiu * Update bin/runtime-common/src/refund_relayer_extension.rs Co-authored-by: Adrian Catangiu Co-authored-by: Adrian Catangiu --- bridges/bin/millau/runtime/Cargo.toml | 2 + bridges/bin/millau/runtime/src/lib.rs | 8 + bridges/bin/runtime-common/Cargo.toml | 8 + bridges/bin/runtime-common/src/lib.rs | 3 +- .../src/refund_relayer_extension.rs | 845 ++++++++++++++++++ bridges/modules/grandpa/src/lib.rs | 7 + bridges/modules/messages/src/lib.rs | 9 +- bridges/modules/parachains/src/lib.rs | 5 + bridges/modules/relayers/Cargo.toml | 2 + bridges/modules/relayers/src/lib.rs | 4 +- 10 files changed, 889 insertions(+), 4 deletions(-) create mode 100644 bridges/bin/runtime-common/src/refund_relayer_extension.rs diff --git a/bridges/bin/millau/runtime/Cargo.toml b/bridges/bin/millau/runtime/Cargo.toml index b297dedfa5375..7605a642cdcc9 100644 --- a/bridges/bin/millau/runtime/Cargo.toml +++ b/bridges/bin/millau/runtime/Cargo.toml @@ -49,6 +49,7 @@ pallet-sudo = { git = "https://github.com/paritytech/substrate", branch = "maste pallet-timestamp = { 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-transaction-payment-rpc-runtime-api = { 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 } sp-block-builder = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false } sp-consensus-aura = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false } @@ -113,6 +114,7 @@ std = [ "pallet-timestamp/std", "pallet-transaction-payment-rpc-runtime-api/std", "pallet-transaction-payment/std", + "pallet-utility/std", "pallet-xcm/std", "scale-info/std", "sp-api/std", diff --git a/bridges/bin/millau/runtime/src/lib.rs b/bridges/bin/millau/runtime/src/lib.rs index 2bc26002b84f2..09171dd640c60 100644 --- a/bridges/bin/millau/runtime/src/lib.rs +++ b/bridges/bin/millau/runtime/src/lib.rs @@ -550,6 +550,13 @@ impl pallet_bridge_parachains::Config for Runtime type MaxParaHeadSize = MaxWestendParaHeadSize; } +impl pallet_utility::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type RuntimeCall = RuntimeCall; + type PalletsOrigin = OriginCaller; + type WeightInfo = (); +} + construct_runtime!( pub enum Runtime where Block = Block, @@ -558,6 +565,7 @@ construct_runtime!( { System: frame_system::{Pallet, Call, Config, Storage, Event}, Sudo: pallet_sudo::{Pallet, Call, Config, Storage, Event}, + Utility: pallet_utility, // Must be before session. Aura: pallet_aura::{Pallet, Config}, diff --git a/bridges/bin/runtime-common/Cargo.toml b/bridges/bin/runtime-common/Cargo.toml index 4f8f7e34c4042..9153d7a05dda8 100644 --- a/bridges/bin/runtime-common/Cargo.toml +++ b/bridges/bin/runtime-common/Cargo.toml @@ -23,12 +23,15 @@ bp-runtime = { path = "../../primitives/runtime", default-features = false } pallet-bridge-grandpa = { path = "../../modules/grandpa", default-features = false } pallet-bridge-messages = { path = "../../modules/messages", default-features = false } pallet-bridge-parachains = { path = "../../modules/parachains", default-features = false } +pallet-bridge-relayers = { path = "../../modules/relayers", default-features = false } # Substrate dependencies 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, optional = true } +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 } sp-core = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false } sp-io = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false } @@ -43,6 +46,8 @@ xcm-builder = { git = "https://github.com/paritytech/polkadot", branch = "master xcm-executor = { git = "https://github.com/paritytech/polkadot", branch = "master", default-features = false } [dev-dependencies] +bp-rialto = { path = "../../primitives/chain-rialto" } +bp-test-utils = { path = "../../primitives/test-utils" } millau-runtime = { path = "../millau/runtime" } [features] @@ -61,6 +66,9 @@ std = [ "pallet-bridge-grandpa/std", "pallet-bridge-messages/std", "pallet-bridge-parachains/std", + "pallet-bridge-relayers/std", + "pallet-transaction-payment/std", + "pallet-utility/std", "pallet-xcm/std", "scale-info/std", "sp-api/std", diff --git a/bridges/bin/runtime-common/src/lib.rs b/bridges/bin/runtime-common/src/lib.rs index ca8f2268404e5..f8d2e7a039ebb 100644 --- a/bridges/bin/runtime-common/src/lib.rs +++ b/bridges/bin/runtime-common/src/lib.rs @@ -27,6 +27,7 @@ pub mod messages_api; pub mod messages_benchmarking; pub mod messages_extension; pub mod parachains_benchmarking; +pub mod refund_relayer_extension; mod messages_generation; @@ -78,7 +79,7 @@ where #[macro_export] macro_rules! generate_bridge_reject_obsolete_headers_and_messages { ($call:ty, $account_id:ty, $($filter_call:ty),*) => { - #[derive(Clone, codec::Decode, codec::Encode, Eq, PartialEq, frame_support::RuntimeDebug, scale_info::TypeInfo)] + #[derive(Clone, codec::Decode, Default, codec::Encode, Eq, PartialEq, frame_support::RuntimeDebug, scale_info::TypeInfo)] pub struct BridgeRejectObsoleteHeadersAndMessages; impl sp_runtime::traits::SignedExtension for BridgeRejectObsoleteHeadersAndMessages { const IDENTIFIER: &'static str = "BridgeRejectObsoleteHeadersAndMessages"; diff --git a/bridges/bin/runtime-common/src/refund_relayer_extension.rs b/bridges/bin/runtime-common/src/refund_relayer_extension.rs new file mode 100644 index 0000000000000..cb3448d3dd4cd --- /dev/null +++ b/bridges/bin/runtime-common/src/refund_relayer_extension.rs @@ -0,0 +1,845 @@ +// 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 . + +//! Signed extension that refunds relayer if he has delivered some new messages. +//! It also refunds transaction cost if the transaction is an `utility.batchAll()` +//! with calls that are: delivering new messsage and all necessary underlying headers +//! (parachain or relay chain). + +// hack because we have circular (test-level) dependency between `millau-runtime` +// and `bridge-runtime-common` crates +#[cfg(not(test))] +use crate::messages::target::FromBridgedChainMessagesProof; +#[cfg(test)] +use millau_runtime::bridge_runtime_common::messages::target::FromBridgedChainMessagesProof; + +use bp_messages::{target_chain::SourceHeaderChain, LaneId, MessageNonce}; +use bp_polkadot_core::parachains::ParaId; +use bp_runtime::{Chain, HashOf}; +use codec::{Decode, Encode}; +use frame_support::{ + dispatch::{CallableCallFor, DispatchInfo, Dispatchable, PostDispatchInfo}, + traits::IsSubType, + CloneNoBound, EqNoBound, PartialEqNoBound, RuntimeDebugNoBound, +}; +use pallet_bridge_grandpa::{ + BridgedChain, Call as GrandpaCall, Config as GrandpaConfig, Pallet as GrandpaPallet, +}; +use pallet_bridge_messages::{ + Call as MessagesCall, Config as MessagesConfig, Pallet as MessagesPallet, +}; +use pallet_bridge_parachains::{ + Call as ParachainsCall, Config as ParachainsConfig, Pallet as ParachainsPallet, RelayBlockHash, + RelayBlockHasher, RelayBlockNumber, +}; +use pallet_bridge_relayers::{Config as RelayersConfig, Pallet as RelayersPallet}; +use pallet_transaction_payment::{Config as TransactionPaymentConfig, OnChargeTransaction}; +use pallet_utility::{Call as UtilityCall, Config as UtilityConfig, Pallet as UtilityPallet}; +use scale_info::TypeInfo; +use sp_runtime::{ + traits::{DispatchInfoOf, Get, Header as HeaderT, PostDispatchInfoOf, SignedExtension, Zero}, + transaction_validity::{TransactionValidity, TransactionValidityError, ValidTransaction}, + DispatchResult, FixedPointOperand, +}; +use sp_std::marker::PhantomData; + +// TODO (https://github.com/paritytech/parity-bridges-common/issues/1667): +// support multiple bridges in this extension + +/// Transaction fee calculation. +pub trait TransactionFeeCalculation { + /// Compute fee that is paid for given transaction. The fee is later refunded to relayer. + fn compute_fee( + info: &DispatchInfo, + post_info: &PostDispatchInfo, + len: usize, + tip: Balance, + ) -> Balance; +} + +impl TransactionFeeCalculation> for R +where + R: TransactionPaymentConfig, + ::RuntimeCall: + Dispatchable, + BalanceOf: FixedPointOperand, +{ + fn compute_fee( + info: &DispatchInfo, + post_info: &PostDispatchInfo, + len: usize, + tip: BalanceOf, + ) -> BalanceOf { + pallet_transaction_payment::Pallet::::compute_actual_fee(len as _, info, post_info, tip) + } +} +/// Signed extension that refunds relayer for new messages coming from the parachain. +/// +/// Also refunds relayer for successful finality delivery if it comes in batch (`utility.batchAll`) +/// with message delivery transaction. Batch may deliver either both relay chain header and +/// parachain head, or just parachain head. Corresponding headers must be used in messages +/// proof verification. +/// +/// Extension does not refund transaction tip due to security reasons. +#[derive( + CloneNoBound, Decode, Encode, EqNoBound, PartialEqNoBound, RuntimeDebugNoBound, TypeInfo, +)] +#[scale_info(skip_type_params(RT, GI, PI, MI, BE, PID, LID, FEE))] +#[allow(clippy::type_complexity)] // TODO: get rid of that in https://github.com/paritytech/parity-bridges-common/issues/1666 +pub struct RefundRelayerForMessagesFromParachain( + PhantomData<(RT, GI, PI, MI, BE, PID, LID, FEE)>, +); + +/// Data that is crafted in `pre_dispatch` method and used at `post_dispatch`. +#[derive(PartialEq)] +#[cfg_attr(test, derive(Debug))] +pub struct PreDispatchData { + /// Transaction submitter (relayer) account. + pub relayer: AccountId, + /// Type of the call. + pub call_type: CallType, +} + +/// Type of the call that the extension recognizes. +#[derive(Clone, Copy, PartialEq, RuntimeDebugNoBound)] +pub enum CallType { + /// Relay chain finality + parachain finality + message delivery calls. + AllFinalityAndDelivery(ExpectedRelayChainState, ExpectedParachainState, MessagesState), + /// Parachain finality + message delivery calls. + ParachainFinalityAndDelivery(ExpectedParachainState, MessagesState), + /// Standalone message delivery call. + Delivery(MessagesState), +} + +impl CallType { + /// Returns the pre-dispatch messages pallet state. + fn pre_dispatch_messages_state(&self) -> MessagesState { + match *self { + Self::AllFinalityAndDelivery(_, _, messages_state) => messages_state, + Self::ParachainFinalityAndDelivery(_, messages_state) => messages_state, + Self::Delivery(messages_state) => messages_state, + } + } +} + +/// Expected post-dispatch state of the relay chain pallet. +#[derive(Clone, Copy, PartialEq, RuntimeDebugNoBound)] +pub struct ExpectedRelayChainState { + /// Best known relay chain block number. + pub best_block_number: RelayBlockNumber, +} + +/// Expected post-dispatch state of the parachain pallet. +#[derive(Clone, Copy, PartialEq, RuntimeDebugNoBound)] +pub struct ExpectedParachainState { + /// At which relay block the parachain head has been updated? + pub at_relay_block_number: RelayBlockNumber, +} + +/// Pre-dispatch state of messages pallet. +/// +/// This struct is for pre-dispatch state of the pallet, not the expected post-dispatch state. +/// That's because message delivery transaction may deliver some of messages that it brings. +/// If this happens, we consider it "helpful" and refund its cost. If transaction fails to +/// deliver at least one message, it is considered wrong and is not refunded. +#[derive(Clone, Copy, PartialEq, RuntimeDebugNoBound)] +pub struct MessagesState { + /// Best delivered message nonce. + pub best_nonce: MessageNonce, +} + +// without this typedef rustfmt fails with internal err +type BalanceOf = + <::OnChargeTransaction as OnChargeTransaction>::Balance; +type CallOf = ::RuntimeCall; + +impl SignedExtension + for RefundRelayerForMessagesFromParachain +where + R: 'static + + Send + + Sync + + frame_system::Config + + UtilityConfig> + + GrandpaConfig + + ParachainsConfig + + MessagesConfig + + RelayersConfig, + GI: 'static + Send + Sync, + PI: 'static + Send + Sync, + MI: 'static + Send + Sync, + BE: 'static + + Send + + Sync + + Default + + SignedExtension>, + PID: 'static + Send + Sync + Get, + LID: 'static + Send + Sync + Get, + FEE: 'static + Send + Sync + TransactionFeeCalculation<::Reward>, + ::RuntimeCall: + Dispatchable, + CallOf: IsSubType, R>> + + IsSubType, R>> + + IsSubType, R>> + + IsSubType, R>>, + >::BridgedChain: + Chain, + >::SourceHeaderChain: SourceHeaderChain< + MessagesProof = FromBridgedChainMessagesProof>>, + >, +{ + const IDENTIFIER: &'static str = "RefundRelayerForMessagesFromParachain"; + type AccountId = R::AccountId; + type Call = CallOf; + type AdditionalSigned = (); + type Pre = Option>; + + fn additional_signed(&self) -> Result<(), TransactionValidityError> { + Ok(()) + } + + fn validate( + &self, + _who: &Self::AccountId, + _call: &Self::Call, + _info: &DispatchInfoOf, + _len: usize, + ) -> TransactionValidity { + Ok(ValidTransaction::default()) + } + + fn pre_dispatch( + self, + who: &Self::AccountId, + call: &Self::Call, + post_info: &DispatchInfoOf, + len: usize, + ) -> Result { + // reject batch transactions with obsolete headers + if let Some(UtilityCall::::batch_all { ref calls }) = call.is_sub_type() { + for nested_call in calls { + let reject_obsolete_transactions = BE::default(); + reject_obsolete_transactions.pre_dispatch(who, nested_call, post_info, len)?; + } + } + + // now try to check if tx matches one of types we support + let parse_call_type = || { + if let Some(UtilityCall::::batch_all { ref calls }) = call.is_sub_type() { + if calls.len() == 3 { + return Some(CallType::AllFinalityAndDelivery( + extract_expected_relay_chain_state::(&calls[0])?, + extract_expected_parachain_state::(&calls[1])?, + extract_messages_state::(&calls[2])?, + )) + } + if calls.len() == 2 { + return Some(CallType::ParachainFinalityAndDelivery( + extract_expected_parachain_state::(&calls[0])?, + extract_messages_state::(&calls[1])?, + )) + } + return None + } + + Some(CallType::Delivery(extract_messages_state::(call)?)) + }; + + Ok(parse_call_type().map(|call_type| PreDispatchData { relayer: who.clone(), call_type })) + } + + fn post_dispatch( + pre: Option, + info: &DispatchInfoOf, + post_info: &PostDispatchInfoOf, + len: usize, + result: &DispatchResult, + ) -> Result<(), TransactionValidityError> { + // we never refund anything if it is not bridge transaction or if it is a bridge + // transaction that we do not support here + let (relayer, call_type) = match pre { + Some(Some(pre)) => (pre.relayer, pre.call_type), + _ => return Ok(()), + }; + + // we never refund anything if transaction has failed + if result.is_err() { + return Ok(()) + } + + // check if relay chain state has been updated + if let CallType::AllFinalityAndDelivery(expected_relay_chain_state, _, _) = call_type { + let actual_relay_chain_state = relay_chain_state::(); + if actual_relay_chain_state != Some(expected_relay_chain_state) { + // we only refund relayer if all calls have updated chain state + return Ok(()) + } + + // there's a conflict between how bridge GRANDPA pallet works and the + // `AllFinalityAndDelivery` 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. + } + + // check if parachain state has been updated + match call_type { + CallType::AllFinalityAndDelivery(_, expected_parachain_state, _) | + CallType::ParachainFinalityAndDelivery(expected_parachain_state, _) => { + let actual_parachain_state = parachain_state::(); + if actual_parachain_state != Some(expected_parachain_state) { + // we only refund relayer if all calls have updated chain state + return Ok(()) + } + }, + _ => (), + } + + // check if messages have been delivered + let actual_messages_state = messages_state::(); + let pre_dispatch_messages_state = call_type.pre_dispatch_messages_state(); + if actual_messages_state == Some(pre_dispatch_messages_state) { + // we only refund relayer if all calls have updated chain state + 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 here. 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(); + + // compute the relayer reward + let reward = FEE::compute_fee(info, post_info, len, tip); + + // finally - register reward in relayers pallet + RelayersPallet::::register_relayer_reward(LID::get(), &relayer, reward); + + Ok(()) + } +} + +/// Extracts expected relay chain state from the call. +fn extract_expected_relay_chain_state(call: &CallOf) -> Option +where + R: GrandpaConfig, + GI: 'static, + >::BridgedChain: Chain, + CallOf: IsSubType, R>>, +{ + if let Some(GrandpaCall::::submit_finality_proof { ref finality_target, .. }) = + call.is_sub_type() + { + return Some(ExpectedRelayChainState { best_block_number: *finality_target.number() }) + } + None +} + +/// Extracts expected parachain state from the call. +fn extract_expected_parachain_state( + call: &CallOf, +) -> Option +where + R: GrandpaConfig + ParachainsConfig, + GI: 'static, + PI: 'static, + PID: Get, + >::BridgedChain: + Chain, + CallOf: IsSubType, R>>, +{ + if let Some(ParachainsCall::::submit_parachain_heads { + ref at_relay_block, + ref parachains, + .. + }) = call.is_sub_type() + { + if parachains.len() != 1 || parachains[0].0 != ParaId(PID::get()) { + return None + } + + return Some(ExpectedParachainState { at_relay_block_number: at_relay_block.0 }) + } + None +} + +/// Extracts messages state from the call. +fn extract_messages_state(call: &CallOf) -> Option +where + R: GrandpaConfig + MessagesConfig, + GI: 'static, + MI: 'static, + LID: Get, + CallOf: IsSubType, R>>, + >::SourceHeaderChain: SourceHeaderChain< + MessagesProof = FromBridgedChainMessagesProof>>, + >, +{ + if let Some(MessagesCall::::receive_messages_proof { ref proof, .. }) = + call.is_sub_type() + { + if LID::get() != proof.lane { + return None + } + + return Some(MessagesState { + best_nonce: MessagesPallet::::inbound_lane_data(proof.lane) + .last_delivered_nonce(), + }) + } + None +} + +/// Returns relay chain state that we are interested in. +fn relay_chain_state() -> Option +where + R: GrandpaConfig, + GI: 'static, + >::BridgedChain: Chain, +{ + GrandpaPallet::::best_finalized_number() + .map(|best_block_number| ExpectedRelayChainState { best_block_number }) +} + +/// Returns parachain state that we are interested in. +fn parachain_state() -> Option +where + R: ParachainsConfig, + PI: 'static, + PID: Get, +{ + ParachainsPallet::::best_parachain_info(ParaId(PID::get())).map(|para_info| { + ExpectedParachainState { + at_relay_block_number: para_info.best_head_hash.at_relay_block_number, + } + }) +} + +/// Returns messages state that we are interested in. +fn messages_state() -> Option +where + R: MessagesConfig, + MI: 'static, + LID: Get, +{ + Some(MessagesState { + best_nonce: MessagesPallet::::inbound_lane_data(LID::get()).last_delivered_nonce(), + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use bp_messages::InboundLaneData; + use bp_parachains::{BestParaHeadHash, ParaInfo}; + use bp_polkadot_core::parachains::ParaHeadsProof; + use bp_runtime::HeaderId; + use bp_test_utils::make_default_justification; + use frame_support::{assert_storage_noop, parameter_types, weights::Weight}; + use millau_runtime::{ + RialtoGrandpaInstance, Runtime, RuntimeCall, WithRialtoParachainMessagesInstance, + WithRialtoParachainsInstance, + }; + use sp_runtime::{transaction_validity::InvalidTransaction, DispatchError}; + + parameter_types! { + pub TestParachain: u32 = 1000; + pub TestLaneId: LaneId = [0, 0, 0, 0]; + } + + type TestExtension = RefundRelayerForMessagesFromParachain< + millau_runtime::Runtime, + RialtoGrandpaInstance, + WithRialtoParachainsInstance, + WithRialtoParachainMessagesInstance, + millau_runtime::BridgeRejectObsoleteHeadersAndMessages, + TestParachain, + TestLaneId, + millau_runtime::Runtime, + >; + + fn relayer_account() -> millau_runtime::AccountId { + [0u8; 32].into() + } + + fn initialize_environment( + best_relay_header_number: RelayBlockNumber, + parachain_head_at_relay_header_number: RelayBlockNumber, + best_delivered_message: MessageNonce, + ) { + let best_relay_header = HeaderId(best_relay_header_number, RelayBlockHash::default()); + pallet_bridge_grandpa::BestFinalized::::put( + best_relay_header, + ); + + let para_id = ParaId(TestParachain::get()); + let para_info = ParaInfo { + best_head_hash: BestParaHeadHash { + at_relay_block_number: parachain_head_at_relay_header_number, + head_hash: Default::default(), + }, + next_imported_hash_position: 0, + }; + pallet_bridge_parachains::ParasInfo::::insert( + para_id, para_info, + ); + + let lane_id = TestLaneId::get(); + let lane_data = + InboundLaneData { last_confirmed_nonce: best_delivered_message, ..Default::default() }; + pallet_bridge_messages::InboundLanes::::insert(lane_id, lane_data); + } + + fn submit_relay_header_call(relay_header_number: RelayBlockNumber) -> RuntimeCall { + let relay_header = bp_rialto::Header::new( + relay_header_number, + Default::default(), + Default::default(), + Default::default(), + Default::default(), + ); + let relay_justification = make_default_justification(&relay_header); + + RuntimeCall::BridgeRialtoGrandpa(GrandpaCall::submit_finality_proof { + finality_target: Box::new(relay_header), + justification: relay_justification, + }) + } + + fn submit_parachain_head_call( + parachain_head_at_relay_header_number: RelayBlockNumber, + ) -> RuntimeCall { + RuntimeCall::BridgeRialtoParachains(ParachainsCall::submit_parachain_heads { + at_relay_block: (parachain_head_at_relay_header_number, RelayBlockHash::default()), + parachains: vec![(ParaId(TestParachain::get()), [1u8; 32].into())], + parachain_heads_proof: ParaHeadsProof(vec![]), + }) + } + + fn message_delivery_call(best_message: MessageNonce) -> RuntimeCall { + RuntimeCall::BridgeRialtoParachainMessages(MessagesCall::receive_messages_proof { + relayer_id_at_bridged_chain: relayer_account(), + proof: millau_runtime::bridge_runtime_common::messages::target::FromBridgedChainMessagesProof { + bridged_header_hash: Default::default(), + storage_proof: vec![], + lane: TestLaneId::get(), + nonces_start: best_message, + nonces_end: best_message, + }, + messages_count: 1, + dispatch_weight: Weight::zero(), + }) + } + + fn parachain_finality_and_delivery_batch_call( + parachain_head_at_relay_header_number: RelayBlockNumber, + best_message: MessageNonce, + ) -> RuntimeCall { + RuntimeCall::Utility(UtilityCall::batch_all { + calls: vec![ + submit_parachain_head_call(parachain_head_at_relay_header_number), + message_delivery_call(best_message), + ], + }) + } + + fn all_finality_and_delivery_batch_call( + relay_header_number: RelayBlockNumber, + parachain_head_at_relay_header_number: RelayBlockNumber, + best_message: MessageNonce, + ) -> RuntimeCall { + RuntimeCall::Utility(UtilityCall::batch_all { + calls: vec![ + submit_relay_header_call(relay_header_number), + submit_parachain_head_call(parachain_head_at_relay_header_number), + message_delivery_call(best_message), + ], + }) + } + + fn all_finality_pre_dispatch_data() -> PreDispatchData { + PreDispatchData { + relayer: relayer_account(), + call_type: CallType::AllFinalityAndDelivery( + ExpectedRelayChainState { best_block_number: 200 }, + ExpectedParachainState { at_relay_block_number: 200 }, + MessagesState { best_nonce: 100 }, + ), + } + } + + fn parachain_finality_pre_dispatch_data() -> PreDispatchData { + PreDispatchData { + relayer: relayer_account(), + call_type: CallType::ParachainFinalityAndDelivery( + ExpectedParachainState { at_relay_block_number: 200 }, + MessagesState { best_nonce: 100 }, + ), + } + } + + fn delivery_pre_dispatch_data() -> PreDispatchData { + PreDispatchData { + relayer: relayer_account(), + call_type: CallType::Delivery(MessagesState { best_nonce: 100 }), + } + } + + fn run_test(test: impl FnOnce()) { + sp_io::TestExternalities::new(Default::default()).execute_with(|| test()) + } + + fn run_pre_dispatch( + call: RuntimeCall, + ) -> Result>, TransactionValidityError> { + let extension: TestExtension = RefundRelayerForMessagesFromParachain(PhantomData); + extension.pre_dispatch(&relayer_account(), &call, &DispatchInfo::default(), 0) + } + + fn dispatch_info() -> DispatchInfo { + DispatchInfo { + weight: frame_support::weights::constants::WEIGHT_PER_SECOND, + class: frame_support::dispatch::DispatchClass::Normal, + pays_fee: frame_support::dispatch::Pays::Yes, + } + } + + fn post_dispatch_info() -> PostDispatchInfo { + PostDispatchInfo { actual_weight: None, pays_fee: frame_support::dispatch::Pays::Yes } + } + + fn run_post_dispatch( + pre_dispatch_data: Option>, + dispatch_result: DispatchResult, + ) { + let post_dispatch_result = TestExtension::post_dispatch( + Some(pre_dispatch_data), + &dispatch_info(), + &post_dispatch_info(), + 1024, + &dispatch_result, + ); + assert_eq!(post_dispatch_result, Ok(())); + } + + fn expected_reward() -> millau_runtime::Balance { + pallet_transaction_payment::Pallet::::compute_actual_fee( + 1024, + &dispatch_info(), + &post_dispatch_info(), + Zero::zero(), + ) + } + + #[test] + fn pre_dispatch_rejects_batch_with_obsolete_relay_chain_header() { + run_test(|| { + initialize_environment(100, 100, 100); + + assert_eq!( + run_pre_dispatch(all_finality_and_delivery_batch_call(100, 200, 200)), + Err(TransactionValidityError::Invalid(InvalidTransaction::Stale)), + ); + }); + } + + #[test] + fn pre_dispatch_rejects_batch_with_obsolete_parachain_head() { + run_test(|| { + initialize_environment(100, 100, 100); + + assert_eq!( + run_pre_dispatch(all_finality_and_delivery_batch_call(101, 100, 200)), + Err(TransactionValidityError::Invalid(InvalidTransaction::Stale)), + ); + + assert_eq!( + run_pre_dispatch(parachain_finality_and_delivery_batch_call(100, 200)), + Err(TransactionValidityError::Invalid(InvalidTransaction::Stale)), + ); + }); + } + + #[test] + fn pre_dispatch_rejects_batch_with_obsolete_messages() { + run_test(|| { + initialize_environment(100, 100, 100); + + assert_eq!( + run_pre_dispatch(all_finality_and_delivery_batch_call(200, 200, 100)), + Err(TransactionValidityError::Invalid(InvalidTransaction::Stale)), + ); + + assert_eq!( + run_pre_dispatch(parachain_finality_and_delivery_batch_call(200, 100)), + Err(TransactionValidityError::Invalid(InvalidTransaction::Stale)), + ); + }); + } + + #[test] + fn pre_dispatch_parses_batch_with_relay_chain_and_parachain_headers() { + run_test(|| { + initialize_environment(100, 100, 100); + + assert_eq!( + run_pre_dispatch(all_finality_and_delivery_batch_call(200, 200, 200)), + Ok(Some(all_finality_pre_dispatch_data())), + ); + }); + } + + #[test] + fn pre_dispatch_parses_batch_with_parachain_header() { + run_test(|| { + initialize_environment(100, 100, 100); + + assert_eq!( + run_pre_dispatch(parachain_finality_and_delivery_batch_call(200, 200)), + Ok(Some(parachain_finality_pre_dispatch_data())), + ); + }); + } + + #[test] + fn pre_dispatch_fails_to_parse_batch_with_multiple_parachain_headers() { + run_test(|| { + initialize_environment(100, 100, 100); + + let call = RuntimeCall::Utility(UtilityCall::batch_all { + calls: vec![ + RuntimeCall::BridgeRialtoParachains(ParachainsCall::submit_parachain_heads { + at_relay_block: (100, RelayBlockHash::default()), + parachains: vec![ + (ParaId(TestParachain::get()), [1u8; 32].into()), + (ParaId(TestParachain::get() + 1), [1u8; 32].into()), + ], + parachain_heads_proof: ParaHeadsProof(vec![]), + }), + message_delivery_call(200), + ], + }); + + assert_eq!(run_pre_dispatch(call), Ok(None),); + }); + } + + #[test] + fn pre_dispatch_parses_message_delivery_transaction() { + run_test(|| { + initialize_environment(100, 100, 100); + + assert_eq!( + run_pre_dispatch(message_delivery_call(200)), + Ok(Some(delivery_pre_dispatch_data())), + ); + }); + } + + #[test] + fn post_dispatch_ignores_unknown_transaction() { + run_test(|| { + assert_storage_noop!(run_post_dispatch(None, Ok(()))); + }); + } + + #[test] + fn post_dispatch_ignores_failed_transaction() { + run_test(|| { + assert_storage_noop!(run_post_dispatch( + Some(all_finality_pre_dispatch_data()), + Err(DispatchError::BadOrigin) + )); + }); + } + + #[test] + fn post_dispatch_ignores_transaction_that_has_not_updated_relay_chain_state() { + run_test(|| { + initialize_environment(100, 200, 200); + + assert_storage_noop!(run_post_dispatch(Some(all_finality_pre_dispatch_data()), Ok(()))); + }); + } + + #[test] + fn post_dispatch_ignores_transaction_that_has_not_updated_parachain_state() { + run_test(|| { + initialize_environment(200, 100, 200); + + assert_storage_noop!(run_post_dispatch(Some(all_finality_pre_dispatch_data()), Ok(()))); + assert_storage_noop!(run_post_dispatch( + Some(parachain_finality_pre_dispatch_data()), + Ok(()) + )); + }); + } + + #[test] + fn post_dispatch_ignores_transaction_that_has_not_delivered_any_messages() { + run_test(|| { + initialize_environment(200, 200, 100); + + assert_storage_noop!(run_post_dispatch(Some(all_finality_pre_dispatch_data()), Ok(()))); + assert_storage_noop!(run_post_dispatch( + Some(parachain_finality_pre_dispatch_data()), + Ok(()) + )); + assert_storage_noop!(run_post_dispatch(Some(delivery_pre_dispatch_data()), Ok(()))); + }); + } + + #[test] + fn post_dispatch_refunds_relayer_in_all_finality_batch() { + run_test(|| { + initialize_environment(200, 200, 200); + + run_post_dispatch(Some(all_finality_pre_dispatch_data()), Ok(())); + assert_eq!( + RelayersPallet::::relayer_reward(relayer_account(), TestLaneId::get()), + Some(expected_reward()), + ); + }); + } + + #[test] + fn post_dispatch_refunds_relayer_in_parachain_finality_batch() { + run_test(|| { + initialize_environment(200, 200, 200); + + run_post_dispatch(Some(parachain_finality_pre_dispatch_data()), Ok(())); + assert_eq!( + RelayersPallet::::relayer_reward(relayer_account(), TestLaneId::get()), + Some(expected_reward()), + ); + }); + } + + #[test] + fn post_dispatch_refunds_relayer_in_message_delivery_transaction() { + run_test(|| { + initialize_environment(200, 200, 200); + + run_post_dispatch(Some(delivery_pre_dispatch_data()), Ok(())); + assert_eq!( + RelayersPallet::::relayer_reward(relayer_account(), TestLaneId::get()), + Some(expected_reward()), + ); + }); + } +} diff --git a/bridges/modules/grandpa/src/lib.rs b/bridges/modules/grandpa/src/lib.rs index 54bf5a8810c4f..f1e4a8da1ecf2 100644 --- a/bridges/modules/grandpa/src/lib.rs +++ b/bridges/modules/grandpa/src/lib.rs @@ -540,6 +540,13 @@ pub mod pallet { } } +impl, I: 'static> Pallet { + /// Get the best finalized block number. + pub fn best_finalized_number() -> Option> { + BestFinalized::::get().map(|id| id.number()) + } +} + /// Bridge GRANDPA pallet as header chain. pub type GrandpaChainHeaders = Pallet; diff --git a/bridges/modules/messages/src/lib.rs b/bridges/modules/messages/src/lib.rs index 33e250574ba32..8e394c6bcea33 100644 --- a/bridges/modules/messages/src/lib.rs +++ b/bridges/modules/messages/src/lib.rs @@ -168,10 +168,10 @@ pub mod pallet { } /// Shortcut to messages proof type for Config. - type MessagesProofOf = + pub type MessagesProofOf = <>::SourceHeaderChain as SourceHeaderChain>::MessagesProof; /// Shortcut to messages delivery proof type for Config. - type MessagesDeliveryProofOf = + pub type MessagesDeliveryProofOf = <>::TargetHeaderChain as TargetHeaderChain< >::OutboundPayload, ::AccountId, @@ -631,6 +631,11 @@ pub mod pallet { dispatch_weight: T::MessageDispatch::dispatch_weight(&mut dispatch_message), } } + + /// Return inbound lane data. + pub fn inbound_lane_data(lane: LaneId) -> InboundLaneData { + InboundLanes::::get(lane).0 + } } } diff --git a/bridges/modules/parachains/src/lib.rs b/bridges/modules/parachains/src/lib.rs index 9ca2ac0f48277..1b1b4de52504c 100644 --- a/bridges/modules/parachains/src/lib.rs +++ b/bridges/modules/parachains/src/lib.rs @@ -401,6 +401,11 @@ pub mod pallet { } impl, I: 'static> Pallet { + /// Get stored parachain info. + pub fn best_parachain_info(parachain: ParaId) -> Option { + ParasInfo::::get(parachain) + } + /// Get best finalized header of the given parachain. pub fn best_parachain_head(parachain: ParaId) -> Option { let best_para_head_hash = ParasInfo::::get(parachain)?.best_head_hash.head_hash; diff --git a/bridges/modules/relayers/Cargo.toml b/bridges/modules/relayers/Cargo.toml index 27e7424a97caf..9c07123836381 100644 --- a/bridges/modules/relayers/Cargo.toml +++ b/bridges/modules/relayers/Cargo.toml @@ -22,6 +22,7 @@ frame-benchmarking = { git = "https://github.com/paritytech/substrate", branch = 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 } sp-arithmetic = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false } +sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false } sp-std = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false } [dev-dependencies] @@ -42,6 +43,7 @@ std = [ "log/std", "scale-info/std", "sp-arithmetic/std", + "sp-runtime/std", "sp-std/std", ] runtime-benchmarks = [ diff --git a/bridges/modules/relayers/src/lib.rs b/bridges/modules/relayers/src/lib.rs index 7467653710953..11bbeb10845f8 100644 --- a/bridges/modules/relayers/src/lib.rs +++ b/bridges/modules/relayers/src/lib.rs @@ -109,8 +109,9 @@ pub mod pallet { log::trace!( target: crate::LOG_TARGET, - "Relayer {:?} can now claim reward: {:?}", + "Relayer {:?} can now claim reward for serving lane {:?}: {:?}", relayer, + lane_id, new_reward, ); }); @@ -141,6 +142,7 @@ pub mod pallet { /// Map of the relayer => accumulated reward. #[pallet::storage] + #[pallet::getter(fn relayer_reward)] pub type RelayerRewards = StorageDoubleMap< _, Blake2_128Concat,