From 31a6cbeafba4abab10166227c1729b37ce529466 Mon Sep 17 00:00:00 2001
From: Svyatoslav Nikolsky <svyatonik@gmail.com>
Date: Wed, 16 Aug 2023 16:49:11 +0300
Subject: [PATCH] Dynamic fees for bridges-v1 (#2294)

* impl backpressure in the XcmBlobHaulerAdapter

* LocalXcmQueueManager + more adapters

* OnMessageDelviered callback

* forbid mesage delivery transactions when the channel between target bridge hub and target asset hub is suspended

* pallet-xcm-bridge-hub-router

* removed commented code

* improvements and tests for palle-xcm-bridge-router

* use LocalXcmChannel in XcmBlobMessageDispatch

* new tests for logic changes in messages pallet

* tests for LocalXcmQueueSuspender

* tests for LocalXcmQueueMessageProcessor

* tests for new logic in the XcmBlobHaulerAdapter

* fix other tests in the bridge-runtime-common

* extension_reject_call_when_dispatcher_is_inactive

* benchmarks for pallet-xcm-bridge-hub-router

* get rid of redundant storage value

* add new pallet to verify-pallets-build.sh

* fixing spellcheck, clippy and rustdoc

* trigger CI

* Revert "trigger CI"

This reverts commit 48f1ba032334e3c6d8470436483736988aa060ac.

* change log target for xcm bridge router pallet

* Update modules/xcm-bridge-hub-router/src/lib.rs

Co-authored-by: Branislav Kontur <bkontur@gmail.com>

* use saturated_len where possible

* fmt

* (Suggestion) Ability to externalize configuration for `ExporterFor` (#2313)

* Ability to externalize configuration for `ExporterFor`
(Replaced `BridgedNetworkId/SiblingBridgeHubLocation` with `Bridges: ExporterFor`)

* Fix millau

* Compile fix

* Return back `BridgedNetworkId` but as optional filter

* Replaced `BaseFee` with fees from inner `Bridges: ExporterFor`

* typo

* Clippy

* Rename LocalXcmChannel to XcmChannelStatusProvider (#2319)

* Rename LocalXcmChannel to XcmChannelStatusProvider

* fmt

* added/fixed some docs

* Dynamic fees v1: report congestion status to sending chain (#2318)

* report congestion status: changes at the sending chain

* OnMessagesDelivered is back

* report congestion status: changes at the bridge hub

* moer logging

* fix? benchmarks

* spelling

* tests for XcmBlobHaulerAdapter and LocalXcmQueueManager

* tests for messages pallet

* fix typo

* rustdoc

* Update modules/messages/src/lib.rs

* apply review suggestions

* ".git/.scripts/commands/fmt/fmt.sh"

* Added `XcmBridgeHubRouterCall::report_bridge_status` encodings for AHK/P (#2350)

* Added `XcmBridgeHubRouterCall::report_bridge_status` encodings for AHK/P

* Spellcheck

* Added const for `XcmBridgeHubRouterTransactCallMaxWeight`

* Cargo.lock

* Introduced base delivery fee constants

* Congestion messages as Optional to turn on/off `supports_congestion_detection`

* Spellcheck

* Ability to externalize dest for benchmarks

* Ability to externalize dest for benchmarks

---------

Co-authored-by: Branislav Kontur <bkontur@gmail.com>
Co-authored-by: command-bot <>
---
 bridges/bin/millau/runtime/Cargo.toml         |   5 +
 bridges/bin/millau/runtime/src/lib.rs         |  43 +-
 .../bin/millau/runtime/src/rialto_messages.rs |  19 +-
 .../runtime/src/rialto_parachain_messages.rs  |  20 +-
 bridges/bin/millau/runtime/src/xcm_config.rs  |  40 +-
 .../bin/rialto-parachain/runtime/src/lib.rs   |   1 +
 .../runtime/src/millau_messages.rs            |  19 +-
 bridges/bin/rialto/runtime/src/lib.rs         |   1 +
 .../bin/rialto/runtime/src/millau_messages.rs |  19 +-
 bridges/bin/runtime-common/Cargo.toml         |   2 +
 .../runtime-common/src/messages_call_ext.rs   |  29 +-
 .../src/messages_xcm_extension.rs             | 397 ++++++++++++-
 bridges/bin/runtime-common/src/mock.rs        |  40 +-
 .../src/refund_relayer_extension.rs           |   3 +-
 bridges/modules/messages/src/lib.rs           |  81 ++-
 bridges/modules/messages/src/mock.rs          |  37 +-
 .../modules/xcm-bridge-hub-router/Cargo.toml  |  59 ++
 .../xcm-bridge-hub-router/src/benchmarking.rs |  96 +++
 .../modules/xcm-bridge-hub-router/src/lib.rs  | 557 ++++++++++++++++++
 .../modules/xcm-bridge-hub-router/src/mock.rs | 148 +++++
 .../xcm-bridge-hub-router/src/weights.rs      | 208 +++++++
 .../chain-asset-hub-kusama/Cargo.toml         |  26 +
 .../chain-asset-hub-kusama/src/lib.rs         |  49 ++
 .../chain-asset-hub-polkadot/Cargo.toml       |  26 +
 .../chain-asset-hub-polkadot/src/lib.rs       |  49 ++
 bridges/primitives/messages/src/lib.rs        |   8 +
 .../primitives/messages/src/source_chain.rs   |  17 +-
 .../primitives/messages/src/target_chain.rs   |  13 +
 .../xcm-bridge-hub-router/Cargo.toml          |  24 +
 .../xcm-bridge-hub-router/src/lib.rs          |  66 +++
 30 files changed, 2031 insertions(+), 71 deletions(-)
 create mode 100644 bridges/modules/xcm-bridge-hub-router/Cargo.toml
 create mode 100644 bridges/modules/xcm-bridge-hub-router/src/benchmarking.rs
 create mode 100644 bridges/modules/xcm-bridge-hub-router/src/lib.rs
 create mode 100644 bridges/modules/xcm-bridge-hub-router/src/mock.rs
 create mode 100644 bridges/modules/xcm-bridge-hub-router/src/weights.rs
 create mode 100644 bridges/primitives/chain-asset-hub-kusama/Cargo.toml
 create mode 100644 bridges/primitives/chain-asset-hub-kusama/src/lib.rs
 create mode 100644 bridges/primitives/chain-asset-hub-polkadot/Cargo.toml
 create mode 100644 bridges/primitives/chain-asset-hub-polkadot/src/lib.rs
 create mode 100644 bridges/primitives/xcm-bridge-hub-router/Cargo.toml
 create mode 100644 bridges/primitives/xcm-bridge-hub-router/src/lib.rs

diff --git a/bridges/bin/millau/runtime/Cargo.toml b/bridges/bin/millau/runtime/Cargo.toml
index f4ad89159e0..ccff85b99eb 100644
--- a/bridges/bin/millau/runtime/Cargo.toml
+++ b/bridges/bin/millau/runtime/Cargo.toml
@@ -23,12 +23,14 @@ bp-rialto = { path = "../../../primitives/chain-rialto", default-features = fals
 bp-rialto-parachain = { path = "../../../primitives/chain-rialto-parachain", default-features = false }
 bp-runtime = { path = "../../../primitives/runtime", default-features = false }
 bp-westend = { path = "../../../primitives/chain-westend", default-features = false }
+bp-xcm-bridge-hub-router = { path = "../../../primitives/xcm-bridge-hub-router", default-features = false }
 bridge-runtime-common = { path = "../../runtime-common", 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 }
 pallet-shift-session-manager = { path = "../../../modules/shift-session-manager", default-features = false }
+pallet-xcm-bridge-hub-router = { path = "../../../modules/xcm-bridge-hub-router", default-features = false }
 
 # Substrate Dependencies
 
@@ -90,6 +92,7 @@ std = [
 	"bp-rialto-parachain/std",
 	"bp-runtime/std",
 	"bp-westend/std",
+	"bp-xcm-bridge-hub-router/std",
 	"bridge-runtime-common/std",
 	"codec/std",
 	"frame-executive/std",
@@ -114,6 +117,7 @@ std = [
 	"pallet-transaction-payment/std",
 	"pallet-utility/std",
 	"pallet-xcm/std",
+	"pallet-xcm-bridge-hub-router/std",
 	"scale-info/std",
 	"sp-api/std",
 	"sp-block-builder/std",
@@ -140,6 +144,7 @@ runtime-benchmarks = [
 	"pallet-bridge-parachains/runtime-benchmarks",
 	"pallet-bridge-relayers/runtime-benchmarks",
 	"pallet-xcm/runtime-benchmarks",
+	"pallet-xcm-bridge-hub-router/runtime-benchmarks",
 	"sp-runtime/runtime-benchmarks",
 	"xcm-builder/runtime-benchmarks",
 ]
diff --git a/bridges/bin/millau/runtime/src/lib.rs b/bridges/bin/millau/runtime/src/lib.rs
index a1526bc2e18..2067dfccb56 100644
--- a/bridges/bin/millau/runtime/src/lib.rs
+++ b/bridges/bin/millau/runtime/src/lib.rs
@@ -55,6 +55,7 @@ use sp_std::prelude::*;
 #[cfg(feature = "std")]
 use sp_version::NativeVersion;
 use sp_version::RuntimeVersion;
+use xcm_builder::NetworkExportTable;
 
 // to be able to use Millau runtime in `bridge-runtime-common` tests
 pub use bridge_runtime_common;
@@ -65,8 +66,8 @@ pub use frame_support::{
 	dispatch::DispatchClass,
 	parameter_types,
 	traits::{
-		ConstBool, ConstU32, ConstU64, ConstU8, Currency, ExistenceRequirement, Imbalance,
-		KeyOwnerProofSystem,
+		ConstBool, ConstU128, ConstU32, ConstU64, ConstU8, Currency, ExistenceRequirement,
+		Imbalance, KeyOwnerProofSystem,
 	},
 	weights::{
 		constants::WEIGHT_REF_TIME_PER_SECOND, ConstantMultiplier, IdentityFee, RuntimeDbWeight,
@@ -464,6 +465,7 @@ impl pallet_bridge_messages::Config<WithRialtoMessagesInstance> for Runtime {
 		WithRialtoMessagesInstance,
 		frame_support::traits::ConstU64<100_000>,
 	>;
+	type OnMessagesDelivered = ();
 
 	type SourceHeaderChain = crate::rialto_messages::RialtoAsSourceHeaderChain;
 	type MessageDispatch = crate::rialto_messages::FromRialtoMessageDispatch;
@@ -495,6 +497,7 @@ impl pallet_bridge_messages::Config<WithRialtoParachainMessagesInstance> for Run
 		WithRialtoParachainMessagesInstance,
 		frame_support::traits::ConstU64<100_000>,
 	>;
+	type OnMessagesDelivered = ();
 
 	type SourceHeaderChain = crate::rialto_parachain_messages::RialtoParachainAsSourceHeaderChain;
 	type MessageDispatch = crate::rialto_parachain_messages::FromRialtoParachainMessageDispatch;
@@ -544,6 +547,27 @@ impl pallet_utility::Config for Runtime {
 	type WeightInfo = ();
 }
 
+// this config is totally incorrect - the pallet is not actually used at this runtime. We need
+// it only to be able to run benchmarks and make required traits (and default weights for tests).
+parameter_types! {
+	pub BridgeTable: Vec<(xcm::prelude::NetworkId, xcm::prelude::MultiLocation, Option<xcm::prelude::MultiAsset>)>
+		= vec![(xcm_config::RialtoNetwork::get(), xcm_config::TokenLocation::get(), Some((xcm_config::TokenAssetId::get(), 1_000_000_000_u128).into()))];
+}
+impl pallet_xcm_bridge_hub_router::Config for Runtime {
+	type WeightInfo = ();
+
+	type UniversalLocation = xcm_config::UniversalLocation;
+	type BridgedNetworkId = xcm_config::RialtoNetwork;
+	type Bridges = NetworkExportTable<BridgeTable>;
+
+	type BridgeHubOrigin = frame_system::EnsureRoot<AccountId>;
+	type ToBridgeHubSender = xcm_config::XcmRouter;
+	type WithBridgeHubChannel = xcm_config::EmulatedSiblingXcmpChannel;
+
+	type ByteFee = ConstU128<1_000>;
+	type FeeAsset = xcm_config::TokenAssetId;
+}
+
 construct_runtime!(
 	pub enum Runtime {
 		System: frame_system::{Pallet, Call, Config<T>, Storage, Event<T>},
@@ -582,6 +606,9 @@ construct_runtime!(
 
 		// Pallet for sending XCM.
 		XcmPallet: pallet_xcm::{Pallet, Call, Storage, Event<T>, Origin, Config<T>} = 99,
+
+		// Pallets that are not actually used here (yet?), but we need to run benchmarks on it.
+		XcmBridgeHubRouter: pallet_xcm_bridge_hub_router::{Pallet, Storage} = 200,
 	}
 );
 
@@ -654,6 +681,7 @@ mod benches {
 		[pallet_bridge_grandpa, BridgeRialtoGrandpa]
 		[pallet_bridge_parachains, ParachainsBench::<Runtime, WithRialtoParachainsInstance>]
 		[pallet_bridge_relayers, RelayersBench::<Runtime>]
+		[pallet_xcm_bridge_hub_router, XcmBridgeHubRouterBench::<Runtime>]
 	);
 }
 
@@ -980,6 +1008,7 @@ impl_runtime_apis! {
 			use pallet_bridge_messages::benchmarking::Pallet as MessagesBench;
 			use pallet_bridge_parachains::benchmarking::Pallet as ParachainsBench;
 			use pallet_bridge_relayers::benchmarking::Pallet as RelayersBench;
+			use pallet_xcm_bridge_hub_router::benchmarking::Pallet as XcmBridgeHubRouterBench;
 
 			let mut list = Vec::<BenchmarkList>::new();
 			list_benchmarks!(list, extra);
@@ -1026,6 +1055,10 @@ impl_runtime_apis! {
 				Pallet as RelayersBench,
 				Config as RelayersConfig,
 			};
+			use pallet_xcm_bridge_hub_router::benchmarking::{
+				Pallet as XcmBridgeHubRouterBench,
+				Config as XcmBridgeHubRouterConfig,
+			};
 			use rialto_messages::WithRialtoMessageBridge;
 			use rialto_parachain_messages::WithRialtoParachainMessageBridge;
 
@@ -1136,6 +1169,12 @@ impl_runtime_apis! {
 				}
 			}
 
+			impl XcmBridgeHubRouterConfig<()> for Runtime {
+				fn make_congested() {
+					xcm_config::EmulatedSiblingXcmpChannel::make_congested()
+				}
+			}
+
 			let mut batches = Vec::<BenchmarkBatch>::new();
 			let params = (&config, &whitelist);
 
diff --git a/bridges/bin/millau/runtime/src/rialto_messages.rs b/bridges/bin/millau/runtime/src/rialto_messages.rs
index a9a80fdf7d7..884f994feaa 100644
--- a/bridges/bin/millau/runtime/src/rialto_messages.rs
+++ b/bridges/bin/millau/runtime/src/rialto_messages.rs
@@ -23,10 +23,11 @@ use bridge_runtime_common::{
 	messages::{
 		self, source::TargetHeaderChainAdapter, target::SourceHeaderChainAdapter, MessageBridge,
 	},
-	messages_xcm_extension::{XcmBlobHauler, XcmBlobHaulerAdapter},
+	messages_xcm_extension::{SenderAndLane, XcmBlobHauler, XcmBlobHaulerAdapter},
 };
 use frame_support::{parameter_types, weights::Weight, RuntimeDebug};
 use pallet_bridge_relayers::WeightInfoExt as _;
+use xcm::latest::prelude::*;
 use xcm_builder::HaulBlobExporter;
 
 /// Default lane that is used to send messages to Rialto.
@@ -42,6 +43,11 @@ parameter_types! {
 	/// 2 XCM instructions is for simple `Trap(42)` program, coming through bridge
 	/// (it is prepended with `UniversalOrigin` instruction).
 	pub const WeightCredit: Weight = BASE_XCM_WEIGHT_TWICE;
+	/// Lane used by the with-Rialto bridge.
+	pub RialtoSenderAndLane: SenderAndLane = SenderAndLane::new(Here.into(), XCM_LANE);
+
+	/// Dummy message used in configuration.
+	pub DummyXcmMessage: Xcm<()> = Xcm::new();
 }
 
 /// Message payload for Millau -> Rialto messages.
@@ -66,6 +72,7 @@ pub type FromRialtoMessageDispatch =
 	bridge_runtime_common::messages_xcm_extension::XcmBlobMessageDispatch<
 		crate::xcm_config::OnMillauBlobDispatcher,
 		(),
+		(),
 	>;
 
 /// Maximal outbound payload size of Millau -> Rialto messages.
@@ -122,11 +129,13 @@ pub type ToRialtoBlobExporter = HaulBlobExporter<
 pub struct ToRialtoXcmBlobHauler;
 
 impl XcmBlobHauler for ToRialtoXcmBlobHauler {
-	type MessageSender = pallet_bridge_messages::Pallet<Runtime, WithRialtoMessagesInstance>;
+	type Runtime = Runtime;
+	type MessagesInstance = WithRialtoMessagesInstance;
+	type SenderAndLane = RialtoSenderAndLane;
 
-	fn xcm_lane() -> LaneId {
-		XCM_LANE
-	}
+	type ToSourceChainSender = crate::xcm_config::XcmRouter;
+	type CongestedMessage = DummyXcmMessage;
+	type UncongestedMessage = DummyXcmMessage;
 }
 
 impl pallet_bridge_messages::WeightInfoExt for crate::weights::RialtoMessagesWeightInfo<Runtime> {
diff --git a/bridges/bin/millau/runtime/src/rialto_parachain_messages.rs b/bridges/bin/millau/runtime/src/rialto_parachain_messages.rs
index a8540b705d7..2eff0835c39 100644
--- a/bridges/bin/millau/runtime/src/rialto_parachain_messages.rs
+++ b/bridges/bin/millau/runtime/src/rialto_parachain_messages.rs
@@ -25,10 +25,11 @@ use bridge_runtime_common::{
 	messages::{
 		self, source::TargetHeaderChainAdapter, target::SourceHeaderChainAdapter, MessageBridge,
 	},
-	messages_xcm_extension::{XcmBlobHauler, XcmBlobHaulerAdapter},
+	messages_xcm_extension::{SenderAndLane, XcmBlobHauler, XcmBlobHaulerAdapter},
 };
 use frame_support::{parameter_types, weights::Weight, RuntimeDebug};
 use pallet_bridge_relayers::WeightInfoExt as _;
+use xcm::latest::prelude::*;
 use xcm_builder::HaulBlobExporter;
 
 /// Default lane that is used to send messages to Rialto parachain.
@@ -44,6 +45,11 @@ parameter_types! {
 	/// 2 XCM instructions is for simple `Trap(42)` program, coming through bridge
 	/// (it is prepended with `UniversalOrigin` instruction).
 	pub const WeightCredit: Weight = BASE_XCM_WEIGHT_TWICE;
+	/// Lane used by the with-RialtoParachain bridge.
+	pub RialtoParachainSenderAndLane: SenderAndLane = SenderAndLane::new(Here.into(), XCM_LANE);
+
+	/// Dummy message used in configuration.
+	pub DummyXcmMessage: Xcm<()> = Xcm::new();
 }
 
 /// Message payload for Millau -> RialtoParachain messages.
@@ -61,6 +67,7 @@ pub type FromRialtoParachainMessageDispatch =
 	bridge_runtime_common::messages_xcm_extension::XcmBlobMessageDispatch<
 		crate::xcm_config::OnMillauBlobDispatcher,
 		(),
+		(),
 	>;
 
 /// Maximal outbound payload size of Millau -> RialtoParachain messages.
@@ -122,12 +129,13 @@ pub type ToRialtoParachainBlobExporter = HaulBlobExporter<
 pub struct ToRialtoParachainXcmBlobHauler;
 
 impl XcmBlobHauler for ToRialtoParachainXcmBlobHauler {
-	type MessageSender =
-		pallet_bridge_messages::Pallet<Runtime, WithRialtoParachainMessagesInstance>;
+	type Runtime = Runtime;
+	type MessagesInstance = WithRialtoParachainMessagesInstance;
+	type SenderAndLane = RialtoParachainSenderAndLane;
 
-	fn xcm_lane() -> LaneId {
-		XCM_LANE
-	}
+	type ToSourceChainSender = crate::xcm_config::XcmRouter;
+	type CongestedMessage = DummyXcmMessage;
+	type UncongestedMessage = DummyXcmMessage;
 }
 
 impl pallet_bridge_messages::WeightInfoExt
diff --git a/bridges/bin/millau/runtime/src/xcm_config.rs b/bridges/bin/millau/runtime/src/xcm_config.rs
index 623c8219016..45236360ddc 100644
--- a/bridges/bin/millau/runtime/src/xcm_config.rs
+++ b/bridges/bin/millau/runtime/src/xcm_config.rs
@@ -42,6 +42,8 @@ parameter_types! {
 	/// chain, we make it synonymous with it and thus it is the `Here` location, which means "equivalent to
 	/// the context".
 	pub const TokenLocation: MultiLocation = Here.into_location();
+	/// Token asset identifier.
+	pub TokenAssetId: AssetId = TokenLocation::get().into();
 	/// The Millau network ID.
 	pub const ThisNetwork: NetworkId = CustomNetworkId::Millau.as_network_id();
 	/// The Rialto network ID.
@@ -98,7 +100,7 @@ parameter_types! {
 }
 
 /// The XCM router. We are not sending messages to sibling/parent/child chains here.
-pub type XcmRouter = ();
+pub type XcmRouter = EmulatedSiblingXcmpChannel;
 
 /// The barriers one of which must be passed for an XCM message to be executed.
 pub type Barrier = (
@@ -235,6 +237,38 @@ impl ExportXcm for ToRialtoOrRialtoParachainSwitchExporter {
 	}
 }
 
+/// Emulating XCMP channel with sibling chain. We don't have required infra here, at Millau,
+/// so we have to provide at least something to be able to run benchmarks.
+pub struct EmulatedSiblingXcmpChannel;
+
+impl SendXcm for EmulatedSiblingXcmpChannel {
+	type Ticket = ();
+
+	fn validate(
+		_destination: &mut Option<MultiLocation>,
+		_message: &mut Option<Xcm<()>>,
+	) -> SendResult<Self::Ticket> {
+		Ok(((), Default::default()))
+	}
+
+	fn deliver(_ticket: Self::Ticket) -> Result<XcmHash, SendError> {
+		Ok(XcmHash::default())
+	}
+}
+
+impl EmulatedSiblingXcmpChannel {
+	/// Start emulating congested channel.
+	pub fn make_congested() {
+		frame_support::storage::unhashed::put(b"EmulatedSiblingXcmpChannel.Congested", &true);
+	}
+}
+
+impl bp_xcm_bridge_hub_router::XcmChannelStatusProvider for EmulatedSiblingXcmpChannel {
+	fn is_congested() -> bool {
+		frame_support::storage::unhashed::get_or_default(b"EmulatedSiblingXcmpChannel.Congested")
+	}
+}
+
 #[cfg(test)]
 mod tests {
 	use super::*;
@@ -357,7 +391,7 @@ mod tests {
 		let dispatch_result = FromRialtoMessageDispatch::dispatch(incoming_message);
 		assert!(matches!(
 			dispatch_result.dispatch_level_result,
-			XcmBlobMessageDispatchResult::NotDispatched(_),
+			XcmBlobMessageDispatchResult::Dispatched,
 		));
 	}
 
@@ -370,7 +404,7 @@ mod tests {
 		let dispatch_result = FromRialtoMessageDispatch::dispatch(incoming_message);
 		assert!(matches!(
 			dispatch_result.dispatch_level_result,
-			XcmBlobMessageDispatchResult::NotDispatched(_),
+			XcmBlobMessageDispatchResult::Dispatched,
 		));
 	}
 }
diff --git a/bridges/bin/rialto-parachain/runtime/src/lib.rs b/bridges/bin/rialto-parachain/runtime/src/lib.rs
index cae840de967..9bfe2413944 100644
--- a/bridges/bin/rialto-parachain/runtime/src/lib.rs
+++ b/bridges/bin/rialto-parachain/runtime/src/lib.rs
@@ -584,6 +584,7 @@ impl pallet_bridge_messages::Config<WithMillauMessagesInstance> for Runtime {
 		WithMillauMessagesInstance,
 		frame_support::traits::ConstU128<100_000>,
 	>;
+	type OnMessagesDelivered = ();
 
 	type SourceHeaderChain = crate::millau_messages::MillauAsSourceHeaderChain;
 	type MessageDispatch = crate::millau_messages::FromMillauMessageDispatch;
diff --git a/bridges/bin/rialto-parachain/runtime/src/millau_messages.rs b/bridges/bin/rialto-parachain/runtime/src/millau_messages.rs
index 666b1f0a841..fea7fc70c86 100644
--- a/bridges/bin/rialto-parachain/runtime/src/millau_messages.rs
+++ b/bridges/bin/rialto-parachain/runtime/src/millau_messages.rs
@@ -26,9 +26,10 @@ use bridge_runtime_common::{
 	messages::{
 		self, source::TargetHeaderChainAdapter, target::SourceHeaderChainAdapter, MessageBridge,
 	},
-	messages_xcm_extension::{XcmBlobHauler, XcmBlobHaulerAdapter},
+	messages_xcm_extension::{SenderAndLane, XcmBlobHauler, XcmBlobHaulerAdapter},
 };
 use frame_support::{parameter_types, weights::Weight, RuntimeDebug};
+use xcm::latest::prelude::*;
 use xcm_builder::HaulBlobExporter;
 
 /// Default lane that is used to send messages to Millau.
@@ -44,6 +45,11 @@ parameter_types! {
 	/// 2 XCM instructions is for simple `Trap(42)` program, coming through bridge
 	/// (it is prepended with `UniversalOrigin` instruction).
 	pub const WeightCredit: Weight = BASE_XCM_WEIGHT_TWICE;
+	/// Lane used by the with-Millau bridge.
+	pub MullauSenderAndLane: SenderAndLane = SenderAndLane::new(Here.into(), XCM_LANE);
+
+	/// Dummy message used in configuration.
+	pub DummyXcmMessage: Xcm<()> = Xcm::new();
 }
 
 /// Message payload for RialtoParachain -> Millau messages.
@@ -61,6 +67,7 @@ pub type FromMillauMessageDispatch =
 	bridge_runtime_common::messages_xcm_extension::XcmBlobMessageDispatch<
 		crate::OnRialtoParachainBlobDispatcher,
 		(),
+		(),
 	>;
 
 /// Messages proof for Millau -> RialtoParachain messages.
@@ -122,11 +129,13 @@ pub type ToMillauBlobExporter =
 pub struct ToMillauXcmBlobHauler;
 
 impl XcmBlobHauler for ToMillauXcmBlobHauler {
-	type MessageSender = pallet_bridge_messages::Pallet<Runtime, WithMillauMessagesInstance>;
+	type Runtime = Runtime;
+	type MessagesInstance = WithMillauMessagesInstance;
+	type SenderAndLane = MullauSenderAndLane;
 
-	fn xcm_lane() -> LaneId {
-		XCM_LANE
-	}
+	type ToSourceChainSender = crate::XcmRouter;
+	type CongestedMessage = DummyXcmMessage;
+	type UncongestedMessage = DummyXcmMessage;
 }
 
 #[cfg(test)]
diff --git a/bridges/bin/rialto/runtime/src/lib.rs b/bridges/bin/rialto/runtime/src/lib.rs
index dbfa0cf7eb6..95c1f60b601 100644
--- a/bridges/bin/rialto/runtime/src/lib.rs
+++ b/bridges/bin/rialto/runtime/src/lib.rs
@@ -442,6 +442,7 @@ impl pallet_bridge_messages::Config<WithMillauMessagesInstance> for Runtime {
 		WithMillauMessagesInstance,
 		frame_support::traits::ConstU128<100_000>,
 	>;
+	type OnMessagesDelivered = ();
 
 	type SourceHeaderChain = crate::millau_messages::MillauAsSourceHeaderChain;
 	type MessageDispatch = crate::millau_messages::FromMillauMessageDispatch;
diff --git a/bridges/bin/rialto/runtime/src/millau_messages.rs b/bridges/bin/rialto/runtime/src/millau_messages.rs
index 357a5d7f02f..4efef0bf1e5 100644
--- a/bridges/bin/rialto/runtime/src/millau_messages.rs
+++ b/bridges/bin/rialto/runtime/src/millau_messages.rs
@@ -23,9 +23,10 @@ use bridge_runtime_common::{
 	messages::{
 		self, source::TargetHeaderChainAdapter, target::SourceHeaderChainAdapter, MessageBridge,
 	},
-	messages_xcm_extension::{XcmBlobHauler, XcmBlobHaulerAdapter},
+	messages_xcm_extension::{SenderAndLane, XcmBlobHauler, XcmBlobHaulerAdapter},
 };
 use frame_support::{parameter_types, weights::Weight, RuntimeDebug};
+use xcm::latest::prelude::*;
 use xcm_builder::HaulBlobExporter;
 
 /// Lane that is used for XCM messages exchange.
@@ -41,6 +42,11 @@ parameter_types! {
 	/// 2 XCM instructions is for simple `Trap(42)` program, coming through bridge
 	/// (it is prepended with `UniversalOrigin` instruction).
 	pub const WeightCredit: Weight = BASE_XCM_WEIGHT_TWICE;
+	/// Lane used by the with-Millau bridge.
+	pub MullauSenderAndLane: SenderAndLane = SenderAndLane::new(Here.into(), XCM_LANE);
+
+	/// Dummy message used in configuration.
+	pub DummyXcmMessage: Xcm<()> = Xcm::new();
 }
 
 /// Message payload for Rialto -> Millau messages.
@@ -58,6 +64,7 @@ pub type FromMillauMessageDispatch =
 	bridge_runtime_common::messages_xcm_extension::XcmBlobMessageDispatch<
 		crate::xcm_config::OnRialtoBlobDispatcher,
 		(),
+		(),
 	>;
 
 /// Messages proof for Millau -> Rialto messages.
@@ -121,11 +128,13 @@ pub type ToMillauBlobExporter = HaulBlobExporter<
 pub struct ToMillauXcmBlobHauler;
 
 impl XcmBlobHauler for ToMillauXcmBlobHauler {
-	type MessageSender = pallet_bridge_messages::Pallet<Runtime, WithMillauMessagesInstance>;
+	type Runtime = Runtime;
+	type MessagesInstance = WithMillauMessagesInstance;
+	type SenderAndLane = MullauSenderAndLane;
 
-	fn xcm_lane() -> LaneId {
-		XCM_LANE
-	}
+	type ToSourceChainSender = crate::xcm_config::XcmRouter;
+	type CongestedMessage = DummyXcmMessage;
+	type UncongestedMessage = DummyXcmMessage;
 }
 
 #[cfg(test)]
diff --git a/bridges/bin/runtime-common/Cargo.toml b/bridges/bin/runtime-common/Cargo.toml
index 98b3bc99250..9ca6a4c5df9 100644
--- a/bridges/bin/runtime-common/Cargo.toml
+++ b/bridges/bin/runtime-common/Cargo.toml
@@ -21,6 +21,7 @@ bp-parachains = { path = "../../primitives/parachains", default-features = false
 bp-polkadot-core = { path = "../../primitives/polkadot-core", default-features = false }
 bp-relayers = { path = "../../primitives/relayers", default-features = false }
 bp-runtime = { path = "../../primitives/runtime", default-features = false }
+bp-xcm-bridge-hub-router = { path = "../../primitives/xcm-bridge-hub-router", 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 }
@@ -55,6 +56,7 @@ std = [
 	"bp-parachains/std",
 	"bp-polkadot-core/std",
 	"bp-runtime/std",
+	"bp-xcm-bridge-hub-router/std",
 	"codec/std",
 	"frame-support/std",
 	"frame-system/std",
diff --git a/bridges/bin/runtime-common/src/messages_call_ext.rs b/bridges/bin/runtime-common/src/messages_call_ext.rs
index 8b4a50a0f30..0a1d7243620 100644
--- a/bridges/bin/runtime-common/src/messages_call_ext.rs
+++ b/bridges/bin/runtime-common/src/messages_call_ext.rs
@@ -17,7 +17,7 @@
 use crate::messages::{
 	source::FromBridgedChainMessagesDeliveryProof, target::FromBridgedChainMessagesProof,
 };
-use bp_messages::{InboundLaneData, LaneId, MessageNonce};
+use bp_messages::{target_chain::MessageDispatch, InboundLaneData, LaneId, MessageNonce};
 use frame_support::{
 	dispatch::CallableCallFor,
 	traits::{Get, IsSubType},
@@ -77,7 +77,12 @@ impl ReceiveMessagesProofInfo {
 	///
 	/// - or there are no bundled messages, but the inbound lane is blocked by too many unconfirmed
 	///   messages and/or unrewarded relayers.
-	fn is_obsolete(&self) -> bool {
+	fn is_obsolete(&self, is_dispatcher_active: bool) -> bool {
+		// if dispatcher is inactive, we don't accept any delivery transactions
+		if !is_dispatcher_active {
+			return true
+		}
+
 		// transactions with zero bundled nonces are not allowed, unless they're message
 		// delivery transactions, which brings reward confirmations required to unblock
 		// the lane
@@ -275,7 +280,9 @@ impl<
 
 	fn check_obsolete_call(&self) -> TransactionValidity {
 		match self.call_info() {
-			Some(CallInfo::ReceiveMessagesProof(proof_info)) if proof_info.is_obsolete() => {
+			Some(CallInfo::ReceiveMessagesProof(proof_info))
+				if proof_info.is_obsolete(T::MessageDispatch::is_active()) =>
+			{
 				log::trace!(
 					target: pallet_bridge_messages::LOG_TARGET,
 					"Rejecting obsolete messages delivery transaction: {:?}",
@@ -327,8 +334,8 @@ mod tests {
 		},
 		messages_call_ext::MessagesCallSubType,
 		mock::{
-			MaxUnconfirmedMessagesAtInboundLane, MaxUnrewardedRelayerEntriesAtInboundLane,
-			TestRuntime, ThisChainRuntimeCall,
+			DummyMessageDispatch, MaxUnconfirmedMessagesAtInboundLane,
+			MaxUnrewardedRelayerEntriesAtInboundLane, TestRuntime, ThisChainRuntimeCall,
 		},
 	};
 	use bp_messages::{DeliveredMessages, UnrewardedRelayer, UnrewardedRelayersState};
@@ -435,6 +442,18 @@ mod tests {
 		});
 	}
 
+	#[test]
+	fn extension_reject_call_when_dispatcher_is_inactive() {
+		sp_io::TestExternalities::new(Default::default()).execute_with(|| {
+			// when current best delivered is message#10 and we're trying to deliver message 11..=15
+			// => tx is accepted, but we have inactive dispatcher, so...
+			deliver_message_10();
+
+			DummyMessageDispatch::deactivate();
+			assert!(!validate_message_delivery(11, 15));
+		});
+	}
+
 	#[test]
 	fn extension_rejects_empty_delivery_with_rewards_confirmations_if_there_are_free_relayer_and_message_slots(
 	) {
diff --git a/bridges/bin/runtime-common/src/messages_xcm_extension.rs b/bridges/bin/runtime-common/src/messages_xcm_extension.rs
index 0317d745c15..44e554ecb24 100644
--- a/bridges/bin/runtime-common/src/messages_xcm_extension.rs
+++ b/bridges/bin/runtime-common/src/messages_xcm_extension.rs
@@ -22,16 +22,22 @@
 //! `XcmRouter` <- `MessageDispatch` <- `InboundMessageQueue`
 
 use bp_messages::{
-	source_chain::MessagesBridge,
+	source_chain::{MessagesBridge, OnMessagesDelivered},
 	target_chain::{DispatchMessage, MessageDispatch},
-	LaneId,
+	LaneId, MessageNonce,
 };
 use bp_runtime::messages::MessageDispatchResult;
+use bp_xcm_bridge_hub_router::XcmChannelStatusProvider;
 use codec::{Decode, Encode};
-use frame_support::{dispatch::Weight, CloneNoBound, EqNoBound, PartialEqNoBound};
-use pallet_bridge_messages::WeightInfoExt as MessagesPalletWeights;
+use frame_support::{dispatch::Weight, traits::Get, CloneNoBound, EqNoBound, PartialEqNoBound};
+use pallet_bridge_messages::{
+	Config as MessagesConfig, OutboundLanesCongestedSignals, Pallet as MessagesPallet,
+	WeightInfoExt as MessagesPalletWeights,
+};
 use scale_info::TypeInfo;
 use sp_runtime::SaturatedConversion;
+use sp_std::{fmt::Debug, marker::PhantomData};
+use xcm::prelude::*;
 use xcm_builder::{DispatchBlob, DispatchBlobError, HaulBlob, HaulBlobError};
 
 /// Plain "XCM" payload, which we transfer through bridge
@@ -46,16 +52,25 @@ pub enum XcmBlobMessageDispatchResult {
 }
 
 /// [`XcmBlobMessageDispatch`] is responsible for dispatching received messages
-pub struct XcmBlobMessageDispatch<DispatchBlob, Weights> {
-	_marker: sp_std::marker::PhantomData<(DispatchBlob, Weights)>,
+///
+/// It needs to be used at the target bridge hub.
+pub struct XcmBlobMessageDispatch<DispatchBlob, Weights, Channel> {
+	_marker: sp_std::marker::PhantomData<(DispatchBlob, Weights, Channel)>,
 }
 
-impl<BlobDispatcher: DispatchBlob, Weights: MessagesPalletWeights> MessageDispatch
-	for XcmBlobMessageDispatch<BlobDispatcher, Weights>
+impl<
+		BlobDispatcher: DispatchBlob,
+		Weights: MessagesPalletWeights,
+		Channel: XcmChannelStatusProvider,
+	> MessageDispatch for XcmBlobMessageDispatch<BlobDispatcher, Weights, Channel>
 {
 	type DispatchPayload = XcmAsPlainPayload;
 	type DispatchLevelResult = XcmBlobMessageDispatchResult;
 
+	fn is_active() -> bool {
+		!Channel::is_congested()
+	}
+
 	fn dispatch_weight(message: &mut DispatchMessage<Self::DispatchPayload>) -> Weight {
 		match message.data.payload {
 			Ok(ref payload) => {
@@ -106,40 +121,374 @@ impl<BlobDispatcher: DispatchBlob, Weights: MessagesPalletWeights> MessageDispat
 	}
 }
 
+/// A pair of sending chain location and message lane, used by this chain to send messages
+/// over the bridge.
+pub struct SenderAndLane {
+	/// Sending chain relative location.
+	pub location: MultiLocation,
+	/// Message lane, used by the sending chain.
+	pub lane: LaneId,
+}
+
+impl SenderAndLane {
+	/// Create new object using provided location and lane.
+	pub fn new(location: MultiLocation, lane: LaneId) -> Self {
+		SenderAndLane { location, lane }
+	}
+}
+
 /// [`XcmBlobHauler`] is responsible for sending messages to the bridge "point-to-point link" from
 /// one side, where on the other it can be dispatched by [`XcmBlobMessageDispatch`].
 pub trait XcmBlobHauler {
-	/// Runtime message sender adapter.
-	type MessageSender: MessagesBridge<XcmAsPlainPayload>;
+	/// Runtime that has messages pallet deployed.
+	type Runtime: MessagesConfig<Self::MessagesInstance>;
+	/// Instance of the messages pallet that is used to send messages.
+	type MessagesInstance: 'static;
+	/// Returns lane used by this hauler.
+	type SenderAndLane: Get<SenderAndLane>;
+
+	/// Actual XCM message sender (`HRMP` or `UMP`) to the source chain
+	/// location (`Self::SenderAndLane::get().location`).
+	type ToSourceChainSender: SendXcm;
+	/// An XCM message that is sent to the sending chain when the bridge queue becomes congested.
+	type CongestedMessage: Get<Option<Xcm<()>>>;
+	/// An XCM message that is sent to the sending chain when the bridge queue becomes not
+	/// congested.
+	type UncongestedMessage: Get<Option<Xcm<()>>>;
 
-	/// Return message lane (as "point-to-point link") used to deliver XCM messages.
-	fn xcm_lane() -> LaneId;
+	/// Returns `true` if we want to handle congestion.
+	fn supports_congestion_detection() -> bool {
+		Self::CongestedMessage::get().is_some() || Self::UncongestedMessage::get().is_some()
+	}
 }
 
-/// XCM bridge adapter which connects [`XcmBlobHauler`] with [`XcmBlobHauler::MessageSender`] and
-/// makes sure that XCM blob is sent to the [`pallet_bridge_messages`] queue to be relayed.
+/// XCM bridge adapter which connects [`XcmBlobHauler`] with [`pallet_bridge_messages`] and
+/// makes sure that XCM blob is sent to the outbound lane to be relayed.
+///
+/// It needs to be used at the source bridge hub.
 pub struct XcmBlobHaulerAdapter<XcmBlobHauler>(sp_std::marker::PhantomData<XcmBlobHauler>);
-impl<H: XcmBlobHauler> HaulBlob for XcmBlobHaulerAdapter<H> {
+
+impl<H: XcmBlobHauler> HaulBlob for XcmBlobHaulerAdapter<H>
+where
+	H::Runtime: MessagesConfig<H::MessagesInstance, OutboundPayload = XcmAsPlainPayload>,
+{
 	fn haul_blob(blob: sp_std::prelude::Vec<u8>) -> Result<(), HaulBlobError> {
-		let lane = H::xcm_lane();
-		H::MessageSender::send_message(lane, blob)
-			.map(|artifacts| (lane, artifacts.nonce).using_encoded(sp_io::hashing::blake2_256))
-			.map(|result| {
+		let sender_and_lane = H::SenderAndLane::get();
+		MessagesPallet::<H::Runtime, H::MessagesInstance>::send_message(sender_and_lane.lane, blob)
+			.map(|artifacts| {
 				log::info!(
 					target: crate::LOG_TARGET_BRIDGE_DISPATCH,
-					"haul_blob result - ok: {:?} on lane: {:?}",
-					result,
-					lane
-				)
+					"haul_blob result - ok: {:?} on lane: {:?}. Enqueued messages: {}",
+					artifacts.nonce,
+					sender_and_lane.lane,
+					artifacts.enqueued_messages,
+				);
+
+				// notify XCM queue manager about updated lane state
+				LocalXcmQueueManager::<H>::on_bridge_message_enqueued(
+					&sender_and_lane,
+					artifacts.enqueued_messages,
+				);
 			})
 			.map_err(|error| {
 				log::error!(
 					target: crate::LOG_TARGET_BRIDGE_DISPATCH,
 					"haul_blob result - error: {:?} on lane: {:?}",
 					error,
-					lane
+					sender_and_lane.lane,
 				);
 				HaulBlobError::Transport("MessageSenderError")
 			})
 	}
 }
+
+impl<H: XcmBlobHauler> OnMessagesDelivered for XcmBlobHaulerAdapter<H> {
+	fn on_messages_delivered(lane: LaneId, enqueued_messages: MessageNonce) {
+		let sender_and_lane = H::SenderAndLane::get();
+		if sender_and_lane.lane != lane {
+			return
+		}
+
+		// notify XCM queue manager about updated lane state
+		LocalXcmQueueManager::<H>::on_bridge_messages_delivered(
+			&sender_and_lane,
+			enqueued_messages,
+		);
+	}
+}
+
+/// Manager of local XCM queues (and indirectly - underlying transport channels) that
+/// controls the queue state.
+///
+/// It needs to be used at the source bridge hub.
+pub struct LocalXcmQueueManager<H>(PhantomData<H>);
+
+/// Maximal number of messages in the outbound bridge queue. Once we reach this limit, we
+/// send a "congestion" XCM message to the sending chain.
+const OUTBOUND_LANE_CONGESTED_THRESHOLD: MessageNonce = 8_192;
+
+/// After we have sent "congestion" XCM message to the sending chain, we wait until number
+/// of messages in the outbound bridge queue drops to this count, before sending `uncongestion`
+/// XCM message.
+const OUTBOUND_LANE_UNCONGESTED_THRESHOLD: MessageNonce = 1_024;
+
+impl<H: XcmBlobHauler> LocalXcmQueueManager<H> {
+	/// Must be called whenever we push a message to the bridge lane.
+	pub fn on_bridge_message_enqueued(
+		sender_and_lane: &SenderAndLane,
+		enqueued_messages: MessageNonce,
+	) {
+		// skip if we dont want to handle congestion
+		if !H::supports_congestion_detection() {
+			return
+		}
+
+		// if we have already sent the congestion signal, we don't want to do anything
+		if Self::is_congested_signal_sent(sender_and_lane.lane) {
+			return
+		}
+
+		// if the bridge queue is not congested, we don't want to do anything
+		let is_congested = enqueued_messages > OUTBOUND_LANE_CONGESTED_THRESHOLD;
+		if !is_congested {
+			return
+		}
+
+		log::info!(
+			target: crate::LOG_TARGET_BRIDGE_DISPATCH,
+			"Sending 'congested' XCM message to {:?} to avoid overloading lane {:?}: there are\
+			{} messages queued at the bridge queue",
+			sender_and_lane.location,
+			sender_and_lane.lane,
+			enqueued_messages,
+		);
+
+		if let Err(e) = Self::send_congested_signal(sender_and_lane) {
+			log::info!(
+				target: crate::LOG_TARGET_BRIDGE_DISPATCH,
+				"Failed to send the 'congested' XCM message to {:?}: {:?}",
+				sender_and_lane.location,
+				e,
+			);
+		}
+	}
+
+	/// Must be called whenever we receive a message delivery confirmation.
+	pub fn on_bridge_messages_delivered(
+		sender_and_lane: &SenderAndLane,
+		enqueued_messages: MessageNonce,
+	) {
+		// skip if we dont want to handle congestion
+		if !H::supports_congestion_detection() {
+			return
+		}
+
+		// if we have not sent the congestion signal before, we don't want to do anything
+		if !Self::is_congested_signal_sent(sender_and_lane.lane) {
+			return
+		}
+
+		// if the bridge queue is still congested, we don't want to do anything
+		let is_congested = enqueued_messages > OUTBOUND_LANE_UNCONGESTED_THRESHOLD;
+		if is_congested {
+			return
+		}
+
+		log::info!(
+			target: crate::LOG_TARGET_BRIDGE_DISPATCH,
+			"Sending 'uncongested' XCM message to {:?}. Lane {:?}: there are\
+			{} messages queued at the bridge queue",
+			sender_and_lane.location,
+			sender_and_lane.lane,
+			enqueued_messages,
+		);
+
+		if let Err(e) = Self::send_uncongested_signal(sender_and_lane) {
+			log::info!(
+				target: crate::LOG_TARGET_BRIDGE_DISPATCH,
+				"Failed to send the 'uncongested' XCM message to {:?}: {:?}",
+				sender_and_lane.location,
+				e,
+			);
+		}
+	}
+
+	/// Returns true if we have sent "congested" signal to the `sending_chain_location`.
+	fn is_congested_signal_sent(lane: LaneId) -> bool {
+		OutboundLanesCongestedSignals::<H::Runtime, H::MessagesInstance>::get(lane)
+	}
+
+	/// Send congested signal to the `sending_chain_location`.
+	fn send_congested_signal(sender_and_lane: &SenderAndLane) -> Result<(), SendError> {
+		if let Some(msg) = H::CongestedMessage::get() {
+			send_xcm::<H::ToSourceChainSender>(sender_and_lane.location, msg)?;
+			OutboundLanesCongestedSignals::<H::Runtime, H::MessagesInstance>::insert(
+				sender_and_lane.lane,
+				true,
+			);
+		}
+		Ok(())
+	}
+
+	/// Send `uncongested` signal to the `sending_chain_location`.
+	fn send_uncongested_signal(sender_and_lane: &SenderAndLane) -> Result<(), SendError> {
+		if let Some(msg) = H::UncongestedMessage::get() {
+			send_xcm::<H::ToSourceChainSender>(sender_and_lane.location, msg)?;
+			OutboundLanesCongestedSignals::<H::Runtime, H::MessagesInstance>::remove(
+				sender_and_lane.lane,
+			);
+		}
+		Ok(())
+	}
+}
+
+#[cfg(test)]
+mod tests {
+	use super::*;
+	use crate::mock::*;
+
+	use bp_messages::OutboundLaneData;
+	use frame_support::parameter_types;
+	use pallet_bridge_messages::OutboundLanes;
+
+	parameter_types! {
+		pub TestSenderAndLane: SenderAndLane = SenderAndLane {
+			location: MultiLocation::new(1, X1(Parachain(1000))),
+			lane: TEST_LANE_ID,
+		};
+		pub DummyXcmMessage: Xcm<()> = Xcm::new();
+	}
+
+	struct DummySendXcm;
+
+	impl DummySendXcm {
+		fn messages_sent() -> u32 {
+			frame_support::storage::unhashed::get(b"DummySendXcm").unwrap_or(0)
+		}
+	}
+
+	impl SendXcm for DummySendXcm {
+		type Ticket = ();
+
+		fn validate(
+			_destination: &mut Option<MultiLocation>,
+			_message: &mut Option<Xcm<()>>,
+		) -> SendResult<Self::Ticket> {
+			Ok(((), Default::default()))
+		}
+
+		fn deliver(_ticket: Self::Ticket) -> Result<XcmHash, SendError> {
+			let messages_sent: u32 = Self::messages_sent();
+			frame_support::storage::unhashed::put(b"DummySendXcm", &(messages_sent + 1));
+			Ok(XcmHash::default())
+		}
+	}
+
+	struct TestBlobHauler;
+
+	impl XcmBlobHauler for TestBlobHauler {
+		type Runtime = TestRuntime;
+		type MessagesInstance = ();
+		type SenderAndLane = TestSenderAndLane;
+
+		type ToSourceChainSender = DummySendXcm;
+		type CongestedMessage = DummyXcmMessage;
+		type UncongestedMessage = DummyXcmMessage;
+	}
+
+	type TestBlobHaulerAdapter = XcmBlobHaulerAdapter<TestBlobHauler>;
+
+	fn fill_up_lane_to_congestion() {
+		OutboundLanes::<TestRuntime, ()>::insert(
+			TEST_LANE_ID,
+			OutboundLaneData {
+				oldest_unpruned_nonce: 0,
+				latest_received_nonce: 0,
+				latest_generated_nonce: OUTBOUND_LANE_CONGESTED_THRESHOLD,
+			},
+		);
+	}
+
+	#[test]
+	fn congested_signal_is_not_sent_twice() {
+		run_test(|| {
+			fill_up_lane_to_congestion();
+
+			// next sent message leads to congested signal
+			TestBlobHaulerAdapter::haul_blob(vec![42]).unwrap();
+			assert_eq!(DummySendXcm::messages_sent(), 1);
+
+			// next sent message => we don't sent another congested signal
+			TestBlobHaulerAdapter::haul_blob(vec![42]).unwrap();
+			assert_eq!(DummySendXcm::messages_sent(), 1);
+		});
+	}
+
+	#[test]
+	fn congested_signal_is_not_sent_when_outbound_lane_is_not_congested() {
+		run_test(|| {
+			TestBlobHaulerAdapter::haul_blob(vec![42]).unwrap();
+			assert_eq!(DummySendXcm::messages_sent(), 0);
+		});
+	}
+
+	#[test]
+	fn congested_signal_is_sent_when_outbound_lane_is_congested() {
+		run_test(|| {
+			fill_up_lane_to_congestion();
+
+			// next sent message leads to congested signal
+			TestBlobHaulerAdapter::haul_blob(vec![42]).unwrap();
+			assert_eq!(DummySendXcm::messages_sent(), 1);
+			assert!(LocalXcmQueueManager::<TestBlobHauler>::is_congested_signal_sent(TEST_LANE_ID));
+		});
+	}
+
+	#[test]
+	fn uncongested_signal_is_not_sent_when_messages_are_delivered_at_other_lane() {
+		run_test(|| {
+			LocalXcmQueueManager::<TestBlobHauler>::send_congested_signal(&TestSenderAndLane::get()).unwrap();
+			assert_eq!(DummySendXcm::messages_sent(), 1);
+
+			// when we receive a delivery report for other lane, we don't send an uncongested signal
+			TestBlobHaulerAdapter::on_messages_delivered(LaneId([42, 42, 42, 42]), 0);
+			assert_eq!(DummySendXcm::messages_sent(), 1);
+		});
+	}
+
+	#[test]
+	fn uncongested_signal_is_not_sent_when_we_havent_send_congested_signal_before() {
+		run_test(|| {
+			TestBlobHaulerAdapter::on_messages_delivered(TEST_LANE_ID, 0);
+			assert_eq!(DummySendXcm::messages_sent(), 0);
+		});
+	}
+
+	#[test]
+	fn uncongested_signal_is_not_sent_if_outbound_lane_is_still_congested() {
+		run_test(|| {
+			LocalXcmQueueManager::<TestBlobHauler>::send_congested_signal(&TestSenderAndLane::get()).unwrap();
+			assert_eq!(DummySendXcm::messages_sent(), 1);
+
+			TestBlobHaulerAdapter::on_messages_delivered(
+				TEST_LANE_ID,
+				OUTBOUND_LANE_UNCONGESTED_THRESHOLD + 1,
+			);
+			assert_eq!(DummySendXcm::messages_sent(), 1);
+		});
+	}
+
+	#[test]
+	fn uncongested_signal_is_sent_if_outbound_lane_is_uncongested() {
+		run_test(|| {
+			LocalXcmQueueManager::<TestBlobHauler>::send_congested_signal(&TestSenderAndLane::get()).unwrap();
+			assert_eq!(DummySendXcm::messages_sent(), 1);
+
+			TestBlobHaulerAdapter::on_messages_delivered(
+				TEST_LANE_ID,
+				OUTBOUND_LANE_UNCONGESTED_THRESHOLD,
+			);
+			assert_eq!(DummySendXcm::messages_sent(), 2);
+		});
+	}
+}
diff --git a/bridges/bin/runtime-common/src/mock.rs b/bridges/bin/runtime-common/src/mock.rs
index 6b5edabc886..9c41d17fa99 100644
--- a/bridges/bin/runtime-common/src/mock.rs
+++ b/bridges/bin/runtime-common/src/mock.rs
@@ -33,10 +33,15 @@ use crate::messages::{
 };
 
 use bp_header_chain::{ChainWithGrandpa, HeaderChain};
-use bp_messages::{target_chain::ForbidInboundMessages, LaneId, MessageNonce};
+use bp_messages::{
+	target_chain::{DispatchMessage, MessageDispatch},
+	LaneId, MessageNonce,
+};
 use bp_parachains::SingleParaStoredHeaderDataBuilder;
 use bp_relayers::PayRewardFromAccount;
-use bp_runtime::{Chain, ChainId, Parachain, UnderlyingChainProvider};
+use bp_runtime::{
+	messages::MessageDispatchResult, Chain, ChainId, Parachain, UnderlyingChainProvider,
+};
 use codec::{Decode, Encode};
 use frame_support::{
 	parameter_types,
@@ -245,9 +250,10 @@ impl pallet_bridge_messages::Config for TestRuntime {
 		(),
 		ConstU64<100_000>,
 	>;
+	type OnMessagesDelivered = ();
 
 	type SourceHeaderChain = SourceHeaderChainAdapter<OnThisChainBridge>;
-	type MessageDispatch = ForbidInboundMessages<(), FromBridgedChainMessagePayload>;
+	type MessageDispatch = DummyMessageDispatch;
 	type BridgedChainId = BridgedChainId;
 }
 
@@ -259,6 +265,34 @@ impl pallet_bridge_relayers::Config for TestRuntime {
 	type WeightInfo = ();
 }
 
+/// Dummy message dispatcher.
+pub struct DummyMessageDispatch;
+
+impl DummyMessageDispatch {
+	pub fn deactivate() {
+		frame_support::storage::unhashed::put(&b"inactive"[..], &false);
+	}
+}
+
+impl MessageDispatch for DummyMessageDispatch {
+	type DispatchPayload = Vec<u8>;
+	type DispatchLevelResult = ();
+
+	fn is_active() -> bool {
+		frame_support::storage::unhashed::take::<bool>(&b"inactive"[..]) != Some(false)
+	}
+
+	fn dispatch_weight(_message: &mut DispatchMessage<Self::DispatchPayload>) -> Weight {
+		Weight::zero()
+	}
+
+	fn dispatch(
+		_: DispatchMessage<Self::DispatchPayload>,
+	) -> MessageDispatchResult<Self::DispatchLevelResult> {
+		MessageDispatchResult { unspent_weight: Weight::zero(), dispatch_level_result: () }
+	}
+}
+
 /// Bridge that is deployed on `ThisChain` and allows sending/receiving messages to/from
 /// `BridgedChain`.
 #[derive(Debug, PartialEq, Eq)]
diff --git a/bridges/bin/runtime-common/src/refund_relayer_extension.rs b/bridges/bin/runtime-common/src/refund_relayer_extension.rs
index c5419837316..f611686420c 100644
--- a/bridges/bin/runtime-common/src/refund_relayer_extension.rs
+++ b/bridges/bin/runtime-common/src/refund_relayer_extension.rs
@@ -494,8 +494,7 @@ where
 		};
 
 		// compute total number of messages in transaction
-		let bundled_messages =
-			parsed_call.messages_call_info().bundled_messages().checked_len().unwrap_or(0);
+		let bundled_messages = parsed_call.messages_call_info().bundled_messages().saturating_len();
 
 		// a quick check to avoid invalid high-priority transactions
 		if bundled_messages > Runtime::MaxUnconfirmedMessagesAtInboundLane::get() {
diff --git a/bridges/modules/messages/src/lib.rs b/bridges/modules/messages/src/lib.rs
index 91ab3c965b9..67c6fb23cf1 100644
--- a/bridges/modules/messages/src/lib.rs
+++ b/bridges/modules/messages/src/lib.rs
@@ -53,7 +53,8 @@ use crate::{
 
 use bp_messages::{
 	source_chain::{
-		DeliveryConfirmationPayments, LaneMessageVerifier, SendMessageArtifacts, TargetHeaderChain,
+		DeliveryConfirmationPayments, LaneMessageVerifier, OnMessagesDelivered,
+		SendMessageArtifacts, TargetHeaderChain,
 	},
 	target_chain::{
 		DeliveryPayments, DispatchMessage, MessageDispatch, ProvedLaneMessages, ProvedMessages,
@@ -63,7 +64,9 @@ use bp_messages::{
 	MessagePayload, MessagesOperatingMode, OutboundLaneData, OutboundMessageDetails,
 	UnrewardedRelayersState, VerificationError,
 };
-use bp_runtime::{BasicOperatingMode, ChainId, OwnedBridgeModule, PreComputedSize, Size};
+use bp_runtime::{
+	BasicOperatingMode, ChainId, OwnedBridgeModule, PreComputedSize, RangeInclusiveExt, Size,
+};
 use codec::{Decode, Encode, MaxEncodedLen};
 use frame_support::{dispatch::PostDispatchInfo, ensure, fail, traits::Get, DefaultNoBound};
 use sp_runtime::traits::UniqueSaturatedFrom;
@@ -156,6 +159,8 @@ pub mod pallet {
 		type LaneMessageVerifier: LaneMessageVerifier<Self::OutboundPayload>;
 		/// Delivery confirmation payments.
 		type DeliveryConfirmationPayments: DeliveryConfirmationPayments<Self::AccountId>;
+		/// Delivery confirmation callback.
+		type OnMessagesDelivered: OnMessagesDelivered;
 
 		// Types that are used by inbound_lane (on target chain).
 
@@ -281,6 +286,9 @@ pub mod pallet {
 				Error::<T, I>::TooManyMessagesInTheProof
 			);
 
+			// if message dispatcher is currently inactive, we won't accept any messages
+			ensure!(T::MessageDispatch::is_active(), Error::<T, I>::MessageDispatchInactive);
+
 			// why do we need to know the weight of this (`receive_messages_proof`) call? Because
 			// we may want to return some funds for not-dispatching (or partially dispatching) some
 			// messages to the call origin (relayer). And this is done by returning actual weight
@@ -487,6 +495,12 @@ pub mod pallet {
 				lane_id,
 			);
 
+			// notify others about messages delivery
+			T::OnMessagesDelivered::on_messages_delivered(
+				lane_id,
+				lane.data().queued_messages().saturating_len(),
+			);
+
 			// because of lags, the inbound lane state (`lane_data`) may have entries for
 			// already rewarded relayers and messages (if all entries are duplicated, then
 			// this transaction must be filtered out by our signed extension)
@@ -518,6 +532,8 @@ pub mod pallet {
 		NotOperatingNormally,
 		/// The outbound lane is inactive.
 		InactiveOutboundLane,
+		/// The inbound message dispatcher is inactive.
+		MessageDispatchInactive,
 		/// Message has been treated as invalid by chain verifier.
 		MessageRejectedByChainVerifier(VerificationError),
 		/// Message has been treated as invalid by lane verifier.
@@ -580,6 +596,25 @@ pub mod pallet {
 		MaxValues = MaybeOutboundLanesCount<T, I>,
 	>;
 
+	/// Map of lane id => is congested signal sent. It is managed by the
+	/// `bridge_runtime_common::LocalXcmQueueManager`.
+	///
+	/// **bridges-v1**: this map is a temporary hack and will be dropped in the `v2`. We can emulate
+	/// a storage map using `sp_io::unhashed` storage functions, but then benchmarks are not
+	/// accounting its `proof_size`, so it is missing from the final weights. So we need to make it
+	/// a map inside some pallet. We could use a simply value instead of map here, because
+	/// in `v1` we'll only have a single lane. But in the case of adding another lane before `v2`,
+	/// it'll be easier to deal with the isolated storage map instead.
+	#[pallet::storage]
+	pub type OutboundLanesCongestedSignals<T: Config<I>, I: 'static = ()> = StorageMap<
+		Hasher = Blake2_128Concat,
+		Key = LaneId,
+		Value = bool,
+		QueryKind = ValueQuery,
+		OnEmpty = GetDefault,
+		MaxValues = MaybeOutboundLanesCount<T, I>,
+	>;
+
 	/// All queued outbound messages.
 	#[pallet::storage]
 	pub type OutboundMessages<T: Config<I>, I: 'static = ()> =
@@ -627,6 +662,11 @@ pub mod pallet {
 			}
 		}
 
+		/// Return outbound lane data.
+		pub fn outbound_lane_data(lane: LaneId) -> OutboundLaneData {
+			OutboundLanes::<T, I>::get(lane)
+		}
+
 		/// Return inbound lane data.
 		pub fn inbound_lane_data(lane: LaneId) -> InboundLaneData<T::InboundRelayer> {
 			InboundLanes::<T, I>::get(lane).0
@@ -703,6 +743,9 @@ fn send_message<T: Config<I>, I: 'static>(
 		.send_message(encoded_payload)
 		.map_err(Error::<T, I>::MessageRejectedByPallet)?;
 
+	// return number of messages in the queue to let sender know about its state
+	let enqueued_messages = lane.data().queued_messages().saturating_len();
+
 	log::trace!(
 		target: LOG_TARGET,
 		"Accepted message {} to lane {:?}. Message size: {:?}",
@@ -713,7 +756,7 @@ fn send_message<T: Config<I>, I: 'static>(
 
 	Pallet::<T, I>::deposit_event(Event::MessageAccepted { lane_id, nonce });
 
-	Ok(SendMessageArtifacts { nonce })
+	Ok(SendMessageArtifacts { nonce, enqueued_messages })
 }
 
 /// Ensure that the pallet is in normal operational mode.
@@ -881,8 +924,9 @@ mod tests {
 		mock::{
 			inbound_unrewarded_relayers_state, message, message_payload, run_test,
 			unrewarded_relayer, AccountId, DbWeight, RuntimeEvent as TestEvent, RuntimeOrigin,
-			TestDeliveryConfirmationPayments, TestDeliveryPayments, TestMessagesDeliveryProof,
-			TestMessagesProof, TestRelayer, TestRuntime, TestWeightInfo, MAX_OUTBOUND_PAYLOAD_SIZE,
+			TestDeliveryConfirmationPayments, TestDeliveryPayments, TestMessageDispatch,
+			TestMessagesDeliveryProof, TestMessagesProof, TestOnMessagesDelivered, TestRelayer,
+			TestRuntime, TestWeightInfo, MAX_OUTBOUND_PAYLOAD_SIZE,
 			PAYLOAD_REJECTED_BY_TARGET_CHAIN, REGULAR_PAYLOAD, TEST_LANE_ID, TEST_LANE_ID_2,
 			TEST_LANE_ID_3, TEST_RELAYER_A, TEST_RELAYER_B,
 		},
@@ -908,10 +952,12 @@ mod tests {
 	fn send_regular_message() {
 		get_ready_for_events();
 
-		let message_nonce =
-			outbound_lane::<TestRuntime, ()>(TEST_LANE_ID).data().latest_generated_nonce + 1;
-		send_message::<TestRuntime, ()>(TEST_LANE_ID, REGULAR_PAYLOAD)
+		let outbound_lane = outbound_lane::<TestRuntime, ()>(TEST_LANE_ID);
+		let message_nonce = outbound_lane.data().latest_generated_nonce + 1;
+		let prev_enqueud_messages = outbound_lane.data().queued_messages().saturating_len();
+		let artifacts = send_message::<TestRuntime, ()>(TEST_LANE_ID, REGULAR_PAYLOAD)
 			.expect("send_message has failed");
+		assert_eq!(artifacts.enqueued_messages, prev_enqueud_messages + 1);
 
 		// check event with assigned nonce
 		assert_eq!(
@@ -1201,6 +1247,23 @@ mod tests {
 		});
 	}
 
+	#[test]
+	fn receive_messages_fails_if_dispatcher_is_inactive() {
+		run_test(|| {
+			TestMessageDispatch::deactivate();
+			assert_noop!(
+				Pallet::<TestRuntime>::receive_messages_proof(
+					RuntimeOrigin::signed(1),
+					TEST_RELAYER_A,
+					Ok(vec![message(1, REGULAR_PAYLOAD)]).into(),
+					1,
+					REGULAR_PAYLOAD.declared_weight,
+				),
+				Error::<TestRuntime, ()>::MessageDispatchInactive,
+			);
+		});
+	}
+
 	#[test]
 	fn receive_messages_proof_does_not_accept_message_if_dispatch_weight_is_not_enough() {
 		run_test(|| {
@@ -1304,6 +1367,7 @@ mod tests {
 			);
 			assert!(TestDeliveryConfirmationPayments::is_reward_paid(TEST_RELAYER_A, 1));
 			assert!(!TestDeliveryConfirmationPayments::is_reward_paid(TEST_RELAYER_B, 1));
+			assert_eq!(TestOnMessagesDelivered::call_arguments(), Some((TEST_LANE_ID, 1)));
 
 			// this reports delivery of both message 1 and message 2 => reward is paid only to
 			// TEST_RELAYER_B
@@ -1346,6 +1410,7 @@ mod tests {
 			);
 			assert!(!TestDeliveryConfirmationPayments::is_reward_paid(TEST_RELAYER_A, 1));
 			assert!(TestDeliveryConfirmationPayments::is_reward_paid(TEST_RELAYER_B, 1));
+			assert_eq!(TestOnMessagesDelivered::call_arguments(), Some((TEST_LANE_ID, 0)));
 		});
 	}
 
diff --git a/bridges/modules/messages/src/mock.rs b/bridges/modules/messages/src/mock.rs
index 83752523efb..67f7b78a487 100644
--- a/bridges/modules/messages/src/mock.rs
+++ b/bridges/modules/messages/src/mock.rs
@@ -21,7 +21,9 @@ use crate::Config;
 
 use bp_messages::{
 	calc_relayers_rewards,
-	source_chain::{DeliveryConfirmationPayments, LaneMessageVerifier, TargetHeaderChain},
+	source_chain::{
+		DeliveryConfirmationPayments, LaneMessageVerifier, OnMessagesDelivered, TargetHeaderChain,
+	},
 	target_chain::{
 		DeliveryPayments, DispatchMessage, DispatchMessageData, MessageDispatch,
 		ProvedLaneMessages, ProvedMessages, SourceHeaderChain,
@@ -161,6 +163,7 @@ impl Config for TestRuntime {
 	type TargetHeaderChain = TestTargetHeaderChain;
 	type LaneMessageVerifier = TestLaneMessageVerifier;
 	type DeliveryConfirmationPayments = TestDeliveryConfirmationPayments;
+	type OnMessagesDelivered = TestOnMessagesDelivered;
 
 	type SourceHeaderChain = TestSourceHeaderChain;
 	type MessageDispatch = TestMessageDispatch;
@@ -402,14 +405,26 @@ impl SourceHeaderChain for TestSourceHeaderChain {
 	}
 }
 
-/// Source header chain that is used in tests.
+/// Test message dispatcher.
 #[derive(Debug)]
 pub struct TestMessageDispatch;
 
+impl TestMessageDispatch {
+	pub fn deactivate() {
+		frame_support::storage::unhashed::put(b"TestMessageDispatch.IsCongested", &true)
+	}
+}
+
 impl MessageDispatch for TestMessageDispatch {
 	type DispatchPayload = TestPayload;
 	type DispatchLevelResult = TestDispatchLevelResult;
 
+	fn is_active() -> bool {
+		!frame_support::storage::unhashed::get_or_default::<bool>(
+			b"TestMessageDispatch.IsCongested",
+		)
+	}
+
 	fn dispatch_weight(message: &mut DispatchMessage<TestPayload>) -> Weight {
 		match message.data.payload.as_ref() {
 			Ok(payload) => payload.declared_weight,
@@ -427,6 +442,24 @@ impl MessageDispatch for TestMessageDispatch {
 	}
 }
 
+/// Test callback, called during message delivery confirmation transaction.
+pub struct TestOnMessagesDelivered;
+
+impl TestOnMessagesDelivered {
+	pub fn call_arguments() -> Option<(LaneId, MessageNonce)> {
+		frame_support::storage::unhashed::get(b"TestOnMessagesDelivered.OnMessagesDelivered")
+	}
+}
+
+impl OnMessagesDelivered for TestOnMessagesDelivered {
+	fn on_messages_delivered(lane: LaneId, enqueued_messages: MessageNonce) {
+		frame_support::storage::unhashed::put(
+			b"TestOnMessagesDelivered.OnMessagesDelivered",
+			&(lane, enqueued_messages),
+		);
+	}
+}
+
 /// Return test lane message with given nonce and payload.
 pub fn message(nonce: MessageNonce, payload: TestPayload) -> Message {
 	Message { key: MessageKey { lane_id: TEST_LANE_ID, nonce }, payload: payload.encode() }
diff --git a/bridges/modules/xcm-bridge-hub-router/Cargo.toml b/bridges/modules/xcm-bridge-hub-router/Cargo.toml
new file mode 100644
index 00000000000..3d13e7cc3d7
--- /dev/null
+++ b/bridges/modules/xcm-bridge-hub-router/Cargo.toml
@@ -0,0 +1,59 @@
+[package]
+name = "pallet-xcm-bridge-hub-router"
+description = "Bridge hub interface for sibling/parent chains with dynamic fees support."
+version = "0.1.0"
+authors = ["Parity Technologies <admin@parity.io>"]
+edition = "2021"
+license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
+
+[dependencies]
+codec = { package = "parity-scale-codec", version = "3.1.5", default-features = false }
+log = { version = "0.4.19", default-features = false }
+scale-info = { version = "2.8.0", default-features = false, features = ["bit-vec", "derive", "serde"] }
+
+# Bridge dependencies
+
+bp-xcm-bridge-hub-router = { path = "../../primitives/xcm-bridge-hub-router", default-features = false }
+
+# Substrate Dependencies
+
+frame-benchmarking = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false, optional = true }
+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-core = { 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 }
+
+# Polkadot Dependencies
+
+xcm = { git = "https://github.com/paritytech/polkadot", branch = "master", default-features = false }
+xcm-builder = { git = "https://github.com/paritytech/polkadot", branch = "master", default-features = false }
+
+[dev-dependencies]
+sp-io = { git = "https://github.com/paritytech/substrate", branch = "master" }
+sp-std = { git = "https://github.com/paritytech/substrate", branch = "master" }
+
+[features]
+default = ["std"]
+std = [
+	"bp-xcm-bridge-hub-router/std",
+	"codec/std",
+	"frame-benchmarking/std",
+	"frame-support/std",
+	"frame-system/std",
+	"log/std",
+	"scale-info/std",
+	"sp-core/std",
+	"sp-runtime/std",
+	"sp-std/std",
+	"xcm/std",
+	"xcm-builder/std",
+]
+runtime-benchmarks = [
+	"frame-benchmarking/runtime-benchmarks",
+	"xcm-builder/runtime-benchmarks",
+]
+try-runtime = [
+	"frame-support/try-runtime",
+	"frame-system/try-runtime",
+]
diff --git a/bridges/modules/xcm-bridge-hub-router/src/benchmarking.rs b/bridges/modules/xcm-bridge-hub-router/src/benchmarking.rs
new file mode 100644
index 00000000000..b32b983daf7
--- /dev/null
+++ b/bridges/modules/xcm-bridge-hub-router/src/benchmarking.rs
@@ -0,0 +1,96 @@
+// Copyright 2019-2021 Parity Technologies (UK) Ltd.
+// This file is part of Parity Bridges Common.
+
+// Parity Bridges Common is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Parity Bridges Common is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Parity Bridges Common.  If not, see <http://www.gnu.org/licenses/>.
+
+//! XCM bridge hub router pallet benchmarks.
+
+#![cfg(feature = "runtime-benchmarks")]
+
+use crate::{Bridge, Call};
+
+use bp_xcm_bridge_hub_router::{BridgeState, MINIMAL_DELIVERY_FEE_FACTOR};
+use frame_benchmarking::benchmarks_instance_pallet;
+use frame_support::{
+	dispatch::UnfilteredDispatchable,
+	traits::{EnsureOrigin, Get, Hooks},
+};
+use sp_runtime::traits::Zero;
+use xcm::prelude::*;
+
+/// Pallet we're benchmarking here.
+pub struct Pallet<T: Config<I>, I: 'static = ()>(crate::Pallet<T, I>);
+
+/// Trait that must be implemented by runtime to be able to benchmark pallet properly.
+pub trait Config<I: 'static>: crate::Config<I> {
+	/// Fill up queue so it becomes congested.
+	fn make_congested();
+
+	/// Returns destination which is valid for this router instance.
+	/// (Needs to pass `T::Bridges`)
+	/// Make sure that `SendXcm` will pass.
+	fn ensure_bridged_target_destination() -> MultiLocation {
+		MultiLocation::new(
+			Self::UniversalLocation::get().len() as u8,
+			X1(GlobalConsensus(Self::BridgedNetworkId::get().unwrap())),
+		)
+	}
+}
+
+benchmarks_instance_pallet! {
+	on_initialize_when_non_congested {
+		Bridge::<T, I>::put(BridgeState {
+			is_congested: false,
+			delivery_fee_factor: MINIMAL_DELIVERY_FEE_FACTOR + MINIMAL_DELIVERY_FEE_FACTOR,
+		});
+	}: {
+		crate::Pallet::<T, I>::on_initialize(Zero::zero())
+	}
+
+	on_initialize_when_congested {
+		Bridge::<T, I>::put(BridgeState {
+			is_congested: false,
+			delivery_fee_factor: MINIMAL_DELIVERY_FEE_FACTOR + MINIMAL_DELIVERY_FEE_FACTOR,
+		});
+		T::make_congested();
+	}: {
+		crate::Pallet::<T, I>::on_initialize(Zero::zero())
+	}
+
+	report_bridge_status {
+		Bridge::<T, I>::put(BridgeState::default());
+
+		let origin: T::RuntimeOrigin = T::BridgeHubOrigin::try_successful_origin().expect("expected valid BridgeHubOrigin");
+		let bridge_id = Default::default();
+		let is_congested = true;
+
+		let call = Call::<T, I>::report_bridge_status { bridge_id, is_congested };
+	}: { call.dispatch_bypass_filter(origin)? }
+	verify {
+		assert!(Bridge::<T, I>::get().is_congested);
+	}
+
+	send_message {
+		// make local queue congested, because it means additional db write
+		T::make_congested();
+
+		let dest = T::ensure_bridged_target_destination();
+		let xcm = sp_std::vec![].into();
+	}: {
+		send_xcm::<crate::Pallet<T, I>>(dest, xcm).expect("message is sent")
+	}
+	verify {
+		assert!(Bridge::<T, I>::get().delivery_fee_factor > MINIMAL_DELIVERY_FEE_FACTOR);
+	}
+}
diff --git a/bridges/modules/xcm-bridge-hub-router/src/lib.rs b/bridges/modules/xcm-bridge-hub-router/src/lib.rs
new file mode 100644
index 00000000000..87e050b45c7
--- /dev/null
+++ b/bridges/modules/xcm-bridge-hub-router/src/lib.rs
@@ -0,0 +1,557 @@
+// Copyright 2019-2021 Parity Technologies (UK) Ltd.
+// This file is part of Parity Bridges Common.
+
+// Parity Bridges Common is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Parity Bridges Common is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Parity Bridges Common.  If not, see <http://www.gnu.org/licenses/>.
+
+//! 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<I: 'static = ()>: frame_system::Config {
+		/// Benchmarks results from runtime we're plugged into.
+		type WeightInfo: WeightInfo;
+
+		/// Universal location of this runtime.
+		type UniversalLocation: Get<InteriorMultiLocation>;
+		/// 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<Option<NetworkId>>;
+		/// 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;
+
+		/// Origin of the sibling bridge hub that is allowed to report bridge status.
+		type BridgeHubOrigin: EnsureOrigin<Self::RuntimeOrigin>;
+		/// 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<u128>;
+		/// Asset that is used to paid bridge fee.
+		type FeeAsset: Get<AssetId>;
+	}
+
+	#[pallet::pallet]
+	pub struct Pallet<T, I = ()>(PhantomData<(T, I)>);
+
+	#[pallet::hooks]
+	impl<T: Config<I>, I: 'static> Hooks<BlockNumberFor<T>> for Pallet<T, I> {
+		fn on_initialize(_n: BlockNumberFor<T>) -> 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::<T, I>::put(bridge);
+			T::WeightInfo::on_initialize_when_non_congested()
+		}
+	}
+
+	#[pallet::call]
+	impl<T: Config<I>, I: 'static> Pallet<T, I> {
+		/// Notification about congested bridge queue.
+		#[pallet::call_index(0)]
+		#[pallet::weight(T::WeightInfo::report_bridge_status())]
+		pub fn report_bridge_status(
+			origin: OriginFor<T>,
+			// 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::<T, I>::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<T: Config<I>, I: 'static = ()> = StorageValue<_, BridgeState, ValueQuery>;
+
+	impl<T: Config<I>, I: 'static> Pallet<T, I> {
+		/// 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::<T, I>::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<T, I> = SovereignPaidRemoteExporter<
+	Pallet<T, I>,
+	<T as Config<I>>::ToBridgeHubSender,
+	<T as Config<I>>::UniversalLocation,
+>;
+
+// This pallet acts as the `ExporterFor` for the `SovereignPaidRemoteExporter` to compute
+// message fee using fee factor.
+impl<T: Config<I>, I: 'static> ExporterFor for Pallet<T, I> {
+	fn exporter_for(
+		network: &NetworkId,
+		remote_location: &InteriorMultiLocation,
+		message: &Xcm<()>,
+	) -> Option<(MultiLocation, Option<MultiAsset>)> {
+		// 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 {
+				MultiAsset { 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<T: Config<I>, I: 'static> SendXcm for Pallet<T, I> {
+	type Ticket = (u32, <T::ToBridgeHubSender as SendXcm>::Ticket);
+
+	fn validate(
+		dest: &mut Option<MultiLocation>,
+		xcm: &mut Option<Xcm<()>>,
+	) -> SendResult<Self::Ticket> {
+		// we won't have an access to `dest` and `xcm` in the `delvier` method, so precompute
+		// everything required here
+		let message_size = xcm
+			.as_ref()
+			.map(|xcm| xcm.encoded_size() as _)
+			.ok_or(SendError::MissingArgument)?;
+
+		// 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)
+		}
+
+		// 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 bridg hub delivery (returned by
+		// the `Config::ToBridgeHubSender`) and (2) to-bridged bridge hub delivery (returned by
+		// `Self::exporter_for`)
+		ViaBridgeHubExporter::<T, I>::validate(dest, xcm)
+			.map(|(ticket, cost)| ((message_size, ticket), cost))
+	}
+
+	fn deliver(ticket: Self::Ticket) -> Result<XcmHash, SendError> {
+		// 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::<T, I>::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 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::<TestRuntime, ()>::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::<TestRuntime, ()>::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::<TestRuntime, ()>::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::<TestRuntime, ()>::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::<XcmBridgeHubRouter>(
+					MultiLocation::new(2, X2(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::<XcmBridgeHubRouter>(
+					MultiLocation::new(2, X2(GlobalConsensus(Rococo), Parachain(1000))),
+					vec![ClearOrigin; HARD_MESSAGE_SIZE_LIMIT as usize].into(),
+				),
+				Err(SendError::ExceedsMaxMessageSize),
+			);
+		});
+	}
+
+	#[test]
+	fn returns_proper_delivery_price() {
+		run_test(|| {
+			let dest = MultiLocation::new(2, X1(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), &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::<TestRuntime, ()>::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_eq!(
+				send_xcm::<XcmBridgeHubRouter>(
+					MultiLocation::new(
+						2,
+						X2(GlobalConsensus(BridgedNetworkId::get()), Parachain(1000))
+					),
+					vec![ClearOrigin].into(),
+				)
+				.map(drop),
+				Ok(()),
+			);
+
+			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_eq!(
+				send_xcm::<XcmBridgeHubRouter>(
+					MultiLocation::new(
+						2,
+						X2(GlobalConsensus(BridgedNetworkId::get()), Parachain(1000))
+					),
+					vec![ClearOrigin].into(),
+				)
+				.map(drop),
+				Ok(()),
+			);
+
+			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::<TestRuntime, ()>::put(congested_bridge(MINIMAL_DELIVERY_FEE_FACTOR));
+
+			let old_bridge = XcmBridgeHubRouter::bridge();
+			assert_eq!(
+				send_xcm::<XcmBridgeHubRouter>(
+					MultiLocation::new(
+						2,
+						X2(GlobalConsensus(BridgedNetworkId::get()), Parachain(1000))
+					),
+					vec![ClearOrigin].into(),
+				)
+				.map(drop),
+				Ok(()),
+			);
+
+			assert!(TestToBridgeHubSender::is_message_sent());
+			assert!(
+				old_bridge.delivery_fee_factor < XcmBridgeHubRouter::bridge().delivery_fee_factor
+			);
+		});
+	}
+}
diff --git a/bridges/modules/xcm-bridge-hub-router/src/mock.rs b/bridges/modules/xcm-bridge-hub-router/src/mock.rs
new file mode 100644
index 00000000000..5ad7be4890a
--- /dev/null
+++ b/bridges/modules/xcm-bridge-hub-router/src/mock.rs
@@ -0,0 +1,148 @@
+// Copyright 2019-2021 Parity Technologies (UK) Ltd.
+// This file is part of Parity Bridges Common.
+
+// Parity Bridges Common is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Parity Bridges Common is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Parity Bridges Common.  If not, see <http://www.gnu.org/licenses/>.
+
+#![cfg(test)]
+
+use crate as pallet_xcm_bridge_hub_router;
+
+use bp_xcm_bridge_hub_router::XcmChannelStatusProvider;
+use frame_support::{construct_runtime, parameter_types};
+use frame_system::EnsureRoot;
+use sp_core::H256;
+use sp_runtime::{
+	traits::{BlakeTwo256, ConstU128, IdentityLookup},
+	BuildStorage,
+};
+use xcm::prelude::*;
+use xcm_builder::NetworkExportTable;
+
+pub type AccountId = u64;
+type Block = frame_system::mocking::MockBlock<TestRuntime>;
+
+/// HRMP fee.
+pub const HRMP_FEE: u128 = 500;
+/// Base bridge fee.
+pub const BASE_FEE: u128 = 1_000_000;
+/// Byte bridge fee.
+pub const BYTE_FEE: u128 = 1_000;
+
+construct_runtime! {
+	pub enum TestRuntime
+	{
+		System: frame_system::{Pallet, Call, Config<T>, Storage, Event<T>},
+		XcmBridgeHubRouter: pallet_xcm_bridge_hub_router::{Pallet, Storage},
+	}
+}
+
+parameter_types! {
+	pub ThisNetworkId: NetworkId = Polkadot;
+	pub BridgedNetworkId: NetworkId = Kusama;
+	pub UniversalLocation: InteriorMultiLocation = X2(GlobalConsensus(ThisNetworkId::get()), Parachain(1000));
+	pub SiblingBridgeHubLocation: MultiLocation = ParentThen(X1(Parachain(1002))).into();
+	pub BridgeFeeAsset: AssetId = MultiLocation::parent().into();
+	pub BridgeTable: Vec<(NetworkId, MultiLocation, Option<MultiAsset>)>
+		= vec![(BridgedNetworkId::get(), SiblingBridgeHubLocation::get(), Some((BridgeFeeAsset::get(), BASE_FEE).into()))];
+}
+
+impl frame_system::Config for TestRuntime {
+	type RuntimeOrigin = RuntimeOrigin;
+	type Nonce = u64;
+	type RuntimeCall = RuntimeCall;
+	type Block = Block;
+	type Hash = H256;
+	type Hashing = BlakeTwo256;
+	type AccountId = AccountId;
+	type Lookup = IdentityLookup<Self::AccountId>;
+	type RuntimeEvent = RuntimeEvent;
+	type BlockHashCount = frame_support::traits::ConstU64<250>;
+	type Version = ();
+	type PalletInfo = PalletInfo;
+	type AccountData = ();
+	type OnNewAccount = ();
+	type OnKilledAccount = ();
+	type BaseCallFilter = frame_support::traits::Everything;
+	type SystemWeightInfo = ();
+	type BlockWeights = ();
+	type BlockLength = ();
+	type DbWeight = ();
+	type SS58Prefix = ();
+	type OnSetCode = ();
+	type MaxConsumers = frame_support::traits::ConstU32<16>;
+}
+
+impl pallet_xcm_bridge_hub_router::Config<()> for TestRuntime {
+	type WeightInfo = ();
+
+	type UniversalLocation = UniversalLocation;
+	type BridgedNetworkId = BridgedNetworkId;
+	type Bridges = NetworkExportTable<BridgeTable>;
+
+	type BridgeHubOrigin = EnsureRoot<AccountId>;
+	type ToBridgeHubSender = TestToBridgeHubSender;
+	type WithBridgeHubChannel = TestWithBridgeHubChannel;
+
+	type ByteFee = ConstU128<BYTE_FEE>;
+	type FeeAsset = BridgeFeeAsset;
+}
+
+pub struct TestToBridgeHubSender;
+
+impl TestToBridgeHubSender {
+	pub fn is_message_sent() -> bool {
+		frame_support::storage::unhashed::get_or_default(b"TestToBridgeHubSender.Sent")
+	}
+}
+
+impl SendXcm for TestToBridgeHubSender {
+	type Ticket = ();
+
+	fn validate(
+		_destination: &mut Option<MultiLocation>,
+		_message: &mut Option<Xcm<()>>,
+	) -> SendResult<Self::Ticket> {
+		Ok(((), (BridgeFeeAsset::get(), HRMP_FEE).into()))
+	}
+
+	fn deliver(_ticket: Self::Ticket) -> Result<XcmHash, SendError> {
+		frame_support::storage::unhashed::put(b"TestToBridgeHubSender.Sent", &true);
+		Ok([0u8; 32])
+	}
+}
+
+pub struct TestWithBridgeHubChannel;
+
+impl TestWithBridgeHubChannel {
+	pub fn make_congested() {
+		frame_support::storage::unhashed::put(b"TestWithBridgeHubChannel.Congested", &true);
+	}
+}
+
+impl XcmChannelStatusProvider for TestWithBridgeHubChannel {
+	fn is_congested() -> bool {
+		frame_support::storage::unhashed::get_or_default(b"TestWithBridgeHubChannel.Congested")
+	}
+}
+
+/// Return test externalities to use in tests.
+pub fn new_test_ext() -> sp_io::TestExternalities {
+	let t = frame_system::GenesisConfig::<TestRuntime>::default().build_storage().unwrap();
+	sp_io::TestExternalities::new(t)
+}
+
+/// Run pallet test.
+pub fn run_test<T>(test: impl FnOnce() -> T) -> T {
+	new_test_ext().execute_with(|| test())
+}
diff --git a/bridges/modules/xcm-bridge-hub-router/src/weights.rs b/bridges/modules/xcm-bridge-hub-router/src/weights.rs
new file mode 100644
index 00000000000..04909f3b2e8
--- /dev/null
+++ b/bridges/modules/xcm-bridge-hub-router/src/weights.rs
@@ -0,0 +1,208 @@
+// Copyright 2019-2021 Parity Technologies (UK) Ltd.
+// This file is part of Parity Bridges Common.
+
+// Parity Bridges Common is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Parity Bridges Common is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Parity Bridges Common.  If not, see <http://www.gnu.org/licenses/>.
+
+//! Autogenerated weights for pallet_xcm_bridge_hub_router
+//!
+//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev
+//! DATE: 2023-08-03, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]`
+//! WORST CASE MAP SIZE: `1000000`
+//! HOSTNAME: `covid`, CPU: `11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz`
+//! EXECUTION: , WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024
+
+// Executed Command:
+// target/release/millau-bridge-node
+// benchmark
+// pallet
+// --chain=dev
+// --steps=50
+// --repeat=20
+// --pallet=pallet_xcm_bridge_hub_router
+// --extrinsic=*
+// --execution=wasm
+// --wasm-execution=Compiled
+// --heap-pages=4096
+// --output=./modules/xcm-bridge-hub-router/src/weights.rs
+// --template=./.maintain/bridge-weight-template.hbs
+
+#![allow(clippy::all)]
+#![allow(unused_parens)]
+#![allow(unused_imports)]
+#![allow(missing_docs)]
+
+use frame_support::{
+	traits::Get,
+	weights::{constants::RocksDbWeight, Weight},
+};
+use sp_std::marker::PhantomData;
+
+/// Weight functions needed for pallet_xcm_bridge_hub_router.
+pub trait WeightInfo {
+	fn on_initialize_when_non_congested() -> Weight;
+	fn on_initialize_when_congested() -> Weight;
+	fn report_bridge_status() -> Weight;
+	fn send_message() -> Weight;
+}
+
+/// Weights for `pallet_xcm_bridge_hub_router` that are generated using one of the Bridge testnets.
+///
+/// Those weights are test only and must never be used in production.
+pub struct BridgeWeight<T>(PhantomData<T>);
+impl<T: frame_system::Config> WeightInfo for BridgeWeight<T> {
+	/// Storage: `XcmBridgeHubRouter::Bridge` (r:1 w:1)
+	///
+	/// Proof: `XcmBridgeHubRouter::Bridge` (`max_values`: Some(1), `max_size`: Some(17), added:
+	/// 512, mode: `MaxEncodedLen`)
+	///
+	/// Storage: UNKNOWN KEY `0x456d756c617465645369626c696e6758636d704368616e6e656c2e436f6e6765`
+	/// (r:1 w:0)
+	///
+	/// Proof: UNKNOWN KEY `0x456d756c617465645369626c696e6758636d704368616e6e656c2e436f6e6765` (r:1
+	/// w:0)
+	fn on_initialize_when_non_congested() -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `53`
+		//  Estimated: `3518`
+		// Minimum execution time: 11_934 nanoseconds.
+		Weight::from_parts(12_201_000, 3518)
+			.saturating_add(T::DbWeight::get().reads(2_u64))
+			.saturating_add(T::DbWeight::get().writes(1_u64))
+	}
+	/// Storage: `XcmBridgeHubRouter::Bridge` (r:1 w:1)
+	///
+	/// Proof: `XcmBridgeHubRouter::Bridge` (`max_values`: Some(1), `max_size`: Some(17), added:
+	/// 512, mode: `MaxEncodedLen`)
+	///
+	/// Storage: UNKNOWN KEY `0x456d756c617465645369626c696e6758636d704368616e6e656c2e436f6e6765`
+	/// (r:1 w:0)
+	///
+	/// Proof: UNKNOWN KEY `0x456d756c617465645369626c696e6758636d704368616e6e656c2e436f6e6765` (r:1
+	/// w:0)
+	fn on_initialize_when_congested() -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `94`
+		//  Estimated: `3559`
+		// Minimum execution time: 9_010 nanoseconds.
+		Weight::from_parts(9_594_000, 3559)
+			.saturating_add(T::DbWeight::get().reads(2_u64))
+			.saturating_add(T::DbWeight::get().writes(1_u64))
+	}
+	/// Storage: `XcmBridgeHubRouter::Bridge` (r:1 w:1)
+	///
+	/// Proof: `XcmBridgeHubRouter::Bridge` (`max_values`: Some(1), `max_size`: Some(17), added:
+	/// 512, mode: `MaxEncodedLen`)
+	fn report_bridge_status() -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `53`
+		//  Estimated: `1502`
+		// Minimum execution time: 10_427 nanoseconds.
+		Weight::from_parts(10_682_000, 1502)
+			.saturating_add(T::DbWeight::get().reads(1_u64))
+			.saturating_add(T::DbWeight::get().writes(1_u64))
+	}
+	/// Storage: `XcmBridgeHubRouter::Bridge` (r:1 w:1)
+	///
+	/// Proof: `XcmBridgeHubRouter::Bridge` (`max_values`: Some(1), `max_size`: Some(17), added:
+	/// 512, mode: `MaxEncodedLen`)
+	///
+	/// Storage: UNKNOWN KEY `0x456d756c617465645369626c696e6758636d704368616e6e656c2e436f6e6765`
+	/// (r:1 w:0)
+	///
+	/// Proof: UNKNOWN KEY `0x456d756c617465645369626c696e6758636d704368616e6e656c2e436f6e6765` (r:1
+	/// w:0)
+	fn send_message() -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `52`
+		//  Estimated: `3517`
+		// Minimum execution time: 19_709 nanoseconds.
+		Weight::from_parts(20_110_000, 3517)
+			.saturating_add(T::DbWeight::get().reads(2_u64))
+			.saturating_add(T::DbWeight::get().writes(1_u64))
+	}
+}
+
+// For backwards compatibility and tests
+impl WeightInfo for () {
+	/// Storage: `XcmBridgeHubRouter::Bridge` (r:1 w:1)
+	///
+	/// Proof: `XcmBridgeHubRouter::Bridge` (`max_values`: Some(1), `max_size`: Some(17), added:
+	/// 512, mode: `MaxEncodedLen`)
+	///
+	/// Storage: UNKNOWN KEY `0x456d756c617465645369626c696e6758636d704368616e6e656c2e436f6e6765`
+	/// (r:1 w:0)
+	///
+	/// Proof: UNKNOWN KEY `0x456d756c617465645369626c696e6758636d704368616e6e656c2e436f6e6765` (r:1
+	/// w:0)
+	fn on_initialize_when_non_congested() -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `53`
+		//  Estimated: `3518`
+		// Minimum execution time: 11_934 nanoseconds.
+		Weight::from_parts(12_201_000, 3518)
+			.saturating_add(RocksDbWeight::get().reads(2_u64))
+			.saturating_add(RocksDbWeight::get().writes(1_u64))
+	}
+	/// Storage: `XcmBridgeHubRouter::Bridge` (r:1 w:1)
+	///
+	/// Proof: `XcmBridgeHubRouter::Bridge` (`max_values`: Some(1), `max_size`: Some(17), added:
+	/// 512, mode: `MaxEncodedLen`)
+	///
+	/// Storage: UNKNOWN KEY `0x456d756c617465645369626c696e6758636d704368616e6e656c2e436f6e6765`
+	/// (r:1 w:0)
+	///
+	/// Proof: UNKNOWN KEY `0x456d756c617465645369626c696e6758636d704368616e6e656c2e436f6e6765` (r:1
+	/// w:0)
+	fn on_initialize_when_congested() -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `94`
+		//  Estimated: `3559`
+		// Minimum execution time: 9_010 nanoseconds.
+		Weight::from_parts(9_594_000, 3559)
+			.saturating_add(RocksDbWeight::get().reads(2_u64))
+			.saturating_add(RocksDbWeight::get().writes(1_u64))
+	}
+	/// Storage: `XcmBridgeHubRouter::Bridge` (r:1 w:1)
+	///
+	/// Proof: `XcmBridgeHubRouter::Bridge` (`max_values`: Some(1), `max_size`: Some(17), added:
+	/// 512, mode: `MaxEncodedLen`)
+	fn report_bridge_status() -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `53`
+		//  Estimated: `1502`
+		// Minimum execution time: 10_427 nanoseconds.
+		Weight::from_parts(10_682_000, 1502)
+			.saturating_add(RocksDbWeight::get().reads(1_u64))
+			.saturating_add(RocksDbWeight::get().writes(1_u64))
+	}
+	/// Storage: `XcmBridgeHubRouter::Bridge` (r:1 w:1)
+	///
+	/// Proof: `XcmBridgeHubRouter::Bridge` (`max_values`: Some(1), `max_size`: Some(17), added:
+	/// 512, mode: `MaxEncodedLen`)
+	///
+	/// Storage: UNKNOWN KEY `0x456d756c617465645369626c696e6758636d704368616e6e656c2e436f6e6765`
+	/// (r:1 w:0)
+	///
+	/// Proof: UNKNOWN KEY `0x456d756c617465645369626c696e6758636d704368616e6e656c2e436f6e6765` (r:1
+	/// w:0)
+	fn send_message() -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `52`
+		//  Estimated: `3517`
+		// Minimum execution time: 19_709 nanoseconds.
+		Weight::from_parts(20_110_000, 3517)
+			.saturating_add(RocksDbWeight::get().reads(2_u64))
+			.saturating_add(RocksDbWeight::get().writes(1_u64))
+	}
+}
diff --git a/bridges/primitives/chain-asset-hub-kusama/Cargo.toml b/bridges/primitives/chain-asset-hub-kusama/Cargo.toml
new file mode 100644
index 00000000000..6d5a4207ee6
--- /dev/null
+++ b/bridges/primitives/chain-asset-hub-kusama/Cargo.toml
@@ -0,0 +1,26 @@
+[package]
+name = "bp-asset-hub-kusama"
+description = "Primitives of AssetHubKusama parachain runtime."
+version = "0.1.0"
+authors = ["Parity Technologies <admin@parity.io>"]
+edition = "2021"
+license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
+
+[dependencies]
+codec = { package = "parity-scale-codec", version = "3.1.5", default-features = false }
+scale-info = { version = "2.9.0", default-features = false, features = ["derive"] }
+
+# Substrate Dependencies
+frame-support = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false }
+
+# Bridge Dependencies
+bp-xcm-bridge-hub-router = { path = "../xcm-bridge-hub-router", default-features = false }
+
+[features]
+default = ["std"]
+std = [
+	"bp-xcm-bridge-hub-router/std",
+	"frame-support/std",
+	"codec/std",
+	"scale-info/std",
+]
diff --git a/bridges/primitives/chain-asset-hub-kusama/src/lib.rs b/bridges/primitives/chain-asset-hub-kusama/src/lib.rs
new file mode 100644
index 00000000000..b3b25ba6edd
--- /dev/null
+++ b/bridges/primitives/chain-asset-hub-kusama/src/lib.rs
@@ -0,0 +1,49 @@
+// Copyright 2022 Parity Technologies (UK) Ltd.
+// This file is part of Parity Bridges Common.
+
+// Parity Bridges Common is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Parity Bridges Common is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Parity Bridges Common.  If not, see <http://www.gnu.org/licenses/>.
+
+//! Module with configuration which reflects AssetHubKusama runtime setup.
+
+#![cfg_attr(not(feature = "std"), no_std)]
+
+use codec::{Decode, Encode};
+use scale_info::TypeInfo;
+
+pub use bp_xcm_bridge_hub_router::XcmBridgeHubRouterCall;
+
+/// `AssetHubKusama` Runtime `Call` enum.
+///
+/// The enum represents a subset of possible `Call`s we can send to `AssetHubKusama` chain.
+/// Ideally this code would be auto-generated from metadata, because we want to
+/// avoid depending directly on the ENTIRE runtime just to get the encoding of `Dispatchable`s.
+///
+/// All entries here (like pretty much in the entire file) must be kept in sync with
+/// `AssetHubKusama` `construct_runtime`, so that we maintain SCALE-compatibility.
+#[allow(clippy::large_enum_variant)]
+#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone, TypeInfo)]
+pub enum Call {
+	/// `ToPolkadotXcmRouter` bridge pallet.
+	#[codec(index = 43)]
+	ToPolkadotXcmRouter(XcmBridgeHubRouterCall),
+}
+
+frame_support::parameter_types! {
+	/// Some sane weight to execute `xcm::Transact(pallet-xcm-bridge-hub-router::Call::report_bridge_status)`.
+	pub const XcmBridgeHubRouterTransactCallMaxWeight: frame_support::weights::Weight = frame_support::weights::Weight::from_parts(200_000_000, 6144);
+
+	/// Base delivery fee to `BridgeHubKusama`.
+	/// (initially was calculated `170733333` + `10%` by test `BridgeHubKusama::can_calculate_weight_for_paid_export_message_with_reserve_transfer`)
+	pub const BridgeHubKusamaBaseFeeInDots: u128 = 187806666;
+}
diff --git a/bridges/primitives/chain-asset-hub-polkadot/Cargo.toml b/bridges/primitives/chain-asset-hub-polkadot/Cargo.toml
new file mode 100644
index 00000000000..4ab562c6b34
--- /dev/null
+++ b/bridges/primitives/chain-asset-hub-polkadot/Cargo.toml
@@ -0,0 +1,26 @@
+[package]
+name = "bp-asset-hub-polkadot"
+description = "Primitives of AssetHubPolkadot parachain runtime."
+version = "0.1.0"
+authors = ["Parity Technologies <admin@parity.io>"]
+edition = "2021"
+license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
+
+[dependencies]
+codec = { package = "parity-scale-codec", version = "3.1.5", default-features = false }
+scale-info = { version = "2.9.0", default-features = false, features = ["derive"] }
+
+# Substrate Dependencies
+frame-support = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false }
+
+# Bridge Dependencies
+bp-xcm-bridge-hub-router = { path = "../xcm-bridge-hub-router", default-features = false }
+
+[features]
+default = ["std"]
+std = [
+	"bp-xcm-bridge-hub-router/std",
+	"frame-support/std",
+	"codec/std",
+	"scale-info/std",
+]
diff --git a/bridges/primitives/chain-asset-hub-polkadot/src/lib.rs b/bridges/primitives/chain-asset-hub-polkadot/src/lib.rs
new file mode 100644
index 00000000000..7363e5af02a
--- /dev/null
+++ b/bridges/primitives/chain-asset-hub-polkadot/src/lib.rs
@@ -0,0 +1,49 @@
+// Copyright 2022 Parity Technologies (UK) Ltd.
+// This file is part of Parity Bridges Common.
+
+// Parity Bridges Common is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Parity Bridges Common is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Parity Bridges Common.  If not, see <http://www.gnu.org/licenses/>.
+
+//! Module with configuration which reflects AssetHubPolkadot runtime setup.
+
+#![cfg_attr(not(feature = "std"), no_std)]
+
+use codec::{Decode, Encode};
+use scale_info::TypeInfo;
+
+pub use bp_xcm_bridge_hub_router::XcmBridgeHubRouterCall;
+
+/// `AssetHubPolkadot` Runtime `Call` enum.
+///
+/// The enum represents a subset of possible `Call`s we can send to `AssetHubPolkadot` chain.
+/// Ideally this code would be auto-generated from metadata, because we want to
+/// avoid depending directly on the ENTIRE runtime just to get the encoding of `Dispatchable`s.
+///
+/// All entries here (like pretty much in the entire file) must be kept in sync with
+/// `AssetHubPolkadot` `construct_runtime`, so that we maintain SCALE-compatibility.
+#[allow(clippy::large_enum_variant)]
+#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone, TypeInfo)]
+pub enum Call {
+	/// `ToKusamaXcmRouter` bridge pallet.
+	#[codec(index = 43)]
+	ToKusamaXcmRouter(XcmBridgeHubRouterCall),
+}
+
+frame_support::parameter_types! {
+	/// Some sane weight to execute `xcm::Transact(pallet-xcm-bridge-hub-router::Call::report_bridge_status)`.
+	pub const XcmBridgeHubRouterTransactCallMaxWeight: frame_support::weights::Weight = frame_support::weights::Weight::from_parts(200_000_000, 6144);
+
+	/// Base delivery fee to `BridgeHubPolkadot`.
+	/// (initially was calculated `51220000` + `10%` by test `BridgeHubPolkadot::can_calculate_weight_for_paid_export_message_with_reserve_transfer`)
+	pub const BridgeHubPolkadotBaseFeeInDots: u128 = 56342000;
+}
diff --git a/bridges/primitives/messages/src/lib.rs b/bridges/primitives/messages/src/lib.rs
index cb3a14572d7..84c41de3b36 100644
--- a/bridges/primitives/messages/src/lib.rs
+++ b/bridges/primitives/messages/src/lib.rs
@@ -387,6 +387,14 @@ impl Default for OutboundLaneData {
 	}
 }
 
+impl OutboundLaneData {
+	/// Return nonces of all currently queued messages (i.e. messages that we believe
+	/// are not delivered yet).
+	pub fn queued_messages(&self) -> RangeInclusive<MessageNonce> {
+		(self.latest_received_nonce + 1)..=self.latest_generated_nonce
+	}
+}
+
 /// Calculate the number of messages that the relayers have delivered.
 pub fn calc_relayers_rewards<AccountId>(
 	messages_relayers: VecDeque<UnrewardedRelayer<AccountId>>,
diff --git a/bridges/primitives/messages/src/source_chain.rs b/bridges/primitives/messages/src/source_chain.rs
index 09f6396ad74..3cc78522baf 100644
--- a/bridges/primitives/messages/src/source_chain.rs
+++ b/bridges/primitives/messages/src/source_chain.rs
@@ -115,11 +115,26 @@ impl<AccountId> DeliveryConfirmationPayments<AccountId> for () {
 	}
 }
 
+/// Callback that is called at the source chain (bridge hub) when we get delivery confirmation
+/// for new messages.
+pub trait OnMessagesDelivered {
+	/// New messages delivery has been confirmed.
+	///
+	/// The only argument of the function is the number of yet undelivered messages
+	fn on_messages_delivered(lane: LaneId, enqueued_messages: MessageNonce);
+}
+
+impl OnMessagesDelivered for () {
+	fn on_messages_delivered(_lane: LaneId, _enqueued_messages: MessageNonce) {}
+}
+
 /// Send message artifacts.
 #[derive(Eq, RuntimeDebug, PartialEq)]
 pub struct SendMessageArtifacts {
 	/// Nonce of the message.
 	pub nonce: MessageNonce,
+	/// Number of enqueued messages at the lane, after the message is sent.
+	pub enqueued_messages: MessageNonce,
 }
 
 /// Messages bridge API to be used from other pallets.
@@ -141,7 +156,7 @@ impl<Payload> MessagesBridge<Payload> for NoopMessagesBridge {
 	type Error = &'static str;
 
 	fn send_message(_lane: LaneId, _message: Payload) -> Result<SendMessageArtifacts, Self::Error> {
-		Ok(SendMessageArtifacts { nonce: 0 })
+		Ok(SendMessageArtifacts { nonce: 0, enqueued_messages: 0 })
 	}
 }
 
diff --git a/bridges/primitives/messages/src/target_chain.rs b/bridges/primitives/messages/src/target_chain.rs
index 385bc4ac373..60f5c1b3c1c 100644
--- a/bridges/primitives/messages/src/target_chain.rs
+++ b/bridges/primitives/messages/src/target_chain.rs
@@ -91,6 +91,15 @@ pub trait MessageDispatch {
 	/// Fine-grained result of single message dispatch (for better diagnostic purposes)
 	type DispatchLevelResult: Clone + sp_std::fmt::Debug + Eq;
 
+	/// Returns `true` if dispatcher is ready to accept additional messages. The `false` should
+	/// be treated as a hint by both dispatcher and its consumers - i.e. dispatcher shall not
+	/// simply drop messages if it returns `false`. The consumer may still call the `dispatch`
+	/// if dispatcher has returned `false`.
+	///
+	/// We check it in the messages delivery transaction prologue. So if it becomes `false`
+	/// after some portion of messages is already dispatched, it doesn't fail the whole transaction.
+	fn is_active() -> bool;
+
 	/// Estimate dispatch weight.
 	///
 	/// This function must return correct upper bound of dispatch weight. The return value
@@ -186,6 +195,10 @@ impl<MessagesProof, DispatchPayload: Decode> MessageDispatch
 	type DispatchPayload = DispatchPayload;
 	type DispatchLevelResult = ();
 
+	fn is_active() -> bool {
+		false
+	}
+
 	fn dispatch_weight(_message: &mut DispatchMessage<Self::DispatchPayload>) -> Weight {
 		Weight::MAX
 	}
diff --git a/bridges/primitives/xcm-bridge-hub-router/Cargo.toml b/bridges/primitives/xcm-bridge-hub-router/Cargo.toml
new file mode 100644
index 00000000000..ca17f52d169
--- /dev/null
+++ b/bridges/primitives/xcm-bridge-hub-router/Cargo.toml
@@ -0,0 +1,24 @@
+[package]
+name = "bp-xcm-bridge-hub-router"
+description = "Primitives of the xcm-bridge-hub fee pallet."
+version = "0.1.0"
+authors = ["Parity Technologies <admin@parity.io>"]
+edition = "2021"
+license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
+
+[dependencies]
+codec = { package = "parity-scale-codec", version = "3.1.5", default-features = false, features = ["derive", "bit-vec"] }
+scale-info = { version = "2.9.0", default-features = false, features = ["bit-vec", "derive"] }
+
+# Substrate Dependencies
+sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false }
+sp-core = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false }
+
+[features]
+default = ["std"]
+std = [
+	"codec/std",
+	"scale-info/std",
+	"sp-runtime/std",
+	"sp-core/std",
+]
diff --git a/bridges/primitives/xcm-bridge-hub-router/src/lib.rs b/bridges/primitives/xcm-bridge-hub-router/src/lib.rs
new file mode 100644
index 00000000000..0dd329f9e4a
--- /dev/null
+++ b/bridges/primitives/xcm-bridge-hub-router/src/lib.rs
@@ -0,0 +1,66 @@
+// Copyright 2019-2021 Parity Technologies (UK) Ltd.
+// This file is part of Parity Bridges Common.
+
+// Parity Bridges Common is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Parity Bridges Common is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Parity Bridges Common.  If not, see <http://www.gnu.org/licenses/>.
+
+//! Primitives of the `xcm-bridge-hub-router` pallet.
+
+#![cfg_attr(not(feature = "std"), no_std)]
+
+use codec::{Decode, Encode, MaxEncodedLen};
+use scale_info::TypeInfo;
+use sp_core::H256;
+use sp_runtime::{FixedU128, RuntimeDebug};
+
+/// Minimal delivery fee factor.
+pub const MINIMAL_DELIVERY_FEE_FACTOR: FixedU128 = FixedU128::from_u32(1);
+
+/// XCM channel status provider that may report whether it is congested or not.
+///
+/// By channel we mean the physical channel that is used to deliver messages of one
+/// of the bridge queues.
+pub trait XcmChannelStatusProvider {
+	/// Returns true if the channel is currently congested.
+	fn is_congested() -> bool;
+}
+
+impl XcmChannelStatusProvider for () {
+	fn is_congested() -> bool {
+		false
+	}
+}
+
+/// Current status of the bridge.
+#[derive(Clone, Decode, Encode, Eq, PartialEq, TypeInfo, MaxEncodedLen, RuntimeDebug)]
+pub struct BridgeState {
+	/// Current delivery fee factor.
+	pub delivery_fee_factor: FixedU128,
+	/// Bridge congestion flag.
+	pub is_congested: bool,
+}
+
+impl Default for BridgeState {
+	fn default() -> BridgeState {
+		BridgeState { delivery_fee_factor: MINIMAL_DELIVERY_FEE_FACTOR, is_congested: false }
+	}
+}
+
+/// A minimized version of `pallet-xcm-bridge-hub-router::Call` that can be used without a runtime.
+#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone, TypeInfo)]
+#[allow(non_camel_case_types)]
+pub enum XcmBridgeHubRouterCall {
+	/// `pallet-xcm-bridge-hub-router::Call::report_bridge_status`
+	#[codec(index = 0)]
+	report_bridge_status { bridge_id: H256, is_congested: bool },
+}
-- 
GitLab