diff --git a/bridges/bin/runtime-common/Cargo.toml b/bridges/bin/runtime-common/Cargo.toml
index 44799c1b5332c3cb3a61abcfa6f11a7bf6cc5576..1b819194341439cd1fa87070c64a93b6cdb3feb7 100644
--- a/bridges/bin/runtime-common/Cargo.toml
+++ b/bridges/bin/runtime-common/Cargo.toml
@@ -22,6 +22,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 = { path = "../../primitives/xcm-bridge-hub", 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 }
@@ -58,6 +59,7 @@ std = [
 	"bp-polkadot-core/std",
 	"bp-relayers/std",
 	"bp-runtime/std",
+	"bp-xcm-bridge-hub/std",
 	"bp-xcm-bridge-hub-router/std",
 	"codec/std",
 	"frame-support/std",
diff --git a/bridges/bin/runtime-common/src/messages_xcm_extension.rs b/bridges/bin/runtime-common/src/messages_xcm_extension.rs
index 77c23db3b2ba2d01aef0d2c45a20377eaf8ea129..0159ede64813626d384ba85436ef23ee5716f8ca 100644
--- a/bridges/bin/runtime-common/src/messages_xcm_extension.rs
+++ b/bridges/bin/runtime-common/src/messages_xcm_extension.rs
@@ -22,26 +22,23 @@
 //! `XcmRouter` <- `MessageDispatch` <- `InboundMessageQueue`
 
 use bp_messages::{
-	source_chain::{MessagesBridge, OnMessagesDelivered},
+	source_chain::OnMessagesDelivered,
 	target_chain::{DispatchMessage, MessageDispatch},
 	LaneId, MessageNonce,
 };
 use bp_runtime::messages::MessageDispatchResult;
+pub use bp_xcm_bridge_hub::XcmAsPlainPayload;
 use bp_xcm_bridge_hub_router::XcmChannelStatusProvider;
 use codec::{Decode, Encode};
 use frame_support::{traits::Get, weights::Weight, CloneNoBound, EqNoBound, PartialEqNoBound};
 use pallet_bridge_messages::{
-	Config as MessagesConfig, OutboundLanesCongestedSignals, Pallet as MessagesPallet,
-	WeightInfoExt as MessagesPalletWeights,
+	Config as MessagesConfig, OutboundLanesCongestedSignals, 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
-pub type XcmAsPlainPayload = sp_std::prelude::Vec<u8>;
+use xcm_builder::{DispatchBlob, DispatchBlobError};
 
 /// Message dispatch result type for single message
 #[derive(CloneNoBound, EqNoBound, PartialEqNoBound, Encode, Decode, Debug, TypeInfo)]
@@ -123,6 +120,7 @@ impl<
 
 /// A pair of sending chain location and message lane, used by this chain to send messages
 /// over the bridge.
+#[cfg_attr(feature = "std", derive(Debug, Eq, PartialEq))]
 pub struct SenderAndLane {
 	/// Sending chain relative location.
 	pub location: MultiLocation,
@@ -144,8 +142,6 @@ pub trait XcmBlobHauler {
 	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`).
@@ -166,54 +162,25 @@ pub trait XcmBlobHauler {
 /// 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>);
+pub struct XcmBlobHaulerAdapter<XcmBlobHauler, Lanes>(
+	sp_std::marker::PhantomData<(XcmBlobHauler, Lanes)>,
+);
 
-impl<H: XcmBlobHauler> HaulBlob for XcmBlobHaulerAdapter<H>
-where
-	H::Runtime: MessagesConfig<H::MessagesInstance, OutboundPayload = XcmAsPlainPayload>,
+impl<
+		H: XcmBlobHauler,
+		Lanes: Get<sp_std::vec::Vec<(SenderAndLane, (NetworkId, InteriorMultiLocation))>>,
+	> OnMessagesDelivered for XcmBlobHaulerAdapter<H, Lanes>
 {
-	fn haul_blob(blob: sp_std::prelude::Vec<u8>) -> Result<(), HaulBlobError> {
-		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: {:?}. 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,
-					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
+		if let Some(sender_and_lane) =
+			Lanes::get().iter().find(|link| link.0.lane == lane).map(|link| &link.0)
+		{
+			// notify XCM queue manager about updated lane state
+			LocalXcmQueueManager::<H>::on_bridge_messages_delivered(
+				sender_and_lane,
+				enqueued_messages,
+			);
 		}
-
-		// notify XCM queue manager about updated lane state
-		LocalXcmQueueManager::<H>::on_bridge_messages_delivered(
-			&sender_and_lane,
-			enqueued_messages,
-		);
 	}
 }
 
@@ -356,6 +323,9 @@ mod tests {
 			location: MultiLocation::new(1, X1(Parachain(1000))),
 			lane: TEST_LANE_ID,
 		};
+		pub TestLanes: sp_std::vec::Vec<(SenderAndLane, (NetworkId, InteriorMultiLocation))> = sp_std::vec![
+			(TestSenderAndLane::get(), (NetworkId::ByGenesis([0; 32]), InteriorMultiLocation::Here))
+		];
 		pub DummyXcmMessage: Xcm<()> = Xcm::new();
 	}
 
@@ -389,37 +359,44 @@ mod tests {
 	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>;
+	type TestBlobHaulerAdapter = XcmBlobHaulerAdapter<TestBlobHauler, TestLanes>;
 
-	fn fill_up_lane_to_congestion() {
+	fn fill_up_lane_to_congestion() -> MessageNonce {
+		let latest_generated_nonce = OUTBOUND_LANE_CONGESTED_THRESHOLD;
 		OutboundLanes::<TestRuntime, ()>::insert(
 			TEST_LANE_ID,
 			OutboundLaneData {
 				oldest_unpruned_nonce: 0,
 				latest_received_nonce: 0,
-				latest_generated_nonce: OUTBOUND_LANE_CONGESTED_THRESHOLD,
+				latest_generated_nonce,
 			},
 		);
+		latest_generated_nonce
 	}
 
 	#[test]
 	fn congested_signal_is_not_sent_twice() {
 		run_test(|| {
-			fill_up_lane_to_congestion();
+			let enqueued = fill_up_lane_to_congestion();
 
 			// next sent message leads to congested signal
-			TestBlobHaulerAdapter::haul_blob(vec![42]).unwrap();
+			LocalXcmQueueManager::<TestBlobHauler>::on_bridge_message_enqueued(
+				&TestSenderAndLane::get(),
+				enqueued + 1,
+			);
 			assert_eq!(DummySendXcm::messages_sent(), 1);
 
 			// next sent message => we don't sent another congested signal
-			TestBlobHaulerAdapter::haul_blob(vec![42]).unwrap();
+			LocalXcmQueueManager::<TestBlobHauler>::on_bridge_message_enqueued(
+				&TestSenderAndLane::get(),
+				enqueued,
+			);
 			assert_eq!(DummySendXcm::messages_sent(), 1);
 		});
 	}
@@ -427,7 +404,10 @@ mod tests {
 	#[test]
 	fn congested_signal_is_not_sent_when_outbound_lane_is_not_congested() {
 		run_test(|| {
-			TestBlobHaulerAdapter::haul_blob(vec![42]).unwrap();
+			LocalXcmQueueManager::<TestBlobHauler>::on_bridge_message_enqueued(
+				&TestSenderAndLane::get(),
+				1,
+			);
 			assert_eq!(DummySendXcm::messages_sent(), 0);
 		});
 	}
@@ -435,10 +415,13 @@ mod tests {
 	#[test]
 	fn congested_signal_is_sent_when_outbound_lane_is_congested() {
 		run_test(|| {
-			fill_up_lane_to_congestion();
+			let enqueued = fill_up_lane_to_congestion();
 
 			// next sent message leads to congested signal
-			TestBlobHaulerAdapter::haul_blob(vec![42]).unwrap();
+			LocalXcmQueueManager::<TestBlobHauler>::on_bridge_message_enqueued(
+				&TestSenderAndLane::get(),
+				enqueued + 1,
+			);
 			assert_eq!(DummySendXcm::messages_sent(), 1);
 			assert!(LocalXcmQueueManager::<TestBlobHauler>::is_congested_signal_sent(TEST_LANE_ID));
 		});
diff --git a/bridges/modules/xcm-bridge-hub/Cargo.toml b/bridges/modules/xcm-bridge-hub/Cargo.toml
new file mode 100644
index 0000000000000000000000000000000000000000..151b19a527185a78dcfb9ccd86c7ebf4a5be1844
--- /dev/null
+++ b/bridges/modules/xcm-bridge-hub/Cargo.toml
@@ -0,0 +1,74 @@
+[package]
+name = "pallet-xcm-bridge-hub"
+description = "Module that adds dynamic bridges/lanes support to XCM infrastucture at the bridge hub."
+version = "0.1.0"
+authors.workspace = true
+edition.workspace = true
+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.20", default-features = false }
+scale-info = { version = "2.10.0", default-features = false, features = ["derive"] }
+
+# Bridge Dependencies
+bp-messages = { path = "../../primitives/messages", default-features = false }
+bp-runtime = { path = "../../primitives/runtime", default-features = false }
+bp-xcm-bridge-hub = { path = "../../primitives/xcm-bridge-hub", default-features = false }
+pallet-bridge-messages = { path = "../messages", default-features = false }
+bridge-runtime-common = { path = "../../bin/runtime-common", default-features = false }
+
+# Substrate Dependencies
+frame-support = { git = "https://github.com/paritytech/polkadot-sdk", branch = "master", default-features = false }
+frame-system = { git = "https://github.com/paritytech/polkadot-sdk", branch = "master", default-features = false }
+sp-core = { git = "https://github.com/paritytech/polkadot-sdk", branch = "master", default-features = false }
+sp-runtime = { git = "https://github.com/paritytech/polkadot-sdk", branch = "master", default-features = false }
+sp-std = { git = "https://github.com/paritytech/polkadot-sdk", branch = "master", default-features = false }
+
+# Polkadot Dependencies
+xcm = { package = "staging-xcm", git = "https://github.com/paritytech/polkadot-sdk", branch = "master", default-features = false }
+xcm-builder = { package = "staging-xcm-builder", git = "https://github.com/paritytech/polkadot-sdk", branch = "master", default-features = false }
+xcm-executor = { package = "staging-xcm-executor", git = "https://github.com/paritytech/polkadot-sdk", branch = "master", default-features = false }
+
+[dev-dependencies]
+bp-header-chain = { path = "../../primitives/header-chain" }
+pallet-balances = { git = "https://github.com/paritytech/polkadot-sdk", branch = "master" }
+sp-io = { git = "https://github.com/paritytech/polkadot-sdk", branch = "master" }
+
+[features]
+default = ["std"]
+std = [
+	"bp-messages/std",
+	"bp-runtime/std",
+	"bp-xcm-bridge-hub/std",
+	"bridge-runtime-common/std",
+	"codec/std",
+	"frame-support/std",
+	"frame-system/std",
+	"log/std",
+	"pallet-bridge-messages/std",
+	"scale-info/std",
+	"sp-core/std",
+	"sp-runtime/std",
+	"sp-std/std",
+	"xcm-builder/std",
+	"xcm-executor/std",
+	"xcm/std",
+]
+runtime-benchmarks = [
+	"bridge-runtime-common/runtime-benchmarks",
+	"frame-support/runtime-benchmarks",
+	"frame-system/runtime-benchmarks",
+	"pallet-balances/runtime-benchmarks",
+	"pallet-bridge-messages/runtime-benchmarks",
+	"sp-runtime/runtime-benchmarks",
+	"xcm-builder/runtime-benchmarks",
+	"xcm-executor/runtime-benchmarks",
+]
+try-runtime = [
+	"frame-support/try-runtime",
+	"frame-system/try-runtime",
+	"pallet-balances/try-runtime",
+	"pallet-bridge-messages/try-runtime",
+	"sp-runtime/try-runtime",
+]
diff --git a/bridges/modules/xcm-bridge-hub/src/exporter.rs b/bridges/modules/xcm-bridge-hub/src/exporter.rs
new file mode 100644
index 0000000000000000000000000000000000000000..445551d69343094ebceccd9f6b3298fdc52a13ce
--- /dev/null
+++ b/bridges/modules/xcm-bridge-hub/src/exporter.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/>.
+
+//! The code that allows to use the pallet (`pallet-xcm-bridge-hub`) as XCM message
+//! exporter at the sending bridge hub. Internally, it just enqueues outbound blob
+//! in the messages pallet queue.
+//!
+//! This code is executed at the source bridge hub.
+
+use crate::{Config, Pallet, LOG_TARGET};
+
+use bp_messages::source_chain::MessagesBridge;
+use bp_xcm_bridge_hub::XcmAsPlainPayload;
+use bridge_runtime_common::messages_xcm_extension::{LocalXcmQueueManager, SenderAndLane};
+use pallet_bridge_messages::{Config as BridgeMessagesConfig, Pallet as BridgeMessagesPallet};
+use xcm::prelude::*;
+use xcm_builder::{HaulBlob, HaulBlobError, HaulBlobExporter};
+use xcm_executor::traits::ExportXcm;
+
+/// An easy way to access `HaulBlobExporter`.
+pub type PalletAsHaulBlobExporter<T, I> = HaulBlobExporter<
+	DummyHaulBlob,
+	<T as Config<I>>::BridgedNetworkId,
+	<T as Config<I>>::MessageExportPrice,
+>;
+/// An easy way to access associated messages pallet.
+type MessagesPallet<T, I> = BridgeMessagesPallet<T, <T as Config<I>>::BridgeMessagesPalletInstance>;
+
+impl<T: Config<I>, I: 'static> ExportXcm for Pallet<T, I>
+where
+	T: BridgeMessagesConfig<
+		<T as Config<I>>::BridgeMessagesPalletInstance,
+		OutboundPayload = XcmAsPlainPayload,
+	>,
+{
+	type Ticket = (SenderAndLane, XcmAsPlainPayload, XcmHash);
+
+	fn validate(
+		network: NetworkId,
+		channel: u32,
+		universal_source: &mut Option<InteriorMultiLocation>,
+		destination: &mut Option<InteriorMultiLocation>,
+		message: &mut Option<Xcm<()>>,
+	) -> Result<(Self::Ticket, MultiAssets), SendError> {
+		// Find supported lane_id.
+		let sender_and_lane = Self::lane_for(
+			universal_source.as_ref().ok_or(SendError::MissingArgument)?,
+			(&network, destination.as_ref().ok_or(SendError::MissingArgument)?),
+		)
+		.ok_or(SendError::NotApplicable)?;
+
+		// check if we are able to route the message. We use existing `HaulBlobExporter` for that.
+		// It will make all required changes and will encode message properly, so that the
+		// `DispatchBlob` at the bridged bridge hub will be able to decode it
+		let ((blob, id), price) = PalletAsHaulBlobExporter::<T, I>::validate(
+			network,
+			channel,
+			universal_source,
+			destination,
+			message,
+		)?;
+
+		Ok(((sender_and_lane, blob, id), price))
+	}
+
+	fn deliver(
+		(sender_and_lane, blob, id): (SenderAndLane, XcmAsPlainPayload, XcmHash),
+	) -> Result<XcmHash, SendError> {
+		let lane_id = sender_and_lane.lane;
+		let send_result = MessagesPallet::<T, I>::send_message(lane_id, blob);
+
+		match send_result {
+			Ok(artifacts) => {
+				log::info!(
+					target: LOG_TARGET,
+					"XCM message {:?} has been enqueued at bridge {:?} with nonce {}",
+					id,
+					lane_id,
+					artifacts.nonce,
+				);
+
+				// notify XCM queue manager about updated lane state
+				LocalXcmQueueManager::<T::LanesSupport>::on_bridge_message_enqueued(
+					&sender_and_lane,
+					artifacts.enqueued_messages,
+				);
+			},
+			Err(error) => {
+				log::debug!(
+					target: LOG_TARGET,
+					"XCM message {:?} has been dropped because of bridge error {:?} on bridge {:?}",
+					id,
+					error,
+					lane_id,
+				);
+				return Err(SendError::Transport("BridgeSendError"))
+			},
+		}
+
+		Ok(id)
+	}
+}
+
+/// Dummy implementation of the `HaulBlob` trait that is never called.
+///
+/// We are using `HaulBlobExporter`, which requires `HaulBlob` implementation. It assumes that
+/// there's a single channel between two bridge hubs - `HaulBlob` only accepts the blob and nothing
+/// else. But bridge messages pallet may have a dedicated channel (lane) for every pair of bridged
+/// chains. So we are using our own `ExportXcm` implementation, but to utilize `HaulBlobExporter` we
+/// still need this `DummyHaulBlob`.
+pub struct DummyHaulBlob;
+
+impl HaulBlob for DummyHaulBlob {
+	fn haul_blob(_blob: XcmAsPlainPayload) -> Result<(), HaulBlobError> {
+		Err(HaulBlobError::Transport("DummyHaulBlob"))
+	}
+}
+
+#[cfg(test)]
+mod tests {
+	use super::*;
+	use crate::mock::*;
+	use frame_support::assert_ok;
+	use xcm_executor::traits::export_xcm;
+
+	fn universal_source() -> InteriorMultiLocation {
+		X2(GlobalConsensus(RelayNetwork::get()), Parachain(SIBLING_ASSET_HUB_ID))
+	}
+
+	fn universal_destination() -> InteriorMultiLocation {
+		BridgedDestination::get()
+	}
+
+	#[test]
+	fn export_works() {
+		run_test(|| {
+			assert_ok!(export_xcm::<XcmOverBridge>(
+				BridgedRelayNetwork::get(),
+				0,
+				universal_source(),
+				universal_destination(),
+				vec![Instruction::ClearOrigin].into(),
+			));
+		})
+	}
+
+	#[test]
+	fn export_fails_if_argument_is_missing() {
+		run_test(|| {
+			assert_eq!(
+				XcmOverBridge::validate(
+					BridgedRelayNetwork::get(),
+					0,
+					&mut None,
+					&mut Some(universal_destination()),
+					&mut Some(Vec::new().into()),
+				),
+				Err(SendError::MissingArgument),
+			);
+
+			assert_eq!(
+				XcmOverBridge::validate(
+					BridgedRelayNetwork::get(),
+					0,
+					&mut Some(universal_source()),
+					&mut None,
+					&mut Some(Vec::new().into()),
+				),
+				Err(SendError::MissingArgument),
+			);
+		})
+	}
+
+	#[test]
+	fn exporter_computes_correct_lane_id() {
+		run_test(|| {
+			let expected_lane_id = TEST_LANE_ID;
+
+			assert_eq!(
+				XcmOverBridge::validate(
+					BridgedRelayNetwork::get(),
+					0,
+					&mut Some(universal_source()),
+					&mut Some(universal_destination()),
+					&mut Some(Vec::new().into()),
+				)
+				.unwrap()
+				.0
+				 .0
+				.lane,
+				expected_lane_id,
+			);
+		})
+	}
+}
diff --git a/bridges/modules/xcm-bridge-hub/src/lib.rs b/bridges/modules/xcm-bridge-hub/src/lib.rs
new file mode 100644
index 0000000000000000000000000000000000000000..14439a4d8ffe898fb0b5970e2888eff74d30a4fe
--- /dev/null
+++ b/bridges/modules/xcm-bridge-hub/src/lib.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/>.
+
+//! Module that adds XCM support to bridge pallets.
+
+#![warn(missing_docs)]
+#![cfg_attr(not(feature = "std"), no_std)]
+
+use bridge_runtime_common::messages_xcm_extension::XcmBlobHauler;
+use pallet_bridge_messages::Config as BridgeMessagesConfig;
+use xcm::prelude::*;
+
+pub use exporter::PalletAsHaulBlobExporter;
+pub use pallet::*;
+
+mod exporter;
+mod mock;
+
+/// The target that will be used when publishing logs related to this pallet.
+pub const LOG_TARGET: &str = "runtime::bridge-xcm";
+
+#[frame_support::pallet]
+pub mod pallet {
+	use super::*;
+	use bridge_runtime_common::messages_xcm_extension::SenderAndLane;
+	use frame_support::pallet_prelude::*;
+
+	#[pallet::config]
+	#[pallet::disable_frame_system_supertrait_check]
+	pub trait Config<I: 'static = ()>:
+		BridgeMessagesConfig<Self::BridgeMessagesPalletInstance>
+	{
+		/// Runtime's universal location.
+		type UniversalLocation: Get<InteriorMultiLocation>;
+		// TODO: https://github.com/paritytech/parity-bridges-common/issues/1666 remove `ChainId` and
+		// replace it with the `NetworkId` - then we'll be able to use
+		// `T as pallet_bridge_messages::Config<T::BridgeMessagesPalletInstance>::BridgedChain::NetworkId`
+		/// Bridged network id.
+		#[pallet::constant]
+		type BridgedNetworkId: Get<NetworkId>;
+		/// Associated messages pallet instance that bridges us with the
+		/// `BridgedNetworkId` consensus.
+		type BridgeMessagesPalletInstance: 'static;
+
+		/// Price of single message export to the bridged consensus (`Self::BridgedNetworkId`).
+		type MessageExportPrice: Get<MultiAssets>;
+
+		/// Get point-to-point links with bridged consensus (`Self::BridgedNetworkId`).
+		/// (this will be replaced with dynamic on-chain bridges - `Bridges V2`)
+		type Lanes: Get<sp_std::vec::Vec<(SenderAndLane, (NetworkId, InteriorMultiLocation))>>;
+		/// Support for point-to-point links
+		/// (this will be replaced with dynamic on-chain bridges - `Bridges V2`)
+		type LanesSupport: XcmBlobHauler;
+	}
+
+	#[pallet::pallet]
+	pub struct Pallet<T, I = ()>(PhantomData<(T, I)>);
+
+	impl<T: Config<I>, I: 'static> Pallet<T, I> {
+		/// Returns dedicated/configured lane identifier.
+		pub(crate) fn lane_for(
+			source: &InteriorMultiLocation,
+			dest: (&NetworkId, &InteriorMultiLocation),
+		) -> Option<SenderAndLane> {
+			let source = source.relative_to(&T::UniversalLocation::get());
+
+			// Check that we have configured a point-to-point lane for 'source' and `dest`.
+			T::Lanes::get()
+				.into_iter()
+				.find_map(|(lane_source, (lane_dest_network, lane_dest))| {
+					if lane_source.location == source &&
+						&lane_dest_network == dest.0 &&
+						&T::BridgedNetworkId::get() == dest.0 &&
+						&lane_dest == dest.1
+					{
+						Some(lane_source)
+					} else {
+						None
+					}
+				})
+		}
+	}
+}
diff --git a/bridges/modules/xcm-bridge-hub/src/mock.rs b/bridges/modules/xcm-bridge-hub/src/mock.rs
new file mode 100644
index 0000000000000000000000000000000000000000..7766aac1fb73fa0c8f0e30b2fe77def9089340ad
--- /dev/null
+++ b/bridges/modules/xcm-bridge-hub/src/mock.rs
@@ -0,0 +1,328 @@
+// 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;
+
+use bp_messages::{
+	source_chain::LaneMessageVerifier,
+	target_chain::{DispatchMessage, MessageDispatch},
+	LaneId, OutboundLaneData, VerificationError,
+};
+use bp_runtime::{messages::MessageDispatchResult, Chain, UnderlyingChainProvider};
+use bridge_runtime_common::{
+	messages::{
+		source::TargetHeaderChainAdapter, target::SourceHeaderChainAdapter,
+		BridgedChainWithMessages, HashOf, MessageBridge, ThisChainWithMessages,
+	},
+	messages_xcm_extension::{SenderAndLane, XcmBlobHauler},
+};
+use codec::Encode;
+use frame_support::{derive_impl, parameter_types, traits::ConstU32, weights::RuntimeDbWeight};
+use sp_core::H256;
+use sp_runtime::{
+	testing::Header as SubstrateHeader,
+	traits::{BlakeTwo256, IdentityLookup},
+	AccountId32, BuildStorage,
+};
+use xcm::prelude::*;
+
+pub type AccountId = AccountId32;
+pub type Balance = u64;
+
+type Block = frame_system::mocking::MockBlock<TestRuntime>;
+
+pub const SIBLING_ASSET_HUB_ID: u32 = 2001;
+pub const THIS_BRIDGE_HUB_ID: u32 = 2002;
+pub const BRIDGED_ASSET_HUB_ID: u32 = 1001;
+pub const TEST_LANE_ID: LaneId = LaneId([0, 0, 0, 1]);
+
+frame_support::construct_runtime! {
+	pub enum TestRuntime {
+		System: frame_system::{Pallet, Call, Config<T>, Storage, Event<T>},
+		Balances: pallet_balances::{Pallet, Event<T>},
+		Messages: pallet_bridge_messages::{Pallet, Call, Event<T>},
+		XcmOverBridge: pallet_xcm_bridge_hub::{Pallet},
+	}
+}
+
+parameter_types! {
+	pub const DbWeight: RuntimeDbWeight = RuntimeDbWeight { read: 1, write: 2 };
+	pub const ExistentialDeposit: Balance = 1;
+}
+
+#[derive_impl(frame_system::config_preludes::TestDefaultConfig as frame_system::DefaultConfig)]
+impl frame_system::Config for TestRuntime {
+	type AccountId = AccountId;
+	type AccountData = pallet_balances::AccountData<Balance>;
+	type Block = Block;
+	type Lookup = IdentityLookup<Self::AccountId>;
+}
+
+#[derive_impl(pallet_balances::config_preludes::TestDefaultConfig as pallet_balances::DefaultConfig)]
+impl pallet_balances::Config for TestRuntime {
+	type AccountStore = System;
+}
+
+/// Lane message verifier that is used in tests.
+#[derive(Debug, Default)]
+pub struct TestLaneMessageVerifier;
+
+impl LaneMessageVerifier<Vec<u8>> for TestLaneMessageVerifier {
+	fn verify_message(
+		_lane: &LaneId,
+		_lane_outbound_data: &OutboundLaneData,
+		_payload: &Vec<u8>,
+	) -> Result<(), VerificationError> {
+		Ok(())
+	}
+}
+
+parameter_types! {
+	pub const ActiveOutboundLanes: &'static [LaneId] = &[TEST_LANE_ID];
+}
+
+impl pallet_bridge_messages::Config for TestRuntime {
+	type RuntimeEvent = RuntimeEvent;
+	type WeightInfo = TestMessagesWeights;
+
+	type BridgedChainId = ();
+	type ActiveOutboundLanes = ActiveOutboundLanes;
+	type MaxUnrewardedRelayerEntriesAtInboundLane = ();
+	type MaxUnconfirmedMessagesAtInboundLane = ();
+	type MaximalOutboundPayloadSize = ConstU32<2048>;
+	type OutboundPayload = Vec<u8>;
+	type InboundPayload = Vec<u8>;
+	type InboundRelayer = ();
+	type DeliveryPayments = ();
+	type TargetHeaderChain = TargetHeaderChainAdapter<OnThisChainBridge>;
+	type LaneMessageVerifier = TestLaneMessageVerifier;
+	type DeliveryConfirmationPayments = ();
+	type OnMessagesDelivered = ();
+	type SourceHeaderChain = SourceHeaderChainAdapter<OnThisChainBridge>;
+	type MessageDispatch = TestMessageDispatch;
+}
+
+pub struct TestMessagesWeights;
+
+impl pallet_bridge_messages::WeightInfo for TestMessagesWeights {
+	fn receive_single_message_proof() -> Weight {
+		Weight::zero()
+	}
+	fn receive_single_message_proof_with_outbound_lane_state() -> Weight {
+		Weight::zero()
+	}
+	fn receive_delivery_proof_for_single_message() -> Weight {
+		Weight::zero()
+	}
+	fn receive_delivery_proof_for_two_messages_by_single_relayer() -> Weight {
+		Weight::zero()
+	}
+	fn receive_delivery_proof_for_two_messages_by_two_relayers() -> Weight {
+		Weight::zero()
+	}
+
+	fn receive_two_messages_proof() -> Weight {
+		Weight::zero()
+	}
+
+	fn receive_single_message_proof_1_kb() -> Weight {
+		Weight::zero()
+	}
+
+	fn receive_single_message_proof_16_kb() -> Weight {
+		Weight::zero()
+	}
+
+	fn receive_single_message_proof_with_dispatch(_: u32) -> Weight {
+		Weight::from_parts(1, 0)
+	}
+}
+
+impl pallet_bridge_messages::WeightInfoExt for TestMessagesWeights {
+	fn expected_extra_storage_proof_size() -> u32 {
+		0
+	}
+
+	fn receive_messages_proof_overhead_from_runtime() -> Weight {
+		Weight::zero()
+	}
+
+	fn receive_messages_delivery_proof_overhead_from_runtime() -> Weight {
+		Weight::zero()
+	}
+}
+
+parameter_types! {
+	pub const RelayNetwork: NetworkId = NetworkId::Kusama;
+	pub const BridgedRelayNetwork: NetworkId = NetworkId::Polkadot;
+	pub const NonBridgedRelayNetwork: NetworkId = NetworkId::Rococo;
+	pub const BridgeReserve: Balance = 100_000;
+	pub UniversalLocation: InteriorMultiLocation = X2(
+		GlobalConsensus(RelayNetwork::get()),
+		Parachain(THIS_BRIDGE_HUB_ID),
+	);
+	pub const Penalty: Balance = 1_000;
+}
+
+impl pallet_xcm_bridge_hub::Config for TestRuntime {
+	type UniversalLocation = UniversalLocation;
+	type BridgedNetworkId = BridgedRelayNetwork;
+	type BridgeMessagesPalletInstance = ();
+
+	type MessageExportPrice = ();
+	type Lanes = TestLanes;
+	type LanesSupport = TestXcmBlobHauler;
+}
+
+parameter_types! {
+	pub TestSenderAndLane: SenderAndLane = SenderAndLane {
+		location: MultiLocation::new(1, X1(Parachain(SIBLING_ASSET_HUB_ID))),
+		lane: TEST_LANE_ID,
+	};
+	pub const BridgedDestination: InteriorMultiLocation = X1(
+		Parachain(BRIDGED_ASSET_HUB_ID)
+	);
+	pub TestLanes: sp_std::vec::Vec<(SenderAndLane, (NetworkId, InteriorMultiLocation))> = sp_std::vec![
+		(TestSenderAndLane::get(), (BridgedRelayNetwork::get(), BridgedDestination::get()))
+	];
+}
+
+pub struct TestXcmBlobHauler;
+impl XcmBlobHauler for TestXcmBlobHauler {
+	type Runtime = TestRuntime;
+	type MessagesInstance = ();
+	type ToSourceChainSender = ();
+	type CongestedMessage = ();
+	type UncongestedMessage = ();
+}
+
+pub struct ThisChain;
+
+impl Chain for ThisChain {
+	type BlockNumber = u64;
+	type Hash = H256;
+	type Hasher = BlakeTwo256;
+	type Header = SubstrateHeader;
+	type AccountId = AccountId;
+	type Balance = Balance;
+	type Nonce = u64;
+	type Signature = sp_runtime::MultiSignature;
+
+	fn max_extrinsic_size() -> u32 {
+		u32::MAX
+	}
+
+	fn max_extrinsic_weight() -> Weight {
+		Weight::MAX
+	}
+}
+
+pub struct BridgedChain;
+pub type BridgedHeaderHash = H256;
+pub type BridgedChainHeader = SubstrateHeader;
+
+impl Chain for BridgedChain {
+	type BlockNumber = u64;
+	type Hash = BridgedHeaderHash;
+	type Hasher = BlakeTwo256;
+	type Header = BridgedChainHeader;
+	type AccountId = AccountId;
+	type Balance = Balance;
+	type Nonce = u64;
+	type Signature = sp_runtime::MultiSignature;
+
+	fn max_extrinsic_size() -> u32 {
+		4096
+	}
+
+	fn max_extrinsic_weight() -> Weight {
+		Weight::MAX
+	}
+}
+
+/// Test message dispatcher.
+pub struct TestMessageDispatch;
+
+impl TestMessageDispatch {
+	pub fn deactivate(lane: LaneId) {
+		frame_support::storage::unhashed::put(&(b"inactive", lane).encode()[..], &false);
+	}
+}
+
+impl MessageDispatch for TestMessageDispatch {
+	type DispatchPayload = Vec<u8>;
+	type DispatchLevelResult = ();
+
+	fn is_active() -> bool {
+		frame_support::storage::unhashed::take::<bool>(&(b"inactive").encode()[..]) != 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: () }
+	}
+}
+
+pub struct WrappedThisChain;
+impl UnderlyingChainProvider for WrappedThisChain {
+	type Chain = ThisChain;
+}
+impl ThisChainWithMessages for WrappedThisChain {
+	type RuntimeOrigin = RuntimeOrigin;
+}
+
+pub struct WrappedBridgedChain;
+impl UnderlyingChainProvider for WrappedBridgedChain {
+	type Chain = BridgedChain;
+}
+impl BridgedChainWithMessages for WrappedBridgedChain {}
+
+pub struct BridgedHeaderChain;
+impl bp_header_chain::HeaderChain<BridgedChain> for BridgedHeaderChain {
+	fn finalized_header_state_root(
+		_hash: HashOf<WrappedBridgedChain>,
+	) -> Option<HashOf<WrappedBridgedChain>> {
+		unreachable!()
+	}
+}
+
+/// Bridge that is deployed on `ThisChain` and allows sending/receiving messages to/from
+/// `BridgedChain`.
+#[derive(Debug, PartialEq, Eq)]
+pub struct OnThisChainBridge;
+
+impl MessageBridge for OnThisChainBridge {
+	const BRIDGED_MESSAGES_PALLET_NAME: &'static str = "";
+
+	type ThisChain = WrappedThisChain;
+	type BridgedChain = WrappedBridgedChain;
+	type BridgedHeaderChain = BridgedHeaderChain;
+}
+
+/// Run pallet test.
+pub fn run_test<T>(test: impl FnOnce() -> T) -> T {
+	sp_io::TestExternalities::new(
+		frame_system::GenesisConfig::<TestRuntime>::default().build_storage().unwrap(),
+	)
+	.execute_with(test)
+}
diff --git a/bridges/primitives/xcm-bridge-hub/Cargo.toml b/bridges/primitives/xcm-bridge-hub/Cargo.toml
new file mode 100644
index 0000000000000000000000000000000000000000..761fbef46e22c3cf22fe542fe7fe529cd95f788c
--- /dev/null
+++ b/bridges/primitives/xcm-bridge-hub/Cargo.toml
@@ -0,0 +1,16 @@
+[package]
+name = "bp-xcm-bridge-hub"
+description = "Primitives of the xcm-bridge-hub pallet."
+version = "0.1.0"
+authors.workspace = true
+edition.workspace = true
+license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
+
+[dependencies]
+
+# Substrate Dependencies
+sp-std = { git = "https://github.com/paritytech/polkadot-sdk", branch = "master", default-features = false }
+
+[features]
+default = ["std"]
+std = ["sp-std/std"]
diff --git a/bridges/primitives/xcm-bridge-hub/src/lib.rs b/bridges/primitives/xcm-bridge-hub/src/lib.rs
new file mode 100644
index 0000000000000000000000000000000000000000..9745011c902d2c3949b81886c872f438678a11b8
--- /dev/null
+++ b/bridges/primitives/xcm-bridge-hub/src/lib.rs
@@ -0,0 +1,24 @@
+// 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 pallet.
+
+#![warn(missing_docs)]
+#![cfg_attr(not(feature = "std"), no_std)]
+
+/// Encoded XCM blob. We expect the bridge messages pallet to use this blob type for both inbound
+/// and outbound payloads.
+pub type XcmAsPlainPayload = sp_std::vec::Vec<u8>;