// Copyright (C) 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 . //! Pallet that may be used instead of `SovereignPaidRemoteExporter` in the XCM router //! configuration. The main thing that the pallet offers is the dynamic message fee, //! that is computed based on the bridge queues state. It starts exponentially increasing //! if the queue between this chain and the sibling/child bridge hub is congested. //! //! All other bridge hub queues offer some backpressure mechanisms. So if at least one //! of all queues is congested, it will eventually lead to the growth of the queue at //! this chain. //! //! **A note on terminology**: when we mention the bridge hub here, we mean the chain that //! has the messages pallet deployed (`pallet-bridge-grandpa`, `pallet-bridge-messages`, //! `pallet-xcm-bridge-hub`, ...). It may be the system bridge hub parachain or any other //! chain. #![cfg_attr(not(feature = "std"), no_std)] use bp_xcm_bridge_hub_router::{ BridgeState, XcmChannelStatusProvider, MINIMAL_DELIVERY_FEE_FACTOR, }; use codec::Encode; use frame_support::traits::Get; use sp_core::H256; use sp_runtime::{FixedPointNumber, FixedU128, Saturating}; use xcm::prelude::*; use xcm_builder::{ExporterFor, SovereignPaidRemoteExporter}; pub use pallet::*; pub use weights::WeightInfo; pub mod benchmarking; pub mod weights; mod mock; /// The factor that is used to increase current message fee factor when bridge experiencing /// some lags. const EXPONENTIAL_FEE_BASE: FixedU128 = FixedU128::from_rational(105, 100); // 1.05 /// The factor that is used to increase current message fee factor for every sent kilobyte. const MESSAGE_SIZE_FEE_BASE: FixedU128 = FixedU128::from_rational(1, 1000); // 0.001 /// Maximal size of the XCM message that may be sent over bridge. /// /// This should be less than the maximal size, allowed by the messages pallet, because /// the message itself is wrapped in other structs and is double encoded. pub const HARD_MESSAGE_SIZE_LIMIT: u32 = 32 * 1024; /// The target that will be used when publishing logs related to this pallet. /// /// This doesn't match the pattern used by other bridge pallets (`runtime::bridge-*`). But this /// pallet has significant differences with those pallets. The main one is that is intended to /// be deployed at sending chains. Other bridge pallets are likely to be deployed at the separate /// bridge hub parachain. pub const LOG_TARGET: &str = "xcm::bridge-hub-router"; #[frame_support::pallet] pub mod pallet { use super::*; use frame_support::pallet_prelude::*; use frame_system::pallet_prelude::*; #[pallet::config] pub trait Config: frame_system::Config { /// Benchmarks results from runtime we're plugged into. type WeightInfo: WeightInfo; /// Universal location of this runtime. type UniversalLocation: Get; /// The bridged network that this config is for if specified. /// Also used for filtering `Bridges` by `BridgedNetworkId`. /// If not specified, allows all networks pass through. type BridgedNetworkId: Get>; /// Configuration for supported **bridged networks/locations** with **bridge location** and /// **possible fee**. Allows to externalize better control over allowed **bridged /// networks/locations**. type Bridges: ExporterFor; /// Checks the XCM version for the destination. type DestinationVersion: GetVersion; /// Origin of the sibling bridge hub that is allowed to report bridge status. type BridgeHubOrigin: EnsureOrigin; /// Actual message sender (`HRMP` or `DMP`) to the sibling bridge hub location. type ToBridgeHubSender: SendXcm; /// Underlying channel with the sibling bridge hub. It must match the channel, used /// by the `Self::ToBridgeHubSender`. type WithBridgeHubChannel: XcmChannelStatusProvider; /// Additional fee that is paid for every byte of the outbound message. type ByteFee: Get; /// Asset that is used to paid bridge fee. type FeeAsset: Get; } #[pallet::pallet] pub struct Pallet(PhantomData<(T, I)>); #[pallet::hooks] impl, I: 'static> Hooks> for Pallet { fn on_initialize(_n: BlockNumberFor) -> Weight { // TODO: make sure that `WithBridgeHubChannel::is_congested` returns true if either // of XCM channels (outbound/inbound) is suspended. Because if outbound is suspended // that is definitely congestion. If inbound is suspended, then we are not able to // receive the "report_bridge_status" signal (that maybe sent by the bridge hub). // if the channel with sibling/child bridge hub is suspended, we don't change // anything if T::WithBridgeHubChannel::is_congested() { return T::WeightInfo::on_initialize_when_congested() } // if bridge has reported congestion, we don't change anything let mut bridge = Self::bridge(); if bridge.is_congested { return T::WeightInfo::on_initialize_when_congested() } // if fee factor is already minimal, we don't change anything if bridge.delivery_fee_factor == MINIMAL_DELIVERY_FEE_FACTOR { return T::WeightInfo::on_initialize_when_congested() } let previous_factor = bridge.delivery_fee_factor; bridge.delivery_fee_factor = MINIMAL_DELIVERY_FEE_FACTOR.max(bridge.delivery_fee_factor / EXPONENTIAL_FEE_BASE); log::info!( target: LOG_TARGET, "Bridge queue is uncongested. Decreased fee factor from {} to {}", previous_factor, bridge.delivery_fee_factor, ); Bridge::::put(bridge); T::WeightInfo::on_initialize_when_non_congested() } } #[pallet::call] impl, I: 'static> Pallet { /// Notification about congested bridge queue. #[pallet::call_index(0)] #[pallet::weight(T::WeightInfo::report_bridge_status())] pub fn report_bridge_status( origin: OriginFor, // this argument is not currently used, but to ease future migration, we'll keep it // here bridge_id: H256, is_congested: bool, ) -> DispatchResult { let _ = T::BridgeHubOrigin::ensure_origin(origin)?; log::info!( target: LOG_TARGET, "Received bridge status from {:?}: congested = {}", bridge_id, is_congested, ); Bridge::::mutate(|bridge| { bridge.is_congested = is_congested; }); Ok(()) } } /// Bridge that we are using. /// /// **bridges-v1** assumptions: all outbound messages through this router are using single lane /// and to single remote consensus. If there is some other remote consensus that uses the same /// bridge hub, the separate pallet instance shall be used, In `v2` we'll have all required /// primitives (lane-id aka bridge-id, derived from XCM locations) to support multiple bridges /// by the same pallet instance. #[pallet::storage] #[pallet::getter(fn bridge)] pub type Bridge, I: 'static = ()> = StorageValue<_, BridgeState, ValueQuery>; impl, I: 'static> Pallet { /// Called when new message is sent (queued to local outbound XCM queue) over the bridge. pub(crate) fn on_message_sent_to_bridge(message_size: u32) { let _ = Bridge::::try_mutate(|bridge| { let is_channel_with_bridge_hub_congested = T::WithBridgeHubChannel::is_congested(); let is_bridge_congested = bridge.is_congested; // if outbound queue is not congested AND bridge has not reported congestion, do // nothing if !is_channel_with_bridge_hub_congested && !is_bridge_congested { return Err(()) } // ok - we need to increase the fee factor, let's do that let message_size_factor = FixedU128::from_u32(message_size.saturating_div(1024)) .saturating_mul(MESSAGE_SIZE_FEE_BASE); let total_factor = EXPONENTIAL_FEE_BASE.saturating_add(message_size_factor); let previous_factor = bridge.delivery_fee_factor; bridge.delivery_fee_factor = bridge.delivery_fee_factor.saturating_mul(total_factor); log::info!( target: LOG_TARGET, "Bridge channel is congested. Increased fee factor from {} to {}", previous_factor, bridge.delivery_fee_factor, ); Ok(()) }); } } } /// We'll be using `SovereignPaidRemoteExporter` to send remote messages over the sibling/child /// bridge hub. type ViaBridgeHubExporter = SovereignPaidRemoteExporter< Pallet, >::ToBridgeHubSender, >::UniversalLocation, >; // This pallet acts as the `ExporterFor` for the `SovereignPaidRemoteExporter` to compute // message fee using fee factor. impl, I: 'static> ExporterFor for Pallet { fn exporter_for( network: &NetworkId, remote_location: &InteriorLocation, message: &Xcm<()>, ) -> Option<(Location, Option)> { // ensure that the message is sent to the expected bridged network (if specified). if let Some(bridged_network) = T::BridgedNetworkId::get() { if *network != bridged_network { log::trace!( target: LOG_TARGET, "Router with bridged_network_id {:?} does not support bridging to network {:?}!", bridged_network, network, ); return None } } // ensure that the message is sent to the expected bridged network and location. let Some((bridge_hub_location, maybe_payment)) = T::Bridges::exporter_for(network, remote_location, message) else { log::trace!( target: LOG_TARGET, "Router with bridged_network_id {:?} does not support bridging to network {:?} and remote_location {:?}!", T::BridgedNetworkId::get(), network, remote_location, ); return None }; // take `base_fee` from `T::Brides`, but it has to be the same `T::FeeAsset` let base_fee = match maybe_payment { Some(payment) => match payment { Asset { fun: Fungible(amount), id } if id.eq(&T::FeeAsset::get()) => amount, invalid_asset => { log::error!( target: LOG_TARGET, "Router with bridged_network_id {:?} is configured for `T::FeeAsset` {:?} which is not \ compatible with {:?} for bridge_hub_location: {:?} for bridging to {:?}/{:?}!", T::BridgedNetworkId::get(), T::FeeAsset::get(), invalid_asset, bridge_hub_location, network, remote_location, ); return None }, }, None => 0, }; // compute fee amount. Keep in mind that this is only the bridge fee. The fee for sending // message from this chain to child/sibling bridge hub is determined by the // `Config::ToBridgeHubSender` let message_size = message.encoded_size(); let message_fee = (message_size as u128).saturating_mul(T::ByteFee::get()); let fee_sum = base_fee.saturating_add(message_fee); let fee_factor = Self::bridge().delivery_fee_factor; let fee = fee_factor.saturating_mul_int(fee_sum); let fee = if fee > 0 { Some((T::FeeAsset::get(), fee).into()) } else { None }; log::info!( target: LOG_TARGET, "Going to send message to {:?} ({} bytes) over bridge. Computed bridge fee {:?} using fee factor {}", (network, remote_location), message_size, fee, fee_factor ); Some((bridge_hub_location, fee)) } } // This pallet acts as the `SendXcm` to the sibling/child bridge hub instead of regular // XCMP/DMP transport. This allows injecting dynamic message fees into XCM programs that // are going to the bridged network. impl, I: 'static> SendXcm for Pallet { type Ticket = (u32, ::Ticket); fn validate( dest: &mut Option, xcm: &mut Option>, ) -> SendResult { // `dest` and `xcm` are required here let dest_ref = dest.as_ref().ok_or(SendError::MissingArgument)?; let xcm_ref = xcm.as_ref().ok_or(SendError::MissingArgument)?; // we won't have an access to `dest` and `xcm` in the `deliver` method, so precompute // everything required here let message_size = xcm_ref.encoded_size() as _; // bridge doesn't support oversized/overweight messages now. So it is better to drop such // messages here than at the bridge hub. Let's check the message size. if message_size > HARD_MESSAGE_SIZE_LIMIT { return Err(SendError::ExceedsMaxMessageSize) } // We need to ensure that the known `dest`'s XCM version can comprehend the current `xcm` // program. This may seem like an additional, unnecessary check, but it is not. A similar // check is probably performed by the `ViaBridgeHubExporter`, which attempts to send a // versioned message to the sibling bridge hub. However, the local bridge hub may have a // higher XCM version than the remote `dest`. Once again, it is better to discard such // messages here than at the bridge hub (e.g., to avoid losing funds). let destination_version = T::DestinationVersion::get_version_for(dest_ref) .ok_or(SendError::DestinationUnsupported)?; let _ = VersionedXcm::from(xcm_ref.clone()) .into_version(destination_version) .map_err(|()| SendError::DestinationUnsupported)?; // just use exporter to validate destination and insert instructions to pay message fee // at the sibling/child bridge hub // // the cost will include both cost of: (1) to-sibling bridge hub delivery (returned by // the `Config::ToBridgeHubSender`) and (2) to-bridged bridge hub delivery (returned by // `Self::exporter_for`) ViaBridgeHubExporter::::validate(dest, xcm) .map(|(ticket, cost)| ((message_size, ticket), cost)) } fn deliver(ticket: Self::Ticket) -> Result { // use router to enqueue message to the sibling/child bridge hub. This also should handle // payment for passing through this queue. let (message_size, ticket) = ticket; let xcm_hash = ViaBridgeHubExporter::::deliver(ticket)?; // increase delivery fee factor if required Self::on_message_sent_to_bridge(message_size); Ok(xcm_hash) } } #[cfg(test)] mod tests { use super::*; use frame_support::assert_ok; use mock::*; use frame_support::traits::Hooks; use sp_runtime::traits::One; fn congested_bridge(delivery_fee_factor: FixedU128) -> BridgeState { BridgeState { is_congested: true, delivery_fee_factor } } fn uncongested_bridge(delivery_fee_factor: FixedU128) -> BridgeState { BridgeState { is_congested: false, delivery_fee_factor } } #[test] fn initial_fee_factor_is_one() { run_test(|| { assert_eq!( Bridge::::get(), uncongested_bridge(MINIMAL_DELIVERY_FEE_FACTOR), ); }) } #[test] fn fee_factor_is_not_decreased_from_on_initialize_when_xcm_channel_is_congested() { run_test(|| { Bridge::::put(uncongested_bridge(FixedU128::from_rational(125, 100))); TestWithBridgeHubChannel::make_congested(); // it should not decrease, because xcm channel is congested let old_bridge = XcmBridgeHubRouter::bridge(); XcmBridgeHubRouter::on_initialize(One::one()); assert_eq!(XcmBridgeHubRouter::bridge(), old_bridge); }) } #[test] fn fee_factor_is_not_decreased_from_on_initialize_when_bridge_has_reported_congestion() { run_test(|| { Bridge::::put(congested_bridge(FixedU128::from_rational(125, 100))); // it should not decrease, because bridge congested let old_bridge = XcmBridgeHubRouter::bridge(); XcmBridgeHubRouter::on_initialize(One::one()); assert_eq!(XcmBridgeHubRouter::bridge(), old_bridge); }) } #[test] fn fee_factor_is_decreased_from_on_initialize_when_xcm_channel_is_uncongested() { run_test(|| { Bridge::::put(uncongested_bridge(FixedU128::from_rational(125, 100))); // it shold eventually decreased to one while XcmBridgeHubRouter::bridge().delivery_fee_factor > MINIMAL_DELIVERY_FEE_FACTOR { XcmBridgeHubRouter::on_initialize(One::one()); } // verify that it doesn't decreases anymore XcmBridgeHubRouter::on_initialize(One::one()); assert_eq!( XcmBridgeHubRouter::bridge(), uncongested_bridge(MINIMAL_DELIVERY_FEE_FACTOR) ); }) } #[test] fn not_applicable_if_destination_is_within_other_network() { run_test(|| { assert_eq!( send_xcm::( Location::new(2, [GlobalConsensus(Rococo), Parachain(1000)]), vec![].into(), ), Err(SendError::NotApplicable), ); }); } #[test] fn exceeds_max_message_size_if_size_is_above_hard_limit() { run_test(|| { assert_eq!( send_xcm::( Location::new(2, [GlobalConsensus(Rococo), Parachain(1000)]), vec![ClearOrigin; HARD_MESSAGE_SIZE_LIMIT as usize].into(), ), Err(SendError::ExceedsMaxMessageSize), ); }); } #[test] fn destination_unsupported_if_wrap_version_fails() { run_test(|| { assert_eq!( send_xcm::( UnknownXcmVersionLocation::get(), vec![ClearOrigin].into(), ), Err(SendError::DestinationUnsupported), ); }); } #[test] fn returns_proper_delivery_price() { run_test(|| { let dest = Location::new(2, [GlobalConsensus(BridgedNetworkId::get())]); let xcm: Xcm<()> = vec![ClearOrigin].into(); let msg_size = xcm.encoded_size(); // initially the base fee is used: `BASE_FEE + BYTE_FEE * msg_size + HRMP_FEE` let expected_fee = BASE_FEE + BYTE_FEE * (msg_size as u128) + HRMP_FEE; assert_eq!( XcmBridgeHubRouter::validate(&mut Some(dest.clone()), &mut Some(xcm.clone())) .unwrap() .1 .get(0), Some(&(BridgeFeeAsset::get(), expected_fee).into()), ); // but when factor is larger than one, it increases the fee, so it becomes: // `(BASE_FEE + BYTE_FEE * msg_size) * F + HRMP_FEE` let factor = FixedU128::from_rational(125, 100); Bridge::::put(uncongested_bridge(factor)); let expected_fee = (FixedU128::saturating_from_integer(BASE_FEE + BYTE_FEE * (msg_size as u128)) * factor) .into_inner() / FixedU128::DIV + HRMP_FEE; assert_eq!( XcmBridgeHubRouter::validate(&mut Some(dest), &mut Some(xcm)).unwrap().1.get(0), Some(&(BridgeFeeAsset::get(), expected_fee).into()), ); }); } #[test] fn sent_message_doesnt_increase_factor_if_xcm_channel_is_uncongested() { run_test(|| { let old_bridge = XcmBridgeHubRouter::bridge(); assert_ok!(send_xcm::( Location::new(2, [GlobalConsensus(BridgedNetworkId::get()), Parachain(1000)]), vec![ClearOrigin].into(), ) .map(drop)); assert!(TestToBridgeHubSender::is_message_sent()); assert_eq!(old_bridge, XcmBridgeHubRouter::bridge()); }); } #[test] fn sent_message_increases_factor_if_xcm_channel_is_congested() { run_test(|| { TestWithBridgeHubChannel::make_congested(); let old_bridge = XcmBridgeHubRouter::bridge(); assert_ok!(send_xcm::( Location::new(2, [GlobalConsensus(BridgedNetworkId::get()), Parachain(1000)]), vec![ClearOrigin].into(), ) .map(drop)); assert!(TestToBridgeHubSender::is_message_sent()); assert!( old_bridge.delivery_fee_factor < XcmBridgeHubRouter::bridge().delivery_fee_factor ); }); } #[test] fn sent_message_increases_factor_if_bridge_has_reported_congestion() { run_test(|| { Bridge::::put(congested_bridge(MINIMAL_DELIVERY_FEE_FACTOR)); let old_bridge = XcmBridgeHubRouter::bridge(); assert_ok!(send_xcm::( Location::new(2, [GlobalConsensus(BridgedNetworkId::get()), Parachain(1000)]), vec![ClearOrigin].into(), ) .map(drop)); assert!(TestToBridgeHubSender::is_message_sent()); assert!( old_bridge.delivery_fee_factor < XcmBridgeHubRouter::bridge().delivery_fee_factor ); }); } }