diff --git a/bridges/bin/millau/runtime/src/lib.rs b/bridges/bin/millau/runtime/src/lib.rs index 2119fa04610..8f82e645f9e 100644 --- a/bridges/bin/millau/runtime/src/lib.rs +++ b/bridges/bin/millau/runtime/src/lib.rs @@ -608,7 +608,6 @@ pub type BridgeRefundRialtoParachainRelayers = RialtoGrandpaInstance, WithRialtoParachainsInstance, WithRialtoParachainMessagesInstance, - BridgeRejectObsoleteHeadersAndMessages, RialtoParachainId, RialtoParachainMessagesLane, Runtime, diff --git a/bridges/bin/runtime-common/src/lib.rs b/bridges/bin/runtime-common/src/lib.rs index 9f31450bd28..32ea500db3e 100644 --- a/bridges/bin/runtime-common/src/lib.rs +++ b/bridges/bin/runtime-common/src/lib.rs @@ -18,14 +18,16 @@ #![cfg_attr(not(feature = "std"), no_std)] -use bp_runtime::FilterCall; +use crate::messages_call_ext::MessagesCallSubType; +use pallet_bridge_grandpa::CallSubType as GrandpaCallSubType; +use pallet_bridge_parachains::CallSubType as ParachainsCallSubtype; use sp_runtime::transaction_validity::TransactionValidity; use xcm::v3::NetworkId; pub mod messages; pub mod messages_api; pub mod messages_benchmarking; -pub mod messages_extension; +pub mod messages_call_ext; pub mod parachains_benchmarking; pub mod refund_relayer_extension; @@ -44,21 +46,39 @@ pub trait BridgeRuntimeFilterCall { fn validate(call: &Call) -> TransactionValidity; } -impl BridgeRuntimeFilterCall for pallet_bridge_grandpa::Pallet +impl BridgeRuntimeFilterCall for pallet_bridge_grandpa::Pallet where - pallet_bridge_grandpa::Pallet: FilterCall, + T: pallet_bridge_grandpa::Config, + T::RuntimeCall: GrandpaCallSubType, { - fn validate(call: &Call) -> TransactionValidity { - as FilterCall>::validate(call) + fn validate(call: &T::RuntimeCall) -> TransactionValidity { + GrandpaCallSubType::::check_obsolete_submit_finality_proof(call) } } -impl BridgeRuntimeFilterCall for pallet_bridge_parachains::Pallet +impl BridgeRuntimeFilterCall + for pallet_bridge_parachains::Pallet where - pallet_bridge_parachains::Pallet: FilterCall, + T: pallet_bridge_parachains::Config, + T::RuntimeCall: ParachainsCallSubtype, { - fn validate(call: &Call) -> TransactionValidity { - as FilterCall>::validate(call) + fn validate(call: &T::RuntimeCall) -> TransactionValidity { + ParachainsCallSubtype::::check_obsolete_submit_parachain_heads(call) + } +} + +impl, I: 'static> BridgeRuntimeFilterCall + for pallet_bridge_messages::Pallet +where + T::RuntimeCall: MessagesCallSubType, +{ + /// Validate messages in order to avoid "mining" messages delivery and delivery confirmation + /// transactions, that are delivering outdated messages/confirmations. Without this validation, + /// even honest relayers may lose their funds if there are multiple relays running and + /// submitting the same messages/confirmations. + fn validate(call: &T::RuntimeCall) -> TransactionValidity { + call.check_obsolete_receive_messages_proof()?; + call.check_obsolete_receive_messages_delivery_proof() } } diff --git a/bridges/bin/runtime-common/src/messages_extension.rs b/bridges/bin/runtime-common/src/messages_call_ext.rs similarity index 57% rename from bridges/bin/runtime-common/src/messages_extension.rs rename to bridges/bin/runtime-common/src/messages_call_ext.rs index 1e207dc605b..20e604142d9 100644 --- a/bridges/bin/runtime-common/src/messages_extension.rs +++ b/bridges/bin/runtime-common/src/messages_call_ext.rs @@ -14,20 +14,56 @@ // You should have received a copy of the GNU General Public License // along with Parity Bridges Common. If not, see . -use crate::{ - messages::{ - source::FromBridgedChainMessagesDeliveryProof, target::FromBridgedChainMessagesProof, - }, - BridgeRuntimeFilterCall, +use crate::messages::{ + source::FromBridgedChainMessagesDeliveryProof, target::FromBridgedChainMessagesProof, }; +use bp_messages::{LaneId, MessageNonce}; use frame_support::{dispatch::CallableCallFor, traits::IsSubType}; use pallet_bridge_messages::{Config, Pallet}; -use sp_runtime::transaction_validity::TransactionValidity; +use sp_runtime::{transaction_validity::TransactionValidity, RuntimeDebug}; + +/// Info about a `ReceiveMessagesProof` call which tries to update a single lane. +#[derive(Copy, Clone, PartialEq, RuntimeDebug)] +pub struct ReceiveMessagesProofInfo { + pub lane_id: LaneId, + pub best_proof_nonce: MessageNonce, + pub best_stored_nonce: MessageNonce, +} + +/// Helper struct that provides methods for working with the `ReceiveMessagesProof` call. +pub struct ReceiveMessagesProofHelper, I: 'static> { + pub _phantom_data: sp_std::marker::PhantomData<(T, I)>, +} + +impl, I: 'static> ReceiveMessagesProofHelper { + /// Check if the `ReceiveMessagesProof` call delivered at least some of the messages that + /// it contained. + pub fn was_partially_successful(info: &ReceiveMessagesProofInfo) -> bool { + let inbound_lane_data = pallet_bridge_messages::InboundLanes::::get(info.lane_id); + inbound_lane_data.last_delivered_nonce() > info.best_stored_nonce + } +} + +/// Trait representing a call that is a sub type of `pallet_bridge_messages::Call`. +pub trait MessagesCallSubType, I: 'static>: + IsSubType, T>> +{ + /// Create a new instance of `ReceiveMessagesProofInfo` from a `ReceiveMessagesProof` call. + fn receive_messages_proof_info(&self) -> Option; + + /// Create a new instance of `ReceiveMessagesProofInfo` from a `ReceiveMessagesProof` call, + /// if the call is for the provided lane. + fn receive_messages_proof_info_for(&self, lane_id: LaneId) -> Option; + + /// Check that a `ReceiveMessagesProof` call is trying to deliver at least some messages that + /// are better than the ones we know of. + fn check_obsolete_receive_messages_proof(&self) -> TransactionValidity; + + /// Check that a `ReceiveMessagesDeliveryProof` call is trying to deliver at least some message + /// confirmations that are better than the ones we know of. + fn check_obsolete_receive_messages_delivery_proof(&self) -> TransactionValidity; +} -/// Validate messages in order to avoid "mining" messages delivery and delivery confirmation -/// transactions, that are delivering outdated messages/confirmations. Without this validation, -/// even honest relayers may lose their funds if there are multiple relays running and submitting -/// the same messages/confirmations. impl< BridgedHeaderHash, SourceHeaderChain: bp_messages::target_chain::SourceHeaderChain< @@ -42,52 +78,69 @@ impl< T: frame_system::Config + Config, I: 'static, - > BridgeRuntimeFilterCall for Pallet + > MessagesCallSubType for T::RuntimeCall { - fn validate(call: &Call) -> TransactionValidity { - match call.is_sub_type() { - Some(pallet_bridge_messages::Call::::receive_messages_proof { - ref proof, - .. - }) => { - let inbound_lane_data = - pallet_bridge_messages::InboundLanes::::get(proof.lane); - if proof.nonces_end <= inbound_lane_data.last_delivered_nonce() { - log::trace!( - target: pallet_bridge_messages::LOG_TARGET, - "Rejecting obsolete messages delivery transaction: \ + fn receive_messages_proof_info(&self) -> Option { + if let Some(pallet_bridge_messages::Call::::receive_messages_proof { + ref proof, + .. + }) = self.is_sub_type() + { + let inbound_lane_data = pallet_bridge_messages::InboundLanes::::get(proof.lane); + + return Some(ReceiveMessagesProofInfo { + lane_id: proof.lane, + best_proof_nonce: proof.nonces_end, + best_stored_nonce: inbound_lane_data.last_delivered_nonce(), + }) + } + + None + } + + fn receive_messages_proof_info_for(&self, lane_id: LaneId) -> Option { + self.receive_messages_proof_info().filter(|info| info.lane_id == lane_id) + } + + fn check_obsolete_receive_messages_proof(&self) -> TransactionValidity { + if let Some(proof_info) = self.receive_messages_proof_info() { + if proof_info.best_proof_nonce <= proof_info.best_stored_nonce { + log::trace!( + target: pallet_bridge_messages::LOG_TARGET, + "Rejecting obsolete messages delivery transaction: \ lane {:?}, bundled {:?}, best {:?}", - proof.lane, - proof.nonces_end, - inbound_lane_data.last_delivered_nonce(), - ); + proof_info.lane_id, + proof_info.best_proof_nonce, + proof_info.best_stored_nonce, + ); - return sp_runtime::transaction_validity::InvalidTransaction::Stale.into() - } - }, - Some(pallet_bridge_messages::Call::::receive_messages_delivery_proof { - ref proof, - ref relayers_state, - .. - }) => { - let latest_delivered_nonce = relayers_state.last_delivered_nonce; - - let outbound_lane_data = - pallet_bridge_messages::OutboundLanes::::get(proof.lane); - if latest_delivered_nonce <= outbound_lane_data.latest_received_nonce { - log::trace!( - target: pallet_bridge_messages::LOG_TARGET, - "Rejecting obsolete messages confirmation transaction: \ + return sp_runtime::transaction_validity::InvalidTransaction::Stale.into() + } + } + + Ok(sp_runtime::transaction_validity::ValidTransaction::default()) + } + + fn check_obsolete_receive_messages_delivery_proof(&self) -> TransactionValidity { + if let Some(pallet_bridge_messages::Call::::receive_messages_delivery_proof { + ref proof, + ref relayers_state, + .. + }) = self.is_sub_type() + { + let outbound_lane_data = pallet_bridge_messages::OutboundLanes::::get(proof.lane); + if relayers_state.last_delivered_nonce <= outbound_lane_data.latest_received_nonce { + log::trace!( + target: pallet_bridge_messages::LOG_TARGET, + "Rejecting obsolete messages confirmation transaction: \ lane {:?}, bundled {:?}, best {:?}", - proof.lane, - latest_delivered_nonce, - outbound_lane_data.latest_received_nonce, - ); + proof.lane, + relayers_state.last_delivered_nonce, + outbound_lane_data.latest_received_nonce, + ); - return sp_runtime::transaction_validity::InvalidTransaction::Stale.into() - } - }, - _ => {}, + return sp_runtime::transaction_validity::InvalidTransaction::Stale.into() + } } Ok(sp_runtime::transaction_validity::ValidTransaction::default()) diff --git a/bridges/bin/runtime-common/src/refund_relayer_extension.rs b/bridges/bin/runtime-common/src/refund_relayer_extension.rs index 51a7db3b53b..ac65617483d 100644 --- a/bridges/bin/runtime-common/src/refund_relayer_extension.rs +++ b/bridges/bin/runtime-common/src/refund_relayer_extension.rs @@ -19,37 +19,32 @@ //! with calls that are: delivering new messsage and all necessary underlying headers //! (parachain or relay chain). -use crate::messages::target::FromBridgedChainMessagesProof; - -use bp_messages::{target_chain::SourceHeaderChain, LaneId, MessageNonce}; -use bp_polkadot_core::parachains::ParaId; -use bp_runtime::{Chain, HashOf}; +use crate::messages_call_ext::{ + MessagesCallSubType, ReceiveMessagesProofHelper, ReceiveMessagesProofInfo, +}; +use bp_messages::LaneId; use codec::{Decode, Encode}; use frame_support::{ dispatch::{CallableCallFor, DispatchInfo, Dispatchable, PostDispatchInfo}, traits::IsSubType, CloneNoBound, DefaultNoBound, 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_grandpa::{CallSubType as GrandpaCallSubType, SubmitFinalityProofHelper}; +use pallet_bridge_messages::Config as MessagesConfig; use pallet_bridge_parachains::{ - Call as ParachainsCall, Config as ParachainsConfig, Pallet as ParachainsPallet, RelayBlockHash, - RelayBlockHasher, RelayBlockNumber, + BoundedBridgeGrandpaConfig, CallSubType as ParachainsCallSubType, Config as ParachainsConfig, + RelayBlockNumber, SubmitParachainHeadsHelper, SubmitParachainHeadsInfo, }; 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}, + traits::{DispatchInfoOf, Get, PostDispatchInfoOf, SignedExtension, Zero}, transaction_validity::{TransactionValidity, TransactionValidityError, ValidTransaction}, DispatchResult, FixedPointOperand, }; -use sp_std::marker::PhantomData; +use sp_std::{marker::PhantomData, vec, vec::Vec}; // TODO (https://github.com/paritytech/parity-bridges-common/issues/1667): // support multiple bridges in this extension @@ -81,6 +76,7 @@ where 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`) @@ -99,18 +95,40 @@ where 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)>, +#[scale_info(skip_type_params(RT, GI, PI, MI, PID, LID, FEE))] +pub struct RefundRelayerForMessagesFromParachain( + PhantomData<(RT, GI, PI, MI, PID, LID, FEE)>, ); +impl + RefundRelayerForMessagesFromParachain +where + R: UtilityConfig>, + CallOf: IsSubType, R>>, +{ + fn expand_call<'a>(&self, call: &'a CallOf) -> Option>> { + let calls = match call.is_sub_type() { + Some(UtilityCall::::batch_all { ref calls }) => { + if calls.len() > 3 { + return None + } + + calls.iter().collect() + }, + Some(_) => return None, + None => vec![call], + }; + + Some(calls) + } +} + /// 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, + relayer: AccountId, /// Type of the call. pub call_type: CallType, } @@ -119,89 +137,51 @@ pub struct PreDispatchData { #[derive(Clone, Copy, PartialEq, RuntimeDebugNoBound)] pub enum CallType { /// Relay chain finality + parachain finality + message delivery calls. - AllFinalityAndDelivery(ExpectedRelayChainState, ExpectedParachainState, MessagesState), + AllFinalityAndDelivery(RelayBlockNumber, SubmitParachainHeadsInfo, ReceiveMessagesProofInfo), /// Parachain finality + message delivery calls. - ParachainFinalityAndDelivery(ExpectedParachainState, MessagesState), + ParachainFinalityAndDelivery(SubmitParachainHeadsInfo, ReceiveMessagesProofInfo), /// Standalone message delivery call. - Delivery(MessagesState), + Delivery(ReceiveMessagesProofInfo), } impl CallType { /// Returns the pre-dispatch messages pallet state. - fn pre_dispatch_messages_state(&self) -> MessagesState { + fn receive_messages_proof_info(&self) -> ReceiveMessagesProofInfo { match *self { - Self::AllFinalityAndDelivery(_, _, messages_state) => messages_state, - Self::ParachainFinalityAndDelivery(_, messages_state) => messages_state, - Self::Delivery(messages_state) => messages_state, + Self::AllFinalityAndDelivery(_, _, info) => info, + Self::ParachainFinalityAndDelivery(_, info) => info, + Self::Delivery(info) => info, } } } -/// 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 +impl SignedExtension + for RefundRelayerForMessagesFromParachain where R: 'static + Send + Sync - + frame_system::Config + UtilityConfig> - + GrandpaConfig + + BoundedBridgeGrandpaConfig + 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>>, - >, + CallOf: Dispatchable + + IsSubType, R>> + + GrandpaCallSubType + + ParachainsCallSubType + + MessagesCallSubType, { const IDENTIFIER: &'static str = "RefundRelayerForMessagesFromParachain"; type AccountId = R::AccountId; @@ -215,17 +195,20 @@ where fn validate( &self, - who: &Self::AccountId, + _who: &Self::AccountId, call: &Self::Call, - info: &DispatchInfoOf, - len: usize, + _info: &DispatchInfoOf, + _len: usize, ) -> TransactionValidity { - // 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, info, len)?; - } + let calls = match self.expand_call(call) { + Some(calls) => calls, + None => return Ok(ValidTransaction::default()), + }; + + for call in calls { + call.check_obsolete_submit_finality_proof()?; + call.check_obsolete_submit_parachain_heads()?; + call.check_obsolete_receive_messages_proof()?; } Ok(ValidTransaction::default()) @@ -241,40 +224,37 @@ where // reject batch transactions with obsolete headers self.validate(who, call, info, len).map(drop)?; - // now try to check if tx matches one of types we support + // Try to check if the 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 + let mut calls = self.expand_call(call)?.into_iter(); + match calls.len() { + 3 => Some(CallType::AllFinalityAndDelivery( + calls.next()?.submit_finality_proof_info()?, + calls.next()?.submit_parachain_heads_info_for(PID::get())?, + calls.next()?.receive_messages_proof_info_for(LID::get())?, + )), + 2 => Some(CallType::ParachainFinalityAndDelivery( + calls.next()?.submit_parachain_heads_info_for(PID::get())?, + calls.next()?.receive_messages_proof_info_for(LID::get())?, + )), + 1 => Some(CallType::Delivery( + calls.next()?.receive_messages_proof_info_for(LID::get())?, + )), + _ => None, } - - Some(CallType::Delivery(extract_messages_state::(call)?)) }; - Ok(parse_call_type() - .map(|call_type| { - log::trace!( - target: "runtime::bridge", - "RefundRelayerForMessagesFromParachain from parachain {} via {:?} parsed bridge transaction in pre-dispatch: {:?}", - PID::get(), - LID::get(), - call_type, - ); - PreDispatchData { relayer: who.clone(), call_type } - }) - ) + Ok(parse_call_type().map(|call_type| { + log::trace!( + target: "runtime::bridge", + "RefundRelayerForMessagesFromParachain from parachain {} via {:?} \ + parsed bridge transaction in pre-dispatch: {:?}", + PID::get(), + LID::get(), + call_type, + ); + PreDispatchData { relayer: who.clone(), call_type } + })) } fn post_dispatch( @@ -284,40 +264,37 @@ where 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 + // 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_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) { + if let CallType::AllFinalityAndDelivery(relay_block_number, _, _) = call_type { + if !SubmitFinalityProofHelper::::was_successful(relay_block_number) { // 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 + // 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. } // 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) { + CallType::AllFinalityAndDelivery(_, parachain_heads_info, _) | + CallType::ParachainFinalityAndDelivery(parachain_heads_info, _) => { + if !SubmitParachainHeadsHelper::::was_successful(¶chain_heads_info) { // we only refund relayer if all calls have updated chain state return Ok(()) } @@ -325,11 +302,10 @@ where _ => (), } - // 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 + // 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 messages_proof_info = call_type.receive_messages_proof_info(); + if !ReceiveMessagesProofHelper::::was_partially_successful(&messages_proof_info) { return Ok(()) } @@ -359,125 +335,22 @@ where } } -/// 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 crate::{messages::target::FromBridgedChainMessagesProof, mock::*}; - use bp_messages::InboundLaneData; + use bp_messages::{InboundLaneData, MessageNonce}; use bp_parachains::{BestParaHeadHash, ParaInfo}; - use bp_polkadot_core::parachains::ParaHeadsProof; + use bp_polkadot_core::parachains::{ParaHash, ParaHeadsProof, ParaId}; use bp_runtime::HeaderId; use bp_test_utils::make_default_justification; use frame_support::{assert_storage_noop, parameter_types, weights::Weight}; - use sp_runtime::{transaction_validity::InvalidTransaction, DispatchError}; + use pallet_bridge_grandpa::Call as GrandpaCall; + use pallet_bridge_messages::Call as MessagesCall; + use pallet_bridge_parachains::{Call as ParachainsCall, RelayBlockHash}; + use sp_runtime::{ + traits::Header as HeaderT, transaction_validity::InvalidTransaction, DispatchError, + }; parameter_types! { pub TestParachain: u32 = 1000; @@ -489,7 +362,6 @@ mod tests { (), (), (), - BridgeRejectObsoleteHeadersAndMessages, TestParachain, TestLaneId, TestRuntime, @@ -506,6 +378,7 @@ mod tests { fn initialize_environment( best_relay_header_number: RelayBlockNumber, parachain_head_at_relay_header_number: RelayBlockNumber, + parachain_head_hash: ParaHash, best_delivered_message: MessageNonce, ) { let best_relay_header = HeaderId(best_relay_header_number, RelayBlockHash::default()); @@ -515,7 +388,7 @@ mod tests { let para_info = ParaInfo { best_head_hash: BestParaHeadHash { at_relay_block_number: parachain_head_at_relay_header_number, - head_hash: Default::default(), + head_hash: parachain_head_hash, }, next_imported_hash_position: 0, }; @@ -598,9 +471,17 @@ mod tests { PreDispatchData { relayer: relayer_account_at_this_chain(), call_type: CallType::AllFinalityAndDelivery( - ExpectedRelayChainState { best_block_number: 200 }, - ExpectedParachainState { at_relay_block_number: 200 }, - MessagesState { best_nonce: 100 }, + 200, + SubmitParachainHeadsInfo { + at_relay_block_number: 200, + para_id: ParaId(TestParachain::get()), + para_head_hash: [1u8; 32].into(), + }, + ReceiveMessagesProofInfo { + lane_id: TEST_LANE_ID, + best_proof_nonce: 200, + best_stored_nonce: 100, + }, ), } } @@ -609,8 +490,16 @@ mod tests { PreDispatchData { relayer: relayer_account_at_this_chain(), call_type: CallType::ParachainFinalityAndDelivery( - ExpectedParachainState { at_relay_block_number: 200 }, - MessagesState { best_nonce: 100 }, + SubmitParachainHeadsInfo { + at_relay_block_number: 200, + para_id: ParaId(TestParachain::get()), + para_head_hash: [1u8; 32].into(), + }, + ReceiveMessagesProofInfo { + lane_id: TEST_LANE_ID, + best_proof_nonce: 200, + best_stored_nonce: 100, + }, ), } } @@ -618,7 +507,11 @@ mod tests { fn delivery_pre_dispatch_data() -> PreDispatchData { PreDispatchData { relayer: relayer_account_at_this_chain(), - call_type: CallType::Delivery(MessagesState { best_nonce: 100 }), + call_type: CallType::Delivery(ReceiveMessagesProofInfo { + lane_id: TEST_LANE_ID, + best_proof_nonce: 200, + best_stored_nonce: 100, + }), } } @@ -678,7 +571,7 @@ mod tests { #[test] fn validate_allows_non_obsolete_transactions() { run_test(|| { - initialize_environment(100, 100, 100); + initialize_environment(100, 100, Default::default(), 100); assert_eq!(run_validate(message_delivery_call(200)), Ok(ValidTransaction::default()),); @@ -697,7 +590,7 @@ mod tests { #[test] fn ext_rejects_batch_with_obsolete_relay_chain_header() { run_test(|| { - initialize_environment(100, 100, 100); + initialize_environment(100, 100, Default::default(), 100); assert_eq!( run_pre_dispatch(all_finality_and_delivery_batch_call(100, 200, 200)), @@ -714,7 +607,7 @@ mod tests { #[test] fn ext_rejects_batch_with_obsolete_parachain_head() { run_test(|| { - initialize_environment(100, 100, 100); + initialize_environment(100, 100, Default::default(), 100); assert_eq!( run_pre_dispatch(all_finality_and_delivery_batch_call(101, 100, 200)), @@ -741,7 +634,7 @@ mod tests { #[test] fn ext_rejects_batch_with_obsolete_messages() { run_test(|| { - initialize_environment(100, 100, 100); + initialize_environment(100, 100, Default::default(), 100); assert_eq!( run_pre_dispatch(all_finality_and_delivery_batch_call(200, 200, 100)), @@ -768,7 +661,7 @@ mod tests { #[test] fn pre_dispatch_parses_batch_with_relay_chain_and_parachain_headers() { run_test(|| { - initialize_environment(100, 100, 100); + initialize_environment(100, 100, Default::default(), 100); assert_eq!( run_pre_dispatch(all_finality_and_delivery_batch_call(200, 200, 200)), @@ -780,7 +673,7 @@ mod tests { #[test] fn pre_dispatch_parses_batch_with_parachain_header() { run_test(|| { - initialize_environment(100, 100, 100); + initialize_environment(100, 100, Default::default(), 100); assert_eq!( run_pre_dispatch(parachain_finality_and_delivery_batch_call(200, 200)), @@ -792,7 +685,7 @@ mod tests { #[test] fn pre_dispatch_fails_to_parse_batch_with_multiple_parachain_headers() { run_test(|| { - initialize_environment(100, 100, 100); + initialize_environment(100, 100, Default::default(), 100); let call = RuntimeCall::Utility(UtilityCall::batch_all { calls: vec![ @@ -815,7 +708,7 @@ mod tests { #[test] fn pre_dispatch_parses_message_delivery_transaction() { run_test(|| { - initialize_environment(100, 100, 100); + initialize_environment(100, 100, Default::default(), 100); assert_eq!( run_pre_dispatch(message_delivery_call(200)), @@ -844,7 +737,7 @@ mod tests { #[test] fn post_dispatch_ignores_transaction_that_has_not_updated_relay_chain_state() { run_test(|| { - initialize_environment(100, 200, 200); + initialize_environment(100, 200, Default::default(), 200); assert_storage_noop!(run_post_dispatch(Some(all_finality_pre_dispatch_data()), Ok(()))); }); @@ -853,7 +746,7 @@ mod tests { #[test] fn post_dispatch_ignores_transaction_that_has_not_updated_parachain_state() { run_test(|| { - initialize_environment(200, 100, 200); + initialize_environment(200, 100, Default::default(), 200); assert_storage_noop!(run_post_dispatch(Some(all_finality_pre_dispatch_data()), Ok(()))); assert_storage_noop!(run_post_dispatch( @@ -866,7 +759,7 @@ mod tests { #[test] fn post_dispatch_ignores_transaction_that_has_not_delivered_any_messages() { run_test(|| { - initialize_environment(200, 200, 100); + initialize_environment(200, 200, Default::default(), 100); assert_storage_noop!(run_post_dispatch(Some(all_finality_pre_dispatch_data()), Ok(()))); assert_storage_noop!(run_post_dispatch( @@ -880,7 +773,7 @@ mod tests { #[test] fn post_dispatch_refunds_relayer_in_all_finality_batch() { run_test(|| { - initialize_environment(200, 200, 200); + initialize_environment(200, 200, [1u8; 32].into(), 200); run_post_dispatch(Some(all_finality_pre_dispatch_data()), Ok(())); assert_eq!( @@ -896,7 +789,7 @@ mod tests { #[test] fn post_dispatch_refunds_relayer_in_parachain_finality_batch() { run_test(|| { - initialize_environment(200, 200, 200); + initialize_environment(200, 200, [1u8; 32].into(), 200); run_post_dispatch(Some(parachain_finality_pre_dispatch_data()), Ok(())); assert_eq!( @@ -912,7 +805,7 @@ mod tests { #[test] fn post_dispatch_refunds_relayer_in_message_delivery_transaction() { run_test(|| { - initialize_environment(200, 200, 200); + initialize_environment(200, 200, Default::default(), 200); run_post_dispatch(Some(delivery_pre_dispatch_data()), Ok(())); assert_eq!( diff --git a/bridges/modules/grandpa/README.md b/bridges/modules/grandpa/README.md index eaa62e8b206..27b4d2389c4 100644 --- a/bridges/modules/grandpa/README.md +++ b/bridges/modules/grandpa/README.md @@ -87,7 +87,7 @@ It'd be better for anyone (for chain and for submitters) to reject all transacti already known headers to the pallet. This way, we leave block space to other useful transactions and we don't charge concurrent submitters for their honest actions. -To deal with that, we have a [signed extension](./src/extension.rs) that may be added to the runtime. +To deal with that, we have a [signed extension](./src/call_ext) that may be added to the runtime. It does exactly what is required - rejects all transactions with already known headers. The submitter pays nothing for such transactions - they're simply removed from the transaction pool, when the block is built. diff --git a/bridges/modules/grandpa/src/call_ext.rs b/bridges/modules/grandpa/src/call_ext.rs new file mode 100644 index 00000000000..42c276f5f6c --- /dev/null +++ b/bridges/modules/grandpa/src/call_ext.rs @@ -0,0 +1,163 @@ +// 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 . + +use crate::{Config, Error, Pallet}; +use bp_runtime::BlockNumberOf; +use frame_support::{dispatch::CallableCallFor, traits::IsSubType}; +use sp_runtime::{ + traits::Header, + transaction_validity::{InvalidTransaction, TransactionValidity, ValidTransaction}, +}; + +/// Helper struct that provides methods for working with the `SubmitFinalityProof` call. +pub struct SubmitFinalityProofHelper, I: 'static> { + pub _phantom_data: sp_std::marker::PhantomData<(T, I)>, +} + +impl, I: 'static> SubmitFinalityProofHelper { + /// Check that the GRANDPA head provided by the `SubmitFinalityProof` is better than the best + /// one we know. + pub fn check_obsolete( + finality_target: BlockNumberOf, + ) -> Result<(), Error> { + let best_finalized = crate::BestFinalized::::get().ok_or_else(|| { + log::trace!( + target: crate::LOG_TARGET, + "Cannot finalize header {:?} because pallet is not yet initialized", + finality_target, + ); + >::NotInitialized + })?; + + if best_finalized.number() >= finality_target { + log::trace!( + target: crate::LOG_TARGET, + "Cannot finalize obsolete header: bundled {:?}, best {:?}", + finality_target, + best_finalized, + ); + + return Err(Error::::OldHeader) + } + + Ok(()) + } + + /// Check if the `SubmitFinalityProof` was successfully executed. + pub fn was_successful(finality_target: BlockNumberOf) -> bool { + match crate::BestFinalized::::get() { + Some(best_finalized) => best_finalized.number() == finality_target, + None => false, + } + } +} + +/// Trait representing a call that is a sub type of this pallet's call. +pub trait CallSubType, I: 'static>: + IsSubType, T>> +{ + /// Extract the finality target from a `SubmitParachainHeads` call. + fn submit_finality_proof_info(&self) -> Option> { + if let Some(crate::Call::::submit_finality_proof { finality_target, .. }) = + self.is_sub_type() + { + return Some(*finality_target.number()) + } + + None + } + + /// Validate Grandpa headers in order to avoid "mining" transactions that provide outdated + /// bridged chain headers. Without this validation, even honest relayers may lose their funds + /// if there are multiple relays running and submitting the same information. + fn check_obsolete_submit_finality_proof(&self) -> TransactionValidity + where + Self: Sized, + { + let finality_target = match self.submit_finality_proof_info() { + Some(finality_proof) => finality_proof, + _ => return Ok(ValidTransaction::default()), + }; + + match SubmitFinalityProofHelper::::check_obsolete(finality_target) { + Ok(_) => Ok(ValidTransaction::default()), + Err(Error::::OldHeader) => InvalidTransaction::Stale.into(), + Err(_) => InvalidTransaction::Call.into(), + } + } +} + +impl, I: 'static> CallSubType for T::RuntimeCall where + T::RuntimeCall: IsSubType, T>> +{ +} + +#[cfg(test)] +mod tests { + use crate::{ + call_ext::CallSubType, + mock::{run_test, test_header, RuntimeCall, TestNumber, TestRuntime}, + BestFinalized, + }; + use bp_runtime::HeaderId; + use bp_test_utils::make_default_justification; + + fn validate_block_submit(num: TestNumber) -> bool { + let bridge_grandpa_call = crate::Call::::submit_finality_proof { + finality_target: Box::new(test_header(num)), + justification: make_default_justification(&test_header(num)), + }; + RuntimeCall::check_obsolete_submit_finality_proof(&RuntimeCall::Grandpa( + bridge_grandpa_call, + )) + .is_ok() + } + + fn sync_to_header_10() { + let header10_hash = sp_core::H256::default(); + BestFinalized::::put(HeaderId(10, header10_hash)); + } + + #[test] + fn extension_rejects_obsolete_header() { + run_test(|| { + // when current best finalized is #10 and we're trying to import header#5 => tx is + // rejected + sync_to_header_10(); + assert!(!validate_block_submit(5)); + }); + } + + #[test] + fn extension_rejects_same_header() { + run_test(|| { + // when current best finalized is #10 and we're trying to import header#10 => tx is + // rejected + sync_to_header_10(); + assert!(!validate_block_submit(10)); + }); + } + + #[test] + fn extension_accepts_new_header() { + run_test(|| { + // when current best finalized is #10 and we're trying to import header#15 => tx is + // accepted + sync_to_header_10(); + assert!(validate_block_submit(15)); + }); + } +} diff --git a/bridges/modules/grandpa/src/extension.rs b/bridges/modules/grandpa/src/extension.rs deleted file mode 100644 index 87075ee5d58..00000000000 --- a/bridges/modules/grandpa/src/extension.rs +++ /dev/null @@ -1,117 +0,0 @@ -// 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 . - -use crate::{Config, Pallet}; -use bp_runtime::FilterCall; -use frame_support::{dispatch::CallableCallFor, traits::IsSubType}; -use sp_runtime::{ - traits::Header, - transaction_validity::{InvalidTransaction, TransactionValidity, ValidTransaction}, -}; - -/// Validate Grandpa headers in order to avoid "mining" transactions that provide outdated -/// bridged chain headers. Without this validation, even honest relayers may lose their funds -/// if there are multiple relays running and submitting the same information. -impl< - Call: IsSubType, T>>, - T: frame_system::Config + Config, - I: 'static, - > FilterCall for Pallet -{ - fn validate(call: &::RuntimeCall) -> TransactionValidity { - let bundled_block_number = match call.is_sub_type() { - Some(crate::Call::::submit_finality_proof { ref finality_target, .. }) => - *finality_target.number(), - _ => return Ok(ValidTransaction::default()), - }; - - let best_finalized = crate::BestFinalized::::get(); - let best_finalized_number = match best_finalized { - Some(best_finalized_id) => best_finalized_id.number(), - None => return InvalidTransaction::Call.into(), - }; - - if best_finalized_number >= bundled_block_number { - log::trace!( - target: crate::LOG_TARGET, - "Rejecting obsolete bridged header: bundled {:?}, best {:?}", - bundled_block_number, - best_finalized_number, - ); - - return InvalidTransaction::Stale.into() - } - - Ok(ValidTransaction::default()) - } -} - -#[cfg(test)] -mod tests { - use super::FilterCall; - use crate::{ - mock::{run_test, test_header, RuntimeCall, TestNumber, TestRuntime}, - BestFinalized, - }; - use bp_runtime::HeaderId; - use bp_test_utils::make_default_justification; - - fn validate_block_submit(num: TestNumber) -> bool { - crate::Pallet::::validate(&RuntimeCall::Grandpa(crate::Call::< - TestRuntime, - (), - >::submit_finality_proof { - finality_target: Box::new(test_header(num)), - justification: make_default_justification(&test_header(num)), - })) - .is_ok() - } - - fn sync_to_header_10() { - let header10_hash = sp_core::H256::default(); - BestFinalized::::put(HeaderId(10, header10_hash)); - } - - #[test] - fn extension_rejects_obsolete_header() { - run_test(|| { - // when current best finalized is #10 and we're trying to import header#5 => tx is - // rejected - sync_to_header_10(); - assert!(!validate_block_submit(5)); - }); - } - - #[test] - fn extension_rejects_same_header() { - run_test(|| { - // when current best finalized is #10 and we're trying to import header#10 => tx is - // rejected - sync_to_header_10(); - assert!(!validate_block_submit(10)); - }); - } - - #[test] - fn extension_accepts_new_header() { - run_test(|| { - // when current best finalized is #10 and we're trying to import header#15 => tx is - // accepted - sync_to_header_10(); - assert!(validate_block_submit(15)); - }); - } -} diff --git a/bridges/modules/grandpa/src/lib.rs b/bridges/modules/grandpa/src/lib.rs index 09b7f55a73c..c2b79434795 100644 --- a/bridges/modules/grandpa/src/lib.rs +++ b/bridges/modules/grandpa/src/lib.rs @@ -44,7 +44,7 @@ use bp_header_chain::{ }; use bp_runtime::{BlockNumberOf, Chain, HashOf, HasherOf, HeaderId, HeaderOf, OwnedBridgeModule}; use finality_grandpa::voter_set::VoterSet; -use frame_support::{dispatch::PostDispatchInfo, ensure, fail}; +use frame_support::{dispatch::PostDispatchInfo, ensure}; use sp_finality_grandpa::{ConsensusLog, GRANDPA_ENGINE_ID}; use sp_runtime::{ traits::{Header as HeaderT, Zero}, @@ -52,7 +52,7 @@ use sp_runtime::{ }; use sp_std::{boxed::Box, convert::TryInto}; -mod extension; +mod call_ext; #[cfg(test)] mod mock; mod storage_types; @@ -64,6 +64,7 @@ pub mod weights; pub mod benchmarking; // Re-export in crate namespace for `construct_runtime!` +pub use call_ext::*; pub use pallet::*; pub use weights::WeightInfo; @@ -154,7 +155,7 @@ pub mod pallet { /// If successful in verification, it will write the target header to the underlying storage /// pallet. #[pallet::call_index(0)] - #[pallet::weight(T::WeightInfo::submit_finality_proof( + #[pallet::weight(::submit_finality_proof( justification.commit.precommits.len().saturated_into(), justification.votes_ancestries.len().saturated_into(), ))] @@ -174,22 +175,7 @@ pub mod pallet { finality_target ); - let best_finalized_number = match BestFinalized::::get() { - Some(best_finalized_id) => best_finalized_id.number(), - None => { - log::error!( - target: LOG_TARGET, - "Cannot finalize header {:?} because pallet is not yet initialized", - finality_target, - ); - fail!(>::NotInitialized); - }, - }; - - // We do a quick check here to ensure that our header chain is making progress and isn't - // "travelling back in time" (which could be indicative of something bad, e.g a - // hard-fork). - ensure!(best_finalized_number < *number, >::OldHeader); + SubmitFinalityProofHelper::::check_obsolete(*number)?; let authority_set = >::get(); let unused_proof_size = authority_set.unused_proof_size(); diff --git a/bridges/modules/parachains/README.md b/bridges/modules/parachains/README.md index 1bd91a3ba77..5982c65ad31 100644 --- a/bridges/modules/parachains/README.md +++ b/bridges/modules/parachains/README.md @@ -71,7 +71,7 @@ It'd be better for anyone (for chain and for submitters) to reject all transacti already known parachain heads to the pallet. This way, we leave block space to other useful transactions and we don't charge concurrent submitters for their honest actions. -To deal with that, we have a [signed extension](./src/extension.rs) that may be added to the runtime. +To deal with that, we have a [signed extension](./src/call_ext) that may be added to the runtime. It does exactly what is required - rejects all transactions with already known heads. The submitter pays nothing for such transactions - they're simply removed from the transaction pool, when the block is built. diff --git a/bridges/modules/parachains/src/call_ext.rs b/bridges/modules/parachains/src/call_ext.rs new file mode 100644 index 00000000000..41649336579 --- /dev/null +++ b/bridges/modules/parachains/src/call_ext.rs @@ -0,0 +1,243 @@ +// 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 . + +use crate::{Config, Pallet, RelayBlockNumber}; +use bp_parachains::BestParaHeadHash; +use bp_polkadot_core::parachains::{ParaHash, ParaId}; +use frame_support::{dispatch::CallableCallFor, traits::IsSubType}; +use sp_runtime::{ + transaction_validity::{InvalidTransaction, TransactionValidity, ValidTransaction}, + RuntimeDebug, +}; + +/// Info about a `SubmitParachainHeads` call which tries to update a single parachain. +#[derive(Copy, Clone, PartialEq, RuntimeDebug)] +pub struct SubmitParachainHeadsInfo { + pub at_relay_block_number: RelayBlockNumber, + pub para_id: ParaId, + pub para_head_hash: ParaHash, +} + +/// Helper struct that provides methods for working with the `SubmitParachainHeads` call. +pub struct SubmitParachainHeadsHelper, I: 'static> { + pub _phantom_data: sp_std::marker::PhantomData<(T, I)>, +} + +impl, I: 'static> SubmitParachainHeadsHelper { + /// Check if the para head provided by the `SubmitParachainHeads` is better than the best one + /// we know. + pub fn is_obsolete(update: &SubmitParachainHeadsInfo) -> bool { + let stored_best_head = match crate::ParasInfo::::get(update.para_id) { + Some(stored_best_head) => stored_best_head, + None => return false, + }; + + if stored_best_head.best_head_hash.at_relay_block_number >= update.at_relay_block_number { + log::trace!( + target: crate::LOG_TARGET, + "The parachain head can't be updated. The parachain head for {:?} \ + was already updated at better relay chain block {} >= {}.", + update.para_id, + stored_best_head.best_head_hash.at_relay_block_number, + update.at_relay_block_number + ); + return true + } + + if stored_best_head.best_head_hash.head_hash == update.para_head_hash { + log::trace!( + target: crate::LOG_TARGET, + "The parachain head can't be updated. The parachain head hash for {:?} \ + was already updated to {} at block {} < {}.", + update.para_id, + update.para_head_hash, + stored_best_head.best_head_hash.at_relay_block_number, + update.at_relay_block_number + ); + return true + } + + false + } + + /// Check if the `SubmitParachainHeads` was successfully executed. + pub fn was_successful(update: &SubmitParachainHeadsInfo) -> bool { + match crate::ParasInfo::::get(update.para_id) { + Some(stored_best_head) => + stored_best_head.best_head_hash == + BestParaHeadHash { + at_relay_block_number: update.at_relay_block_number, + head_hash: update.para_head_hash, + }, + None => false, + } + } +} + +/// Trait representing a call that is a sub type of this pallet's call. +pub trait CallSubType, I: 'static>: + IsSubType, T>> +{ + /// Create a new instance of `SubmitParachainHeadsInfo` from a `SubmitParachainHeads` call with + /// one single parachain entry. + fn one_entry_submit_parachain_heads_info(&self) -> Option { + if let Some(crate::Call::::submit_parachain_heads { + ref at_relay_block, + ref parachains, + .. + }) = self.is_sub_type() + { + if let &[(para_id, para_head_hash)] = parachains.as_slice() { + return Some(SubmitParachainHeadsInfo { + at_relay_block_number: at_relay_block.0, + para_id, + para_head_hash, + }) + } + } + + None + } + + /// Create a new instance of `SubmitParachainHeadsInfo` from a `SubmitParachainHeads` call with + /// one single parachain entry, if the entry is for the provided parachain id. + fn submit_parachain_heads_info_for(&self, para_id: u32) -> Option { + self.one_entry_submit_parachain_heads_info() + .filter(|update| update.para_id.0 == para_id) + } + + /// Validate parachain heads in order to avoid "mining" transactions that provide + /// outdated bridged parachain heads. Without this validation, even honest relayers + /// may lose their funds if there are multiple relays running and submitting the + /// same information. + /// + /// This validation only works with transactions that are updating single parachain + /// head. We can't use unbounded validation - it may take too long and either break + /// block production, or "eat" significant portion of block production time literally + /// for nothing. In addition, the single-parachain-head-per-transaction is how the + /// pallet will be used in our environment. + fn check_obsolete_submit_parachain_heads(&self) -> TransactionValidity + where + Self: Sized, + { + let update = match self.one_entry_submit_parachain_heads_info() { + Some(update) => update, + None => return Ok(ValidTransaction::default()), + }; + + if SubmitParachainHeadsHelper::::is_obsolete(&update) { + return InvalidTransaction::Stale.into() + } + + Ok(ValidTransaction::default()) + } +} + +impl CallSubType for T::RuntimeCall +where + T: Config, + T::RuntimeCall: IsSubType, T>>, +{ +} + +#[cfg(test)] +mod tests { + use crate::{ + mock::{run_test, RuntimeCall, TestRuntime}, + CallSubType, ParaInfo, ParasInfo, RelayBlockNumber, + }; + use bp_parachains::BestParaHeadHash; + use bp_polkadot_core::parachains::{ParaHash, ParaHeadsProof, ParaId}; + + fn validate_submit_parachain_heads( + num: RelayBlockNumber, + parachains: Vec<(ParaId, ParaHash)>, + ) -> bool { + RuntimeCall::Parachains(crate::Call::::submit_parachain_heads { + at_relay_block: (num, Default::default()), + parachains, + parachain_heads_proof: ParaHeadsProof(Vec::new()), + }) + .check_obsolete_submit_parachain_heads() + .is_ok() + } + + fn sync_to_relay_header_10() { + ParasInfo::::insert( + ParaId(1), + ParaInfo { + best_head_hash: BestParaHeadHash { + at_relay_block_number: 10, + head_hash: [1u8; 32].into(), + }, + next_imported_hash_position: 0, + }, + ); + } + + #[test] + fn extension_rejects_header_from_the_obsolete_relay_block() { + run_test(|| { + // when current best finalized is #10 and we're trying to import header#5 => tx is + // rejected + sync_to_relay_header_10(); + assert!(!validate_submit_parachain_heads(5, vec![(ParaId(1), [1u8; 32].into())])); + }); + } + + #[test] + fn extension_rejects_header_from_the_same_relay_block() { + run_test(|| { + // when current best finalized is #10 and we're trying to import header#10 => tx is + // rejected + sync_to_relay_header_10(); + assert!(!validate_submit_parachain_heads(10, vec![(ParaId(1), [1u8; 32].into())])); + }); + } + + #[test] + fn extension_rejects_header_from_new_relay_block_with_same_hash() { + run_test(|| { + // when current best finalized is #10 and we're trying to import header#10 => tx is + // rejected + sync_to_relay_header_10(); + assert!(!validate_submit_parachain_heads(20, vec![(ParaId(1), [1u8; 32].into())])); + }); + } + + #[test] + fn extension_accepts_new_header() { + run_test(|| { + // when current best finalized is #10 and we're trying to import header#15 => tx is + // accepted + sync_to_relay_header_10(); + assert!(validate_submit_parachain_heads(15, vec![(ParaId(1), [2u8; 32].into())])); + }); + } + + #[test] + fn extension_accepts_if_more_than_one_parachain_is_submitted() { + run_test(|| { + // when current best finalized is #10 and we're trying to import header#5, but another + // parachain head is also supplied => tx is accepted + sync_to_relay_header_10(); + assert!(validate_submit_parachain_heads( + 5, + vec![(ParaId(1), [1u8; 32].into()), (ParaId(2), [1u8; 32].into())] + )); + }); + } +} diff --git a/bridges/modules/parachains/src/extension.rs b/bridges/modules/parachains/src/extension.rs deleted file mode 100644 index 5c2f54257ff..00000000000 --- a/bridges/modules/parachains/src/extension.rs +++ /dev/null @@ -1,166 +0,0 @@ -// 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 . - -use crate::{Config, Pallet, RelayBlockHash, RelayBlockHasher, RelayBlockNumber}; -use bp_runtime::FilterCall; -use frame_support::{dispatch::CallableCallFor, traits::IsSubType}; -use sp_runtime::transaction_validity::{InvalidTransaction, TransactionValidity, ValidTransaction}; - -/// Validate parachain heads in order to avoid "mining" transactions that provide -/// outdated bridged parachain heads. Without this validation, even honest relayers -/// may lose their funds if there are multiple relays running and submitting the -/// same information. -/// -/// This validation only works with transactions that are updating single parachain -/// head. We can't use unbounded validation - it may take too long and either break -/// block production, or "eat" significant portion of block production time literally -/// for nothing. In addition, the single-parachain-head-per-transaction is how the -/// pallet will be used in our environment. -impl< - Call: IsSubType, T>>, - T: frame_system::Config + Config, - I: 'static, - > FilterCall for Pallet -where - >::BridgedChain: - bp_runtime::Chain< - BlockNumber = RelayBlockNumber, - Hash = RelayBlockHash, - Hasher = RelayBlockHasher, - >, -{ - fn validate(call: &Call) -> TransactionValidity { - let (updated_at_relay_block_number, parachains) = match call.is_sub_type() { - Some(crate::Call::::submit_parachain_heads { - ref at_relay_block, - ref parachains, - .. - }) => (at_relay_block.0, parachains), - _ => return Ok(ValidTransaction::default()), - }; - let (parachain, parachain_head_hash) = match parachains.as_slice() { - &[(parachain, parachain_head_hash)] => (parachain, parachain_head_hash), - _ => return Ok(ValidTransaction::default()), - }; - - let maybe_stored_best_head = crate::ParasInfo::::get(parachain); - let is_valid = Self::validate_updated_parachain_head( - parachain, - &maybe_stored_best_head, - updated_at_relay_block_number, - parachain_head_hash, - "Rejecting obsolete parachain-head transaction", - ); - - if is_valid { - Ok(ValidTransaction::default()) - } else { - InvalidTransaction::Stale.into() - } - } -} - -#[cfg(test)] -mod tests { - use crate::{ - extension::FilterCall, - mock::{run_test, RuntimeCall, TestRuntime}, - ParaInfo, ParasInfo, RelayBlockNumber, - }; - use bp_parachains::BestParaHeadHash; - use bp_polkadot_core::parachains::{ParaHash, ParaHeadsProof, ParaId}; - - fn validate_submit_parachain_heads( - num: RelayBlockNumber, - parachains: Vec<(ParaId, ParaHash)>, - ) -> bool { - crate::Pallet::::validate(&RuntimeCall::Parachains(crate::Call::< - TestRuntime, - (), - >::submit_parachain_heads { - at_relay_block: (num, Default::default()), - parachains, - parachain_heads_proof: ParaHeadsProof(Vec::new()), - })) - .is_ok() - } - - fn sync_to_relay_header_10() { - ParasInfo::::insert( - ParaId(1), - ParaInfo { - best_head_hash: BestParaHeadHash { - at_relay_block_number: 10, - head_hash: [1u8; 32].into(), - }, - next_imported_hash_position: 0, - }, - ); - } - - #[test] - fn extension_rejects_header_from_the_obsolete_relay_block() { - run_test(|| { - // when current best finalized is #10 and we're trying to import header#5 => tx is - // rejected - sync_to_relay_header_10(); - assert!(!validate_submit_parachain_heads(5, vec![(ParaId(1), [1u8; 32].into())])); - }); - } - - #[test] - fn extension_rejects_header_from_the_same_relay_block() { - run_test(|| { - // when current best finalized is #10 and we're trying to import header#10 => tx is - // rejected - sync_to_relay_header_10(); - assert!(!validate_submit_parachain_heads(10, vec![(ParaId(1), [1u8; 32].into())])); - }); - } - - #[test] - fn extension_rejects_header_from_new_relay_block_with_same_hash() { - run_test(|| { - // when current best finalized is #10 and we're trying to import header#10 => tx is - // rejected - sync_to_relay_header_10(); - assert!(!validate_submit_parachain_heads(20, vec![(ParaId(1), [1u8; 32].into())])); - }); - } - - #[test] - fn extension_accepts_new_header() { - run_test(|| { - // when current best finalized is #10 and we're trying to import header#15 => tx is - // accepted - sync_to_relay_header_10(); - assert!(validate_submit_parachain_heads(15, vec![(ParaId(1), [2u8; 32].into())])); - }); - } - - #[test] - fn extension_accepts_if_more_than_one_parachain_is_submitted() { - run_test(|| { - // when current best finalized is #10 and we're trying to import header#5, but another - // parachain head is also supplied => tx is accepted - sync_to_relay_header_10(); - assert!(validate_submit_parachain_heads( - 5, - vec![(ParaId(1), [1u8; 32].into()), (ParaId(2), [1u8; 32].into())] - )); - }); - } -} diff --git a/bridges/modules/parachains/src/lib.rs b/bridges/modules/parachains/src/lib.rs index 1f060e675cb..6875a3690f6 100644 --- a/bridges/modules/parachains/src/lib.rs +++ b/bridges/modules/parachains/src/lib.rs @@ -41,6 +41,7 @@ use bp_runtime::HeaderOf; use codec::Encode; // Re-export in crate namespace for `construct_runtime!`. +pub use call_ext::*; pub use pallet::*; pub mod weights; @@ -49,7 +50,7 @@ pub mod weights_ext; #[cfg(feature = "runtime-benchmarks")] pub mod benchmarking; -mod extension; +mod call_ext; #[cfg(test)] mod mock; @@ -136,10 +137,30 @@ pub mod pallet { BridgeModule(bp_runtime::OwnedBridgeModuleError), } + /// Convenience trait for defining `BridgedChain` bounds. + pub trait BoundedBridgeGrandpaConfig: + pallet_bridge_grandpa::Config + { + type BridgedRelayChain: Chain< + BlockNumber = RelayBlockNumber, + Hash = RelayBlockHash, + Hasher = RelayBlockHasher, + >; + } + + impl BoundedBridgeGrandpaConfig for T + where + T: pallet_bridge_grandpa::Config, + T::BridgedChain: + Chain, + { + type BridgedRelayChain = T::BridgedChain; + } + #[pallet::config] #[pallet::disable_frame_system_supertrait_check] pub trait Config: - pallet_bridge_grandpa::Config + BoundedBridgeGrandpaConfig { /// The overarching event type. type RuntimeEvent: From> @@ -267,15 +288,7 @@ pub mod pallet { } #[pallet::call] - impl, I: 'static> Pallet - where - >::BridgedChain: - bp_runtime::Chain< - BlockNumber = RelayBlockNumber, - Hash = RelayBlockHash, - Hasher = RelayBlockHasher, - >, - { + impl, I: 'static> Pallet { /// Submit proof of one or several parachain heads. /// /// The proof is supposed to be proof of some `Heads` entries from the @@ -483,89 +496,38 @@ pub mod pallet { storage.read_and_decode_value(parachain_head_key.0.as_ref()) } - /// Check if para head has been already updated at better relay chain block. - /// Without this check, we may import heads in random order. - /// - /// Returns `true` if the pallet is ready to import given parachain head. - /// Returns `false` if the pallet already knows the same or better parachain head. - #[must_use] - pub fn validate_updated_parachain_head( - parachain: ParaId, - maybe_stored_best_head: &Option, - updated_at_relay_block_number: RelayBlockNumber, - updated_head_hash: ParaHash, - err_log_prefix: &str, - ) -> bool { - let stored_best_head = match maybe_stored_best_head { - Some(stored_best_head) => stored_best_head, - None => return true, - }; - - if stored_best_head.best_head_hash.at_relay_block_number >= - updated_at_relay_block_number - { - log::trace!( - target: LOG_TARGET, - "{}. The parachain head for {:?} was already updated at better relay chain block {} >= {}.", - err_log_prefix, - parachain, - stored_best_head.best_head_hash.at_relay_block_number, - updated_at_relay_block_number - ); - return false - } - - if stored_best_head.best_head_hash.head_hash == updated_head_hash { - log::trace!( - target: LOG_TARGET, - "{}. The parachain head hash for {:?} was already updated to {} at block {} < {}.", - err_log_prefix, - parachain, - updated_head_hash, - stored_best_head.best_head_hash.at_relay_block_number, - updated_at_relay_block_number - ); - return false - } - - true - } - /// Try to update parachain head. pub(super) fn update_parachain_head( parachain: ParaId, stored_best_head: Option, - updated_at_relay_block_number: RelayBlockNumber, - updated_head_data: ParaStoredHeaderData, - updated_head_hash: ParaHash, + new_at_relay_block_number: RelayBlockNumber, + new_head_data: ParaStoredHeaderData, + new_head_hash: ParaHash, ) -> Result { // check if head has been already updated at better relay chain block. Without this // check, we may import heads in random order - let err_log_prefix = "The parachain head can't be updated"; - let is_valid = Self::validate_updated_parachain_head( - parachain, - &stored_best_head, - updated_at_relay_block_number, - updated_head_hash, - err_log_prefix, - ); - if !is_valid { + let update = SubmitParachainHeadsInfo { + at_relay_block_number: new_at_relay_block_number, + para_id: parachain, + para_head_hash: new_head_hash, + }; + if SubmitParachainHeadsHelper::::is_obsolete(&update) { Self::deposit_event(Event::RejectedObsoleteParachainHead { parachain, - parachain_head_hash: updated_head_hash, + parachain_head_hash: new_head_hash, }); return Err(()) } // verify that the parachain head data size is <= `MaxParaHeadDataSize` let updated_head_data = - match StoredParaHeadDataOf::::try_from_inner(updated_head_data) { + match StoredParaHeadDataOf::::try_from_inner(new_head_data) { Ok(updated_head_data) => updated_head_data, Err(e) => { log::trace!( target: LOG_TARGET, - "{}. The parachain head data size for {:?} is {}. It exceeds maximal configured size {}.", - err_log_prefix, + "The parachain head can't be updated. The parachain head data size \ + for {:?} is {}. It exceeds maximal configured size {}.", parachain, e.value_size, e.maximal_size, @@ -573,7 +535,7 @@ pub mod pallet { Self::deposit_event(Event::RejectedLargeParachainHead { parachain, - parachain_head_hash: updated_head_hash, + parachain_head_hash: new_head_hash, parachain_head_size: e.value_size as _, }); @@ -589,8 +551,8 @@ pub mod pallet { ImportedParaHashes::::try_get(parachain, next_imported_hash_position); let updated_best_para_head = ParaInfo { best_head_hash: BestParaHeadHash { - at_relay_block_number: updated_at_relay_block_number, - head_hash: updated_head_hash, + at_relay_block_number: new_at_relay_block_number, + head_hash: new_head_hash, }, next_imported_hash_position: (next_imported_hash_position + 1) % T::HeadsToKeep::get(), @@ -598,14 +560,14 @@ pub mod pallet { ImportedParaHashes::::insert( parachain, next_imported_hash_position, - updated_head_hash, + new_head_hash, ); - ImportedParaHeads::::insert(parachain, updated_head_hash, updated_head_data); + ImportedParaHeads::::insert(parachain, new_head_hash, updated_head_data); log::trace!( target: LOG_TARGET, "Updated head of parachain {:?} to {}", parachain, - updated_head_hash, + new_head_hash, ); // remove old head @@ -621,7 +583,7 @@ pub mod pallet { } Self::deposit_event(Event::UpdatedParachainHead { parachain, - parachain_head_hash: updated_head_hash, + parachain_head_hash: new_head_hash, }); Ok(UpdateParachainHeadArtifacts { best_head: updated_best_para_head, prune_happened }) diff --git a/bridges/primitives/runtime/src/lib.rs b/bridges/primitives/runtime/src/lib.rs index 5eb1de2e3ac..83ffd238ab5 100644 --- a/bridges/primitives/runtime/src/lib.rs +++ b/bridges/primitives/runtime/src/lib.rs @@ -37,7 +37,6 @@ pub use chain::{ }; pub use frame_support::storage::storage_prefix as storage_value_final_key; use num_traits::{CheckedSub, One}; -use sp_runtime::transaction_validity::TransactionValidity; pub use storage_proof::{ record_all_keys as record_all_trie_keys, Error as StorageProofError, ProofSize as StorageProofSize, StorageProofChecker, @@ -490,12 +489,6 @@ pub trait OwnedBridgeModule { } } -/// A trait for querying whether a runtime call is valid. -pub trait FilterCall { - /// Checks if a runtime call is valid. - fn validate(call: &Call) -> TransactionValidity; -} - /// All extra operations with weights that we need in bridges. pub trait WeightExtraOps { /// Checked division of individual components of two weights.