diff --git a/Cargo.lock b/Cargo.lock
index 3525a79aa883..99514b1a5d2f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2006,6 +2006,11 @@ dependencies = [
name = "bp-xcm-bridge-hub"
version = "0.2.0"
dependencies = [
+ "bp-messages",
+ "bp-runtime",
+ "frame-support",
+ "parity-scale-codec",
+ "scale-info",
"sp-std 14.0.0",
"staging-xcm",
]
@@ -11992,6 +11997,7 @@ dependencies = [
"pallet-bridge-messages",
"pallet-xcm-bridge-hub-router",
"parity-scale-codec",
+ "polkadot-parachain-primitives",
"scale-info",
"sp-core",
"sp-io",
diff --git a/bridges/modules/xcm-bridge-hub/Cargo.toml b/bridges/modules/xcm-bridge-hub/Cargo.toml
index 8fbf61a0a535..2cb8d89722e7 100644
--- a/bridges/modules/xcm-bridge-hub/Cargo.toml
+++ b/bridges/modules/xcm-bridge-hub/Cargo.toml
@@ -35,11 +35,12 @@ xcm-builder = { workspace = true }
xcm-executor = { workspace = true }
[dev-dependencies]
-bp-header-chain = { workspace = true, default-features = true }
-pallet-balances = { workspace = true, default-features = true }
-sp-io = { workspace = true, default-features = true }
+bp-header-chain = { workspace = true }
+pallet-balances = { workspace = true }
+sp-io = { workspace = true }
bp-runtime = { features = ["test-helpers"], workspace = true }
pallet-xcm-bridge-hub-router = { workspace = true }
+polkadot-parachain-primitives = { workspace = true }
[features]
default = ["std"]
diff --git a/bridges/modules/xcm-bridge-hub/src/lib.rs b/bridges/modules/xcm-bridge-hub/src/lib.rs
index 60b988497fc5..af3709b5a252 100644
--- a/bridges/modules/xcm-bridge-hub/src/lib.rs
+++ b/bridges/modules/xcm-bridge-hub/src/lib.rs
@@ -14,19 +14,61 @@
// You should have received a copy of the GNU General Public License
// along with Parity Bridges Common. If not, see .
-//! Module that adds XCM support to bridge pallets.
+//! Module that adds XCM support to bridge pallets. The pallet allows to dynamically
+//! open and close bridges between local (to this pallet location) and remote XCM
+//! destinations.
+//!
+//! Every bridge between two XCM locations has a dedicated lane in associated
+//! messages pallet. Assuming that this pallet is deployed at the bridge hub
+//! parachain and there's a similar pallet at the bridged network, the dynamic
+//! bridge lifetime is as follows:
+//!
+//! 1) the sibling parachain opens a XCMP channel with this bridge hub;
+//!
+//! 2) the sibling parachain funds its sovereign parachain account at this bridge hub. It shall hold
+//! enough funds to pay for the bridge (see `BridgeReserve`);
+//!
+//! 3) the sibling parachain opens the bridge by sending XCM `Transact` instruction with the
+//! `open_bridge` call. The `BridgeReserve` amount is reserved on the sovereign account of
+//! sibling parachain;
+//!
+//! 4) at the other side of the bridge, the same thing (1, 2, 3) happens. Parachains that need to
+//! connect over the bridge need to coordinate the moment when they start sending messages over
+//! the bridge. Otherwise they may lose messages and/or bundled assets;
+//!
+//! 5) when either side wants to close the bridge, it sends the XCM `Transact` with the
+//! `close_bridge` call. The bridge is closed immediately if there are no queued messages.
+//! Otherwise, the owner must repeat the `close_bridge` call to prune all queued messages first.
+//!
+//! The pallet doesn't provide any mechanism for graceful closure, because it always involves
+//! some contract between two connected chains and the bridge hub knows nothing about that. It
+//! is the task for the connected chains to make sure that all required actions are completed
+//! before the closure. In the end, the bridge hub can't even guarantee that all messages that
+//! are delivered to the destination, are processed in the way their sender expects. So if we
+//! can't guarantee that, we shall not care about more complex procedures and leave it to the
+//! participating parties.
#![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;
+use bp_messages::{LaneId, LaneState, MessageNonce};
+use bp_runtime::{AccountIdOf, BalanceOf, RangeInclusiveExt};
+use bp_xcm_bridge_hub::{
+ bridge_locations, Bridge, BridgeLocations, BridgeLocationsError, BridgeState,
+};
+use frame_support::traits::{Currency, ReservableCurrency};
+use frame_system::Config as SystemConfig;
+use pallet_bridge_messages::{Config as BridgeMessagesConfig, LanesManagerError};
+use sp_runtime::traits::Zero;
+use xcm::prelude::*;
+use xcm_executor::traits::ConvertLocation;
+
mod mock;
/// The target that will be used when publishing logs related to this pallet.
@@ -37,13 +79,17 @@ pub mod pallet {
use super::*;
use bridge_runtime_common::messages_xcm_extension::SenderAndLane;
use frame_support::pallet_prelude::*;
- use frame_system::pallet_prelude::BlockNumberFor;
+ use frame_system::pallet_prelude::{BlockNumberFor, *};
#[pallet::config]
#[pallet::disable_frame_system_supertrait_check]
pub trait Config:
BridgeMessagesConfig
{
+ /// The overarching event type.
+ type RuntimeEvent: From>
+ + IsType<::RuntimeEvent>;
+
/// Runtime's universal location.
type UniversalLocation: Get;
// TODO: https://github.com/paritytech/parity-bridges-common/issues/1666 remove `ChainId` and
@@ -67,8 +113,36 @@ pub mod pallet {
/// Support for point-to-point links
/// (this will be replaced with dynamic on-chain bridges - `Bridges V2`)
type LanesSupport: XcmBlobHauler;
+
+ /// A set of XCM locations within local consensus system that are allowed to open
+ /// bridges with remote destinations.
+ // TODO: there's only one impl of `EnsureOrigin` -
+ // `EnsureXcmOrigin`, but it doesn't do what we need. Is there some other way to check
+ // `Origin` and get matching `Location`???
+ type OpenBridgeOrigin: EnsureOrigin<
+ ::RuntimeOrigin,
+ Success = Location,
+ >;
+ /// A converter between a multi-location and a sovereign account.
+ type BridgeOriginAccountIdConverter: ConvertLocation;
+
+ /// Amount of this chain native tokens that is reserved on the sibling parachain account
+ /// when bridge open request is registered.
+ #[pallet::constant]
+ type BridgeReserve: Get>>;
+ /// Currency used to pay for bridge registration.
+ type NativeCurrency: ReservableCurrency;
}
+ /// An alias for the bridge metadata.
+ pub type BridgeOf = Bridge>;
+ /// An alias for the this chain.
+ pub type ThisChainOf =
+ pallet_bridge_messages::ThisChainOf>::BridgeMessagesPalletInstance>;
+ /// An alias for the associated lanes manager.
+ pub type LanesManagerOf =
+ pallet_bridge_messages::LanesManager>::BridgeMessagesPalletInstance>;
+
#[pallet::pallet]
pub struct Pallet(PhantomData<(T, I)>);
@@ -76,13 +150,311 @@ pub mod pallet {
impl, I: 'static> Hooks> for Pallet {
fn integrity_test() {
assert!(
- Self::bridged_network_id().is_some(),
- "Configured `T::BridgedNetwork`: {:?} does not contain `GlobalConsensus` junction with `NetworkId`",
- T::BridgedNetwork::get()
+ Self::bridged_network_id().is_ok(),
+ "Configured `T::BridgedNetwork`: {:?} does not contain `GlobalConsensus` junction with `NetworkId`",
+ T::BridgedNetwork::get()
+ )
+ }
+ }
+
+ #[pallet::call]
+ impl, I: 'static> Pallet
+ where
+ T: frame_system::Config>>,
+ T::NativeCurrency: Currency>>,
+ {
+ /// Open a bridge between two locations.
+ ///
+ /// The caller must be within the `T::OpenBridgeOrigin` filter (presumably: a sibling
+ /// parachain or a parent relay chain). The `bridge_destination_universal_location` must be
+ /// a destination within the consensus of the `T::BridgedNetwork` network.
+ ///
+ /// The `BridgeReserve` amount is reserved on the caller account. This reserve
+ /// is unreserved after bridge is closed.
+ ///
+ /// The states after this call: bridge is `Opened`, outbound lane is `Opened`, inbound lane
+ /// is `Opened`.
+ #[pallet::call_index(0)]
+ #[pallet::weight(Weight::zero())] // TODO: https://github.com/paritytech/parity-bridges-common/issues/1760 - weights
+ pub fn open_bridge(
+ origin: OriginFor,
+ bridge_destination_universal_location: Box,
+ ) -> DispatchResult {
+ // check and compute required bridge locations
+ let locations =
+ Self::bridge_locations(origin, bridge_destination_universal_location.clone())?;
+
+ // reserve balance on the parachain sovereign account
+ let reserve = T::BridgeReserve::get();
+ let bridge_owner_account = T::BridgeOriginAccountIdConverter::convert_location(
+ &locations.bridge_origin_relative_location,
+ )
+ .ok_or(Error::::InvalidBridgeOriginAccount)?;
+ T::NativeCurrency::reserve(&bridge_owner_account, reserve)
+ .map_err(|_| Error::::FailedToReserveBridgeReserve)?;
+
+ // save bridge metadata
+ Bridges::::try_mutate(locations.lane_id, |bridge| match bridge {
+ Some(_) => Err(Error::::BridgeAlreadyExists),
+ None => {
+ *bridge = Some(BridgeOf:: {
+ bridge_origin_relative_location: Box::new(
+ locations.bridge_origin_relative_location.clone().into(),
+ ),
+ state: BridgeState::Opened,
+ bridge_owner_account,
+ reserve,
+ });
+ Ok(())
+ },
+ })?;
+
+ // create new lanes. Under normal circumstances, following calls shall never fail
+ let lanes_manager = LanesManagerOf::::new();
+ lanes_manager
+ .create_inbound_lane(locations.lane_id)
+ .map_err(Error::::LanesManager)?;
+ lanes_manager
+ .create_outbound_lane(locations.lane_id)
+ .map_err(Error::::LanesManager)?;
+
+ // write something to log
+ log::trace!(
+ target: LOG_TARGET,
+ "Bridge {:?} between {:?} and {:?} has been opened",
+ locations.lane_id,
+ locations.bridge_origin_universal_location,
+ locations.bridge_destination_universal_location,
+ );
+
+ // deposit `BridgeOpened` event
+ Self::deposit_event(Event::::BridgeOpened {
+ lane_id: locations.lane_id,
+ local_endpoint: Box::new(locations.bridge_origin_universal_location),
+ remote_endpoint: Box::new(locations.bridge_destination_universal_location),
+ });
+
+ Ok(())
+ }
+
+ /// Try to close the bridge.
+ ///
+ /// Can only be called by the "owner" of this side of the bridge, meaning that the
+ /// inbound XCM channel with the local origin chain is working.
+ ///
+ /// Closed bridge is a bridge without any traces in the runtime storage. So this method
+ /// first tries to prune all queued messages at the outbound lane. When there are no
+ /// outbound messages left, outbound and inbound lanes are purged. After that, funds
+ /// are returned back to the owner of this side of the bridge.
+ ///
+ /// The number of messages that we may prune in a single call is limited by the
+ /// `may_prune_messages` argument. If there are more messages in the queue, the method
+ /// prunes exactly `may_prune_messages` and exits early. The caller may call it again
+ /// until outbound queue is depleted and get his funds back.
+ ///
+ /// The states after this call: everything is either `Closed`, or purged from the
+ /// runtime storage.
+ #[pallet::call_index(1)]
+ #[pallet::weight(Weight::zero())] // TODO: https://github.com/paritytech/parity-bridges-common/issues/1760 - weights
+ pub fn close_bridge(
+ origin: OriginFor,
+ bridge_destination_universal_location: Box,
+ may_prune_messages: MessageNonce,
+ ) -> DispatchResult {
+ // compute required bridge locations
+ let locations = Self::bridge_locations(origin, bridge_destination_universal_location)?;
+
+ // TODO: https://github.com/paritytech/parity-bridges-common/issues/1760 - may do refund here, if
+ // bridge/lanes are already closed + for messages that are not pruned
+
+ // update bridge metadata - this also guarantees that the bridge is in the proper state
+ let bridge =
+ Bridges::::try_mutate_exists(locations.lane_id, |bridge| match bridge {
+ Some(bridge) => {
+ bridge.state = BridgeState::Closed;
+ Ok(bridge.clone())
+ },
+ None => Err(Error::::UnknownBridge),
+ })?;
+
+ // close inbound and outbound lanes
+ let lanes_manager = LanesManagerOf::::new();
+ let mut inbound_lane = lanes_manager
+ .any_state_inbound_lane(locations.lane_id)
+ .map_err(Error::::LanesManager)?;
+ let mut outbound_lane = lanes_manager
+ .any_state_outbound_lane(locations.lane_id)
+ .map_err(Error::::LanesManager)?;
+
+ // now prune queued messages
+ let mut pruned_messages = 0;
+ for _ in outbound_lane.queued_messages() {
+ if pruned_messages == may_prune_messages {
+ break
+ }
+
+ outbound_lane.remove_oldest_unpruned_message();
+ pruned_messages += 1;
+ }
+
+ // if there are outbound messages in the queue, just update states and early exit
+ if !outbound_lane.queued_messages().is_empty() {
+ // update lanes state. Under normal circumstances, following calls shall never fail
+ inbound_lane.set_state(LaneState::Closed);
+ outbound_lane.set_state(LaneState::Closed);
+
+ // write something to log
+ let enqueued_messages = outbound_lane.queued_messages().checked_len().unwrap_or(0);
+ log::trace!(
+ target: LOG_TARGET,
+ "Bridge {:?} between {:?} and {:?} is closing. {} messages remaining",
+ locations.lane_id,
+ locations.bridge_origin_universal_location,
+ locations.bridge_destination_universal_location,
+ enqueued_messages,
+ );
+
+ // deposit the `ClosingBridge` event
+ Self::deposit_event(Event::::ClosingBridge {
+ lane_id: locations.lane_id,
+ pruned_messages,
+ enqueued_messages,
+ });
+
+ return Ok(())
+ }
+
+ // else we have pruned all messages, so lanes and the bridge itself may gone
+ inbound_lane.purge();
+ outbound_lane.purge();
+ Bridges::::remove(locations.lane_id);
+
+ // unreserve remaining amount
+ let failed_to_unreserve =
+ T::NativeCurrency::unreserve(&bridge.bridge_owner_account, bridge.reserve);
+ if !failed_to_unreserve.is_zero() {
+ // we can't do anything here - looks like funds have been (partially) unreserved
+ // before by someone else. Let's not fail, though - it'll be worse for the caller
+ log::trace!(
+ target: LOG_TARGET,
+ "Failed to unreserve {:?} during ridge {:?} closure",
+ failed_to_unreserve,
+ locations.lane_id,
+ );
+ }
+
+ // write something to log
+ log::trace!(
+ target: LOG_TARGET,
+ "Bridge {:?} between {:?} and {:?} has been closed",
+ locations.lane_id,
+ locations.bridge_origin_universal_location,
+ locations.bridge_destination_universal_location,
+ );
+
+ // deposit the `BridgePruned` event
+ Self::deposit_event(Event::::BridgePruned {
+ lane_id: locations.lane_id,
+ pruned_messages,
+ });
+
+ Ok(())
+ }
+ }
+
+ impl, I: 'static> Pallet
+ where
+ T: frame_system::Config>>,
+ T::NativeCurrency: Currency>>,
+ {
+ /// Return bridge endpoint locations and dedicated lane identifier.
+ pub fn bridge_locations(
+ origin: OriginFor,
+ bridge_destination_universal_location: Box,
+ ) -> Result, sp_runtime::DispatchError> {
+ bridge_locations(
+ Box::new(T::UniversalLocation::get()),
+ Box::new(T::OpenBridgeOrigin::ensure_origin(origin)?),
+ Box::new(
+ (*bridge_destination_universal_location)
+ .try_into()
+ .map_err(|_| Error::::UnsupportedXcmVersion)?,
+ ),
+ Self::bridged_network_id()?,
)
+ .map_err(|e| Error::::BridgeLocations(e).into())
}
}
+ impl, I: 'static> Pallet {
+ /// Returns some `NetworkId` if contains `GlobalConsensus` junction.
+ fn bridged_network_id() -> Result {
+ match T::BridgedNetwork::get().take_first_interior() {
+ Some(GlobalConsensus(network)) => Ok(network),
+ _ => Err(Error::::BridgeLocations(
+ BridgeLocationsError::InvalidBridgeDestination,
+ )
+ .into()),
+ }
+ }
+ }
+
+ /// All registered bridges.
+ #[pallet::storage]
+ pub type Bridges, I: 'static = ()> =
+ StorageMap<_, Identity, LaneId, BridgeOf>;
+
+ #[pallet::event]
+ #[pallet::generate_deposit(pub(super) fn deposit_event)]
+ pub enum Event, I: 'static = ()> {
+ /// The bridge between two locations has been opened.
+ BridgeOpened {
+ /// Universal location of local bridge endpoint.
+ local_endpoint: Box,
+ /// Universal location of remote bridge endpoint.
+ remote_endpoint: Box,
+ /// Bridge and its lane identifier.
+ lane_id: LaneId,
+ },
+ /// Bridge is going to be closed, but not yet fully pruned from the runtime storage.
+ ClosingBridge {
+ /// Bridge and its lane identifier.
+ lane_id: LaneId,
+ /// Number of pruned messages during the close call.
+ pruned_messages: MessageNonce,
+ /// Number of enqueued messages that need to be pruned in follow up calls.
+ enqueued_messages: MessageNonce,
+ },
+ /// Bridge has been closed and pruned from the runtime storage. It now may be reopened
+ /// again by any participant.
+ BridgePruned {
+ /// Bridge and its lane identifier.
+ lane_id: LaneId,
+ /// Number of pruned messages during the close call.
+ pruned_messages: MessageNonce,
+ },
+ }
+
+ #[pallet::error]
+ pub enum Error {
+ /// Bridge locations error.
+ BridgeLocations(BridgeLocationsError),
+ /// Invalid local bridge origin account.
+ InvalidBridgeOriginAccount,
+ /// The bridge is already registered in this pallet.
+ BridgeAlreadyExists,
+ /// Trying to close already closed bridge.
+ BridgeAlreadyClosed,
+ /// Lanes manager error.
+ LanesManager(LanesManagerError),
+ /// Trying to access unknown bridge.
+ UnknownBridge,
+ /// The bridge origin can't pay the required amount for opening the bridge.
+ FailedToReserveBridgeReserve,
+ /// The version of XCM location argument is unsupported.
+ UnsupportedXcmVersion,
+ }
+
impl, I: 'static> Pallet {
/// Returns dedicated/configured lane identifier.
pub(crate) fn lane_for(
@@ -97,7 +469,7 @@ pub mod pallet {
.find_map(|(lane_source, (lane_dest_network, lane_dest))| {
if lane_source.location == source &&
&lane_dest_network == dest.0 &&
- Self::bridged_network_id().as_ref() == Some(dest.0) &&
+ Self::bridged_network_id().as_ref() == Ok(dest.0) &&
&lane_dest == dest.1
{
Some(lane_source)
@@ -106,13 +478,542 @@ pub mod pallet {
}
})
}
+ }
+}
- /// Returns some `NetworkId` if contains `GlobalConsensus` junction.
- fn bridged_network_id() -> Option {
- match T::BridgedNetwork::get().take_first_interior() {
- Some(GlobalConsensus(network)) => Some(network),
- _ => None,
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use mock::*;
+
+ use frame_support::{assert_noop, assert_ok, traits::fungible::Mutate, BoundedVec};
+ use frame_system::{EventRecord, Phase};
+
+ fn fund_origin_sovereign_account(locations: &BridgeLocations, balance: Balance) -> AccountId {
+ let bridge_owner_account =
+ LocationToAccountId::convert_location(&locations.bridge_origin_relative_location)
+ .unwrap();
+ Balances::mint_into(&bridge_owner_account, balance).unwrap();
+ bridge_owner_account
+ }
+
+ fn mock_open_bridge_from_with(
+ origin: RuntimeOrigin,
+ with: InteriorLocation,
+ ) -> (BridgeOf, BridgeLocations) {
+ let reserve = BridgeDeposit::get();
+ let locations = XcmOverBridge::bridge_locations(origin, Box::new(with.into())).unwrap();
+ let bridge_owner_account =
+ fund_origin_sovereign_account(&locations, reserve + ExistentialDeposit::get());
+ Balances::reserve(&bridge_owner_account, reserve).unwrap();
+
+ let bridge = Bridge {
+ bridge_origin_relative_location: Box::new(
+ locations.bridge_origin_relative_location.clone().into(),
+ ),
+ state: BridgeState::Opened,
+ bridge_owner_account,
+ reserve,
+ };
+ Bridges::::insert(locations.lane_id, bridge.clone());
+
+ let lanes_manager = LanesManagerOf::::new();
+ lanes_manager.create_inbound_lane(locations.lane_id).unwrap();
+ lanes_manager.create_outbound_lane(locations.lane_id).unwrap();
+
+ (bridge, *locations)
+ }
+
+ fn mock_open_bridge_from(
+ origin: RuntimeOrigin,
+ ) -> (BridgeOf, BridgeLocations) {
+ mock_open_bridge_from_with(origin, bridged_asset_hub_location())
+ }
+
+ fn enqueue_message(lane: LaneId) {
+ let lanes_manager = LanesManagerOf::::new();
+ lanes_manager
+ .active_outbound_lane(lane)
+ .unwrap()
+ .send_message(BoundedVec::try_from(vec![42]).expect("We craft valid messages"));
+ }
+
+ #[test]
+ fn open_bridge_fails_if_origin_is_not_allowed() {
+ run_test(|| {
+ assert_noop!(
+ XcmOverBridge::open_bridge(
+ OpenBridgeOrigin::disallowed_origin(),
+ Box::new(bridged_asset_hub_location().into()),
+ ),
+ sp_runtime::DispatchError::BadOrigin,
+ );
+ })
+ }
+
+ #[test]
+ fn open_bridge_fails_if_origin_is_not_relative() {
+ run_test(|| {
+ assert_noop!(
+ XcmOverBridge::open_bridge(
+ OpenBridgeOrigin::parent_relay_chain_universal_origin(),
+ Box::new(bridged_asset_hub_location().into()),
+ ),
+ Error::::BridgeLocations(
+ BridgeLocationsError::InvalidBridgeOrigin
+ ),
+ );
+
+ assert_noop!(
+ XcmOverBridge::open_bridge(
+ OpenBridgeOrigin::sibling_parachain_universal_origin(),
+ Box::new(bridged_asset_hub_location().into()),
+ ),
+ Error::::BridgeLocations(
+ BridgeLocationsError::InvalidBridgeOrigin
+ ),
+ );
+ })
+ }
+
+ #[test]
+ fn open_bridge_fails_if_destination_is_not_remote() {
+ run_test(|| {
+ assert_noop!(
+ XcmOverBridge::open_bridge(
+ OpenBridgeOrigin::parent_relay_chain_origin(),
+ Box::new(
+ [GlobalConsensus(RelayNetwork::get()), Parachain(BRIDGED_ASSET_HUB_ID)]
+ .into()
+ ),
+ ),
+ Error::::BridgeLocations(BridgeLocationsError::DestinationIsLocal),
+ );
+ });
+ }
+
+ #[test]
+ fn open_bridge_fails_if_outside_of_bridged_consensus() {
+ run_test(|| {
+ assert_noop!(
+ XcmOverBridge::open_bridge(
+ OpenBridgeOrigin::parent_relay_chain_origin(),
+ Box::new(
+ [
+ GlobalConsensus(NonBridgedRelayNetwork::get()),
+ Parachain(BRIDGED_ASSET_HUB_ID)
+ ]
+ .into()
+ ),
+ ),
+ Error::::BridgeLocations(
+ BridgeLocationsError::UnreachableDestination
+ ),
+ );
+ });
+ }
+
+ #[test]
+ fn open_bridge_fails_if_origin_has_no_sovereign_account() {
+ run_test(|| {
+ assert_noop!(
+ XcmOverBridge::open_bridge(
+ OpenBridgeOrigin::origin_without_sovereign_account(),
+ Box::new(bridged_asset_hub_location().into()),
+ ),
+ Error::::InvalidBridgeOriginAccount,
+ );
+ });
+ }
+
+ #[test]
+ fn open_bridge_fails_if_origin_sovereign_account_has_no_enough_funds() {
+ run_test(|| {
+ assert_noop!(
+ XcmOverBridge::open_bridge(
+ OpenBridgeOrigin::parent_relay_chain_origin(),
+ Box::new(bridged_asset_hub_location().into()),
+ ),
+ Error::::FailedToReserveBridgeReserve,
+ );
+ });
+ }
+
+ #[test]
+ fn open_bridge_fails_if_it_already_exists() {
+ run_test(|| {
+ let origin = OpenBridgeOrigin::parent_relay_chain_origin();
+ let locations = XcmOverBridge::bridge_locations(
+ origin.clone(),
+ Box::new(bridged_asset_hub_location().into()),
+ )
+ .unwrap();
+ fund_origin_sovereign_account(
+ &locations,
+ BridgeDeposit::get() + ExistentialDeposit::get(),
+ );
+
+ Bridges::::insert(
+ locations.lane_id,
+ Bridge {
+ bridge_origin_relative_location: Box::new(
+ locations.bridge_origin_relative_location.into(),
+ ),
+ state: BridgeState::Opened,
+ bridge_owner_account: [0u8; 32].into(),
+ reserve: 0,
+ },
+ );
+
+ assert_noop!(
+ XcmOverBridge::open_bridge(origin, Box::new(bridged_asset_hub_location().into()),),
+ Error::::BridgeAlreadyExists,
+ );
+ })
+ }
+
+ #[test]
+ fn open_bridge_fails_if_its_lanes_already_exists() {
+ run_test(|| {
+ let origin = OpenBridgeOrigin::parent_relay_chain_origin();
+ let locations = XcmOverBridge::bridge_locations(
+ origin.clone(),
+ Box::new(bridged_asset_hub_location().into()),
+ )
+ .unwrap();
+ fund_origin_sovereign_account(
+ &locations,
+ BridgeDeposit::get() + ExistentialDeposit::get(),
+ );
+
+ let lanes_manager = LanesManagerOf::::new();
+
+ lanes_manager.create_inbound_lane(locations.lane_id).unwrap();
+ assert_noop!(
+ XcmOverBridge::open_bridge(
+ origin.clone(),
+ Box::new(bridged_asset_hub_location().into()),
+ ),
+ Error::::LanesManager(LanesManagerError::InboundLaneAlreadyExists),
+ );
+
+ lanes_manager.active_inbound_lane(locations.lane_id).unwrap().purge();
+ lanes_manager.create_outbound_lane(locations.lane_id).unwrap();
+ assert_noop!(
+ XcmOverBridge::open_bridge(origin, Box::new(bridged_asset_hub_location().into()),),
+ Error::::LanesManager(
+ LanesManagerError::OutboundLaneAlreadyExists
+ ),
+ );
+ })
+ }
+
+ #[test]
+ fn open_bridge_works() {
+ run_test(|| {
+ // in our test runtime, we expect that bridge may be opened by parent relay chain
+ // and any sibling parachain
+ let origins = [
+ OpenBridgeOrigin::parent_relay_chain_origin(),
+ OpenBridgeOrigin::sibling_parachain_origin(),
+ ];
+
+ // check that every origin may open the bridge
+ let lanes_manager = LanesManagerOf::::new();
+ let expected_reserve = BridgeDeposit::get();
+ let existential_deposit = ExistentialDeposit::get();
+ for origin in origins {
+ // reset events
+ System::set_block_number(1);
+ System::reset_events();
+
+ // compute all other locations
+ let locations = XcmOverBridge::bridge_locations(
+ origin.clone(),
+ Box::new(bridged_asset_hub_location().into()),
+ )
+ .unwrap();
+
+ // ensure that there's no bridge and lanes in the storage
+ assert_eq!(Bridges::::get(locations.lane_id), None);
+ assert_eq!(
+ lanes_manager.active_inbound_lane(locations.lane_id).map(drop),
+ Err(LanesManagerError::UnknownInboundLane)
+ );
+ assert_eq!(
+ lanes_manager.active_outbound_lane(locations.lane_id).map(drop),
+ Err(LanesManagerError::UnknownOutboundLane)
+ );
+
+ // give enough funds to the sovereign account of the bridge origin
+ let bridge_owner_account = fund_origin_sovereign_account(
+ &locations,
+ expected_reserve + existential_deposit,
+ );
+ assert_eq!(
+ Balances::free_balance(&bridge_owner_account),
+ expected_reserve + existential_deposit
+ );
+ assert_eq!(Balances::reserved_balance(&bridge_owner_account), 0);
+
+ // now open the bridge
+ assert_ok!(XcmOverBridge::open_bridge(
+ origin,
+ Box::new(locations.bridge_destination_universal_location.clone().into()),
+ ));
+
+ // ensure that everything has been set up in the runtime storage
+ assert_eq!(
+ Bridges::::get(locations.lane_id),
+ Some(Bridge {
+ bridge_origin_relative_location: Box::new(
+ locations.bridge_origin_relative_location.into()
+ ),
+ state: BridgeState::Opened,
+ bridge_owner_account: bridge_owner_account.clone(),
+ reserve: expected_reserve,
+ }),
+ );
+ assert_eq!(
+ lanes_manager.active_inbound_lane(locations.lane_id).map(|l| l.state()),
+ Ok(LaneState::Opened)
+ );
+ assert_eq!(
+ lanes_manager.active_outbound_lane(locations.lane_id).map(|l| l.state()),
+ Ok(LaneState::Opened)
+ );
+ assert_eq!(Balances::free_balance(&bridge_owner_account), existential_deposit);
+ assert_eq!(Balances::reserved_balance(&bridge_owner_account), expected_reserve);
+
+ // ensure that the proper event is deposited
+ assert_eq!(
+ System::events().last(),
+ Some(&EventRecord {
+ phase: Phase::Initialization,
+ event: RuntimeEvent::XcmOverBridge(Event::BridgeOpened {
+ lane_id: locations.lane_id,
+ local_endpoint: Box::new(locations.bridge_origin_universal_location),
+ remote_endpoint: Box::new(
+ locations.bridge_destination_universal_location
+ ),
+ }),
+ topics: vec![],
+ }),
+ );
}
- }
+ });
+ }
+
+ #[test]
+ fn close_bridge_fails_if_origin_is_not_allowed() {
+ run_test(|| {
+ assert_noop!(
+ XcmOverBridge::close_bridge(
+ OpenBridgeOrigin::disallowed_origin(),
+ Box::new(bridged_asset_hub_location().into()),
+ 0,
+ ),
+ sp_runtime::DispatchError::BadOrigin,
+ );
+ })
+ }
+
+ #[test]
+ fn close_bridge_fails_if_origin_is_not_relative() {
+ run_test(|| {
+ assert_noop!(
+ XcmOverBridge::close_bridge(
+ OpenBridgeOrigin::parent_relay_chain_universal_origin(),
+ Box::new(bridged_asset_hub_location().into()),
+ 0,
+ ),
+ Error::::BridgeLocations(
+ BridgeLocationsError::InvalidBridgeOrigin
+ ),
+ );
+
+ assert_noop!(
+ XcmOverBridge::close_bridge(
+ OpenBridgeOrigin::sibling_parachain_universal_origin(),
+ Box::new(bridged_asset_hub_location().into()),
+ 0,
+ ),
+ Error::::BridgeLocations(
+ BridgeLocationsError::InvalidBridgeOrigin
+ ),
+ );
+ })
+ }
+
+ #[test]
+ fn close_bridge_fails_if_its_lanes_are_unknown() {
+ run_test(|| {
+ let origin = OpenBridgeOrigin::parent_relay_chain_origin();
+ let (_, locations) = mock_open_bridge_from(origin.clone());
+
+ let lanes_manager = LanesManagerOf::::new();
+ lanes_manager.any_state_inbound_lane(locations.lane_id).unwrap().purge();
+ assert_noop!(
+ XcmOverBridge::close_bridge(
+ origin.clone(),
+ Box::new(locations.bridge_destination_universal_location.into()),
+ 0,
+ ),
+ Error::::LanesManager(LanesManagerError::UnknownInboundLane),
+ );
+ lanes_manager.any_state_outbound_lane(locations.lane_id).unwrap().purge();
+
+ let (_, locations) = mock_open_bridge_from(origin.clone());
+ lanes_manager.any_state_outbound_lane(locations.lane_id).unwrap().purge();
+ assert_noop!(
+ XcmOverBridge::close_bridge(
+ origin,
+ Box::new(locations.bridge_destination_universal_location.into()),
+ 0,
+ ),
+ Error::::LanesManager(LanesManagerError::UnknownOutboundLane),
+ );
+ });
+ }
+
+ #[test]
+ fn close_bridge_works() {
+ run_test(|| {
+ let origin = OpenBridgeOrigin::parent_relay_chain_origin();
+ let (bridge, locations) = mock_open_bridge_from(origin.clone());
+ System::set_block_number(1);
+
+ // remember owner balances
+ let free_balance = Balances::free_balance(&bridge.bridge_owner_account);
+ let reserved_balance = Balances::reserved_balance(&bridge.bridge_owner_account);
+
+ // enqueue some messages
+ for _ in 0..32 {
+ enqueue_message(locations.lane_id);
+ }
+
+ // now call the `close_bridge`, which will only partially prune messages
+ assert_ok!(XcmOverBridge::close_bridge(
+ origin.clone(),
+ Box::new(locations.bridge_destination_universal_location.clone().into()),
+ 16,
+ ),);
+
+ // as a result, the bridge and lanes are switched to the `Closed` state, some messages
+ // are pruned, but funds are not unreserved
+ let lanes_manager = LanesManagerOf::::new();
+ assert_eq!(
+ Bridges::::get(locations.lane_id).map(|b| b.state),
+ Some(BridgeState::Closed)
+ );
+ assert_eq!(
+ lanes_manager.any_state_inbound_lane(locations.lane_id).unwrap().state(),
+ LaneState::Closed
+ );
+ assert_eq!(
+ lanes_manager.any_state_outbound_lane(locations.lane_id).unwrap().state(),
+ LaneState::Closed
+ );
+ assert_eq!(
+ lanes_manager
+ .any_state_outbound_lane(locations.lane_id)
+ .unwrap()
+ .queued_messages()
+ .checked_len(),
+ Some(16)
+ );
+ assert_eq!(Balances::free_balance(&bridge.bridge_owner_account), free_balance);
+ assert_eq!(Balances::reserved_balance(&bridge.bridge_owner_account), reserved_balance);
+ assert_eq!(
+ System::events().last(),
+ Some(&EventRecord {
+ phase: Phase::Initialization,
+ event: RuntimeEvent::XcmOverBridge(Event::ClosingBridge {
+ lane_id: locations.lane_id,
+ pruned_messages: 16,
+ enqueued_messages: 16,
+ }),
+ topics: vec![],
+ }),
+ );
+
+ // now call the `close_bridge` again, which will only partially prune messages
+ assert_ok!(XcmOverBridge::close_bridge(
+ origin.clone(),
+ Box::new(locations.bridge_destination_universal_location.clone().into()),
+ 8,
+ ),);
+
+ // nothing is changed (apart from the pruned messages)
+ assert_eq!(
+ Bridges::::get(locations.lane_id).map(|b| b.state),
+ Some(BridgeState::Closed)
+ );
+ assert_eq!(
+ lanes_manager.any_state_inbound_lane(locations.lane_id).unwrap().state(),
+ LaneState::Closed
+ );
+ assert_eq!(
+ lanes_manager.any_state_outbound_lane(locations.lane_id).unwrap().state(),
+ LaneState::Closed
+ );
+ assert_eq!(
+ lanes_manager
+ .any_state_outbound_lane(locations.lane_id)
+ .unwrap()
+ .queued_messages()
+ .checked_len(),
+ Some(8)
+ );
+ assert_eq!(Balances::free_balance(&bridge.bridge_owner_account), free_balance);
+ assert_eq!(Balances::reserved_balance(&bridge.bridge_owner_account), reserved_balance);
+ assert_eq!(
+ System::events().last(),
+ Some(&EventRecord {
+ phase: Phase::Initialization,
+ event: RuntimeEvent::XcmOverBridge(Event::ClosingBridge {
+ lane_id: locations.lane_id,
+ pruned_messages: 8,
+ enqueued_messages: 8,
+ }),
+ topics: vec![],
+ }),
+ );
+
+ // now call the `close_bridge` again that will prune all remaining messages and the
+ // bridge
+ assert_ok!(XcmOverBridge::close_bridge(
+ origin,
+ Box::new(locations.bridge_destination_universal_location.into()),
+ 9,
+ ),);
+
+ // there's no traces of bridge in the runtime storage and funds are unreserved
+ assert_eq!(Bridges::::get(locations.lane_id).map(|b| b.state), None);
+ assert_eq!(
+ lanes_manager.any_state_inbound_lane(locations.lane_id).map(drop),
+ Err(LanesManagerError::UnknownInboundLane)
+ );
+ assert_eq!(
+ lanes_manager.any_state_outbound_lane(locations.lane_id).map(drop),
+ Err(LanesManagerError::UnknownOutboundLane)
+ );
+ assert_eq!(
+ Balances::free_balance(&bridge.bridge_owner_account),
+ free_balance + reserved_balance
+ );
+ assert_eq!(Balances::reserved_balance(&bridge.bridge_owner_account), 0);
+ assert_eq!(
+ System::events().last(),
+ Some(&EventRecord {
+ phase: Phase::Initialization,
+ event: RuntimeEvent::XcmOverBridge(Event::BridgePruned {
+ lane_id: locations.lane_id,
+ pruned_messages: 8,
+ }),
+ topics: vec![],
+ }),
+ );
+ });
}
}
diff --git a/bridges/modules/xcm-bridge-hub/src/mock.rs b/bridges/modules/xcm-bridge-hub/src/mock.rs
index 2306174f63fe..673a5790ec08 100644
--- a/bridges/modules/xcm-bridge-hub/src/mock.rs
+++ b/bridges/modules/xcm-bridge-hub/src/mock.rs
@@ -47,6 +47,10 @@ use xcm_executor::XcmExecutor;
pub type AccountId = AccountId32;
pub type Balance = u64;
+use frame_support::traits::{EnsureOrigin, OriginTrait};
+use polkadot_parachain_primitives::primitives::Sibling;
+use xcm_builder::{ParentIsPreset, SiblingParachainConvertsVia};
+
type Block = frame_system::mocking::MockBlock;
pub const SIBLING_ASSET_HUB_ID: u32 = 2001;
@@ -63,7 +67,7 @@ frame_support::construct_runtime! {
System: frame_system::{Pallet, Call, Config, Storage, Event},
Balances: pallet_balances::{Pallet, Event},
Messages: pallet_bridge_messages::{Pallet, Call, Event},
- XcmOverBridge: pallet_xcm_bridge_hub::{Pallet},
+ XcmOverBridge: pallet_xcm_bridge_hub::{Pallet, Call, Event},
XcmOverBridgeRouter: pallet_xcm_bridge_hub_router,
}
}
@@ -178,6 +182,8 @@ parameter_types! {
}
impl pallet_xcm_bridge_hub::Config for TestRuntime {
+ type RuntimeEvent = RuntimeEvent;
+
type UniversalLocation = UniversalLocation;
type BridgedNetwork = BridgedRelayNetworkLocation;
type BridgeMessagesPalletInstance = ();
@@ -187,6 +193,12 @@ impl pallet_xcm_bridge_hub::Config for TestRuntime {
type Lanes = TestLanes;
type LanesSupport = TestXcmBlobHauler;
+
+ type OpenBridgeOrigin = OpenBridgeOrigin;
+ type BridgeOriginAccountIdConverter = LocationToAccountId;
+
+ type BridgeReserve = BridgeDeposit;
+ type NativeCurrency = Balances;
}
impl pallet_xcm_bridge_hub_router::Config<()> for TestRuntime {
@@ -309,10 +321,91 @@ impl XcmBlobHauler for TestXcmBlobHauler {
type UncongestedMessage = ();
}
+/// Type for specifying how a `Location` can be converted into an `AccountId`. This is used
+/// when determining ownership of accounts for asset transacting and when attempting to use XCM
+/// `Transact` in order to determine the dispatch Origin.
+pub type LocationToAccountId = (
+ // The parent (Relay-chain) origin converts to the parent `AccountId`.
+ ParentIsPreset,
+ // Sibling parachain origins convert to AccountId via the `ParaId::into`.
+ SiblingParachainConvertsVia,
+);
+
+pub struct OpenBridgeOrigin;
+
+impl OpenBridgeOrigin {
+ pub fn parent_relay_chain_origin() -> RuntimeOrigin {
+ RuntimeOrigin::signed([0u8; 32].into())
+ }
+
+ pub fn parent_relay_chain_universal_origin() -> RuntimeOrigin {
+ RuntimeOrigin::signed([1u8; 32].into())
+ }
+
+ pub fn sibling_parachain_origin() -> RuntimeOrigin {
+ let mut account = [0u8; 32];
+ account[..4].copy_from_slice(&SIBLING_ASSET_HUB_ID.encode()[..4]);
+ RuntimeOrigin::signed(account.into())
+ }
+
+ pub fn sibling_parachain_universal_origin() -> RuntimeOrigin {
+ RuntimeOrigin::signed([2u8; 32].into())
+ }
+
+ pub fn origin_without_sovereign_account() -> RuntimeOrigin {
+ RuntimeOrigin::signed([3u8; 32].into())
+ }
+
+ pub fn disallowed_origin() -> RuntimeOrigin {
+ RuntimeOrigin::signed([42u8; 32].into())
+ }
+}
+
+impl EnsureOrigin for OpenBridgeOrigin {
+ type Success = Location;
+
+ fn try_origin(o: RuntimeOrigin) -> Result {
+ let signer = o.clone().into_signer();
+ if signer == Self::parent_relay_chain_origin().into_signer() {
+ return Ok(Location { parents: 1, interior: Here })
+ } else if signer == Self::parent_relay_chain_universal_origin().into_signer() {
+ return Ok(Location {
+ parents: 2,
+ interior: GlobalConsensus(RelayNetwork::get()).into(),
+ })
+ } else if signer == Self::sibling_parachain_universal_origin().into_signer() {
+ return Ok(Location {
+ parents: 2,
+ interior: [GlobalConsensus(RelayNetwork::get()), Parachain(SIBLING_ASSET_HUB_ID)]
+ .into(),
+ })
+ } else if signer == Self::origin_without_sovereign_account().into_signer() {
+ return Ok(Location {
+ parents: 1,
+ interior: [Parachain(SIBLING_ASSET_HUB_ID), OnlyChild].into(),
+ })
+ }
+
+ let mut sibling_account = [0u8; 32];
+ sibling_account[..4].copy_from_slice(&SIBLING_ASSET_HUB_ID.encode()[..4]);
+ if signer == Some(sibling_account.into()) {
+ return Ok(Location { parents: 1, interior: Parachain(SIBLING_ASSET_HUB_ID).into() })
+ }
+
+ Err(o)
+ }
+
+ #[cfg(feature = "runtime-benchmarks")]
+ fn try_successful_origin() -> Result {
+ Ok(Self::parent_relay_chain_origin())
+ }
+}
+
pub struct ThisUnderlyingChain;
impl Chain for ThisUnderlyingChain {
const ID: ChainId = *b"tuch";
+
type BlockNumber = u64;
type Hash = H256;
type Hasher = BlakeTwo256;
@@ -334,16 +427,15 @@ impl Chain for ThisUnderlyingChain {
}
impl ChainWithMessages for ThisUnderlyingChain {
- const WITH_CHAIN_MESSAGES_PALLET_NAME: &'static str = "";
-
+ const WITH_CHAIN_MESSAGES_PALLET_NAME: &'static str = "WithThisChainBridgeMessages";
const MAX_UNREWARDED_RELAYERS_IN_CONFIRMATION_TX: MessageNonce = 16;
- const MAX_UNCONFIRMED_MESSAGES_IN_CONFIRMATION_TX: MessageNonce = 1000;
+ const MAX_UNCONFIRMED_MESSAGES_IN_CONFIRMATION_TX: MessageNonce = 128;
}
-pub struct BridgedUnderlyingChain;
pub type BridgedHeaderHash = H256;
pub type BridgedChainHeader = SubstrateHeader;
+pub struct BridgedUnderlyingChain;
impl Chain for BridgedUnderlyingChain {
const ID: ChainId = *b"bgdc";
type BlockNumber = u64;
@@ -367,9 +459,9 @@ impl Chain for BridgedUnderlyingChain {
}
impl ChainWithMessages for BridgedUnderlyingChain {
- const WITH_CHAIN_MESSAGES_PALLET_NAME: &'static str = "";
+ const WITH_CHAIN_MESSAGES_PALLET_NAME: &'static str = "WithBridgedChainBridgeMessages";
const MAX_UNREWARDED_RELAYERS_IN_CONFIRMATION_TX: MessageNonce = 16;
- const MAX_UNCONFIRMED_MESSAGES_IN_CONFIRMATION_TX: MessageNonce = 1000;
+ const MAX_UNCONFIRMED_MESSAGES_IN_CONFIRMATION_TX: MessageNonce = 128;
}
/// Test message dispatcher.
@@ -409,6 +501,11 @@ impl bp_header_chain::HeaderChain for BridgedHeaderChain
}
}
+/// Location of bridged asset hub.
+pub fn bridged_asset_hub_location() -> InteriorLocation {
+ [GlobalConsensus(BridgedRelayNetwork::get()), Parachain(BRIDGED_ASSET_HUB_ID)].into()
+}
+
/// Run pallet test.
pub fn run_test(test: impl FnOnce() -> T) -> T {
sp_io::TestExternalities::new(
diff --git a/bridges/primitives/header-chain/src/lib.rs b/bridges/primitives/header-chain/src/lib.rs
index 509e2d062704..26295dee1801 100644
--- a/bridges/primitives/header-chain/src/lib.rs
+++ b/bridges/primitives/header-chain/src/lib.rs
@@ -90,19 +90,6 @@ pub trait HeaderChain {
}
}
-impl HeaderChain for () {
- fn finalized_header_state_root(_header_hash: HashOf) -> Option> {
- None
- }
-
- fn verify_storage_proof(
- _header_hash: HashOf,
- _db: UnverifiedStorageProof,
- ) -> Result {
- Err(HeaderChainError::UnknownHeader)
- }
-}
-
/// A type that can be used as a parameter in a dispatchable function.
///
/// When using `decl_module` all arguments for call functions must implement this trait.
diff --git a/bridges/primitives/messages/src/lib.rs b/bridges/primitives/messages/src/lib.rs
index 540b6a37d794..b6d8660c08d2 100644
--- a/bridges/primitives/messages/src/lib.rs
+++ b/bridges/primitives/messages/src/lib.rs
@@ -254,10 +254,6 @@ impl TypeId for LaneId {
pub enum LaneState {
/// Lane is opened and messages may be sent/received over it.
Opened,
- /// Lane is closing. It is equal to the `Opened` state, but it will switch to
- /// the `Closed` state and then vanish after some period. This state is here
- /// to give bridged chain ability to know that the lane is going to be closed.
- Closing,
/// Lane is closed and all attempts to send/receive messages to/from this lane
/// will fail.
///
@@ -271,7 +267,7 @@ pub enum LaneState {
impl LaneState {
/// Returns true if lane state allows sending/receiving messages.
pub fn is_active(&self) -> bool {
- matches!(*self, LaneState::Opened | LaneState::Closing)
+ matches!(*self, LaneState::Opened)
}
}
diff --git a/bridges/primitives/xcm-bridge-hub/Cargo.toml b/bridges/primitives/xcm-bridge-hub/Cargo.toml
index b990a232bff1..fa8f9ef0b76f 100644
--- a/bridges/primitives/xcm-bridge-hub/Cargo.toml
+++ b/bridges/primitives/xcm-bridge-hub/Cargo.toml
@@ -11,16 +11,28 @@ repository.workspace = true
workspace = true
[dependencies]
+codec = { features = ["derive"], default-features = false, workspace = true }
+scale-info = { features = ["derive"], workspace = true }
+
+# Bridge Dependencies
+bp-messages = { path = "../messages", default-features = false }
+bp-runtime = { path = "../runtime", default-features = false }
# Substrate Dependencies
sp-std = { workspace = true }
+frame-support = { workspace = true }
# Polkadot Dependencies
-xcm = { package = "staging-xcm", path = "../../../polkadot/xcm", default-features = false }
+xcm = { workspace = true }
[features]
default = ["std"]
std = [
+ "bp-messages/std",
+ "bp-runtime/std",
+ "codec/std",
+ "frame-support/std",
+ "scale-info/std",
"sp-std/std",
"xcm/std",
]
diff --git a/bridges/primitives/xcm-bridge-hub/src/lib.rs b/bridges/primitives/xcm-bridge-hub/src/lib.rs
index 78f913273370..44dbb9698241 100644
--- a/bridges/primitives/xcm-bridge-hub/src/lib.rs
+++ b/bridges/primitives/xcm-bridge-hub/src/lib.rs
@@ -16,9 +16,19 @@
//! Primitives of the xcm-bridge-hub pallet.
+#![warn(missing_docs)]
#![cfg_attr(not(feature = "std"), no_std)]
-use xcm::latest::prelude::*;
+use bp_messages::LaneId;
+use bp_runtime::{AccountIdOf, BalanceOf, Chain};
+use codec::{Decode, Encode, MaxEncodedLen};
+use frame_support::{
+ ensure, sp_runtime::RuntimeDebug, CloneNoBound, PalletError, PartialEqNoBound,
+ RuntimeDebugNoBound,
+};
+use scale_info::TypeInfo;
+use sp_std::boxed::Box;
+use xcm::{latest::prelude::*, VersionedLocation};
/// Encoded XCM blob. We expect the bridge messages pallet to use this blob type for both inbound
/// and outbound payloads.
@@ -45,8 +55,7 @@ pub trait LocalXcmChannelManager {
/// `owner` chain (in any form), we expect it to stop sending messages to us and queue
/// messages at that `owner` chain instead.
///
- /// This method will be called if we detect a misbehavior in one of bridges, owned by
- /// the `owner`. We expect that:
+ /// We expect that:
///
/// - no more incoming XCM messages from the `owner` will be processed until further
/// `resume_inbound_channel` call;
@@ -60,9 +69,8 @@ pub trait LocalXcmChannelManager {
/// Start handling incoming messages from from given bridge `owner` (parent/sibling chain)
/// again.
///
- /// This method is called when the `owner` tries to resume bridge operations after
- /// resolving "misbehavior" issues. The channel is assumed to be suspended by the previous
- /// `suspend_inbound_channel` call, however we don't check it anywhere.
+ /// The channel is assumed to be suspended by the previous `suspend_inbound_channel` call,
+ /// however we don't check it anywhere.
///
/// This method shall not fail if the channel is already resumed.
fn resume_inbound_channel(owner: Location) -> Result<(), ()>;
@@ -77,3 +85,416 @@ impl LocalXcmChannelManager for () {
Err(())
}
}
+
+/// Bridge state.
+#[derive(Clone, Copy, Decode, Encode, Eq, PartialEq, TypeInfo, MaxEncodedLen, RuntimeDebug)]
+pub enum BridgeState {
+ /// Bridge is opened. Associated lanes are also opened.
+ Opened,
+ /// Bridge is closed. Associated lanes are also closed.
+ /// After all outbound messages will be pruned, the bridge will vanish without any traces.
+ Closed,
+}
+
+/// Bridge metadata.
+#[derive(
+ CloneNoBound, Decode, Encode, Eq, PartialEqNoBound, TypeInfo, MaxEncodedLen, RuntimeDebugNoBound,
+)]
+#[scale_info(skip_type_params(ThisChain))]
+pub struct Bridge {
+ /// Relative location of the bridge origin chain.
+ pub bridge_origin_relative_location: Box,
+ /// Current bridge state.
+ pub state: BridgeState,
+ /// Account with the reserved funds.
+ pub bridge_owner_account: AccountIdOf,
+ /// Reserved amount on the sovereign account of the sibling bridge origin.
+ pub reserve: BalanceOf,
+}
+
+/// Locations of bridge endpoints at both sides of the bridge.
+#[derive(Clone, RuntimeDebug, PartialEq, Eq)]
+pub struct BridgeLocations {
+ /// Relative (to this bridge hub) location of this side of the bridge.
+ pub bridge_origin_relative_location: Location,
+ /// Universal (unique) location of this side of the bridge.
+ pub bridge_origin_universal_location: InteriorLocation,
+ /// Universal (unique) location of other side of the bridge.
+ pub bridge_destination_universal_location: InteriorLocation,
+ /// An identifier of the dedicated bridge message lane.
+ pub lane_id: LaneId,
+}
+
+/// Errors that may happen when we check bridge locations.
+#[derive(Encode, Decode, RuntimeDebug, PartialEq, Eq, PalletError, TypeInfo)]
+pub enum BridgeLocationsError {
+ /// Origin or destination locations are not universal.
+ NonUniversalLocation,
+ /// Bridge origin location is not supported.
+ InvalidBridgeOrigin,
+ /// Bridge destination is not supported (in general).
+ InvalidBridgeDestination,
+ /// Destination location is within the same global consensus.
+ DestinationIsLocal,
+ /// Destination network is not the network we are bridged with.
+ UnreachableDestination,
+ /// Destination location is unsupported. We only support bridges with relay
+ /// chain or its parachains.
+ UnsupportedDestinationLocation,
+}
+
+/// Given XCM locations, generate lane id and universal locations of bridge endpoints.
+///
+/// The `here_universal_location` is the universal location of the bridge hub runtime.
+///
+/// The `bridge_origin_relative_location` is the relative (to the `here_universal_location`)
+/// location of the bridge endpoint at this side of the bridge. It may be the parent relay
+/// chain or the sibling parachain. All junctions below parachain level are dropped.
+///
+/// The `bridge_destination_universal_location` is the universal location of the bridge
+/// destination. It may be the parent relay or the sibling parachain of the **bridged**
+/// bridge hub. All junctions below parachain level are dropped.
+///
+/// Why we drop all junctions between parachain level - that's because the lane is a bridge
+/// between two chains. All routing under this level happens when the message is delivered
+/// to the bridge destination. So at bridge level we don't care about low level junctions.
+///
+/// Returns error if `bridge_origin_relative_location` is outside of `here_universal_location`
+/// local consensus OR if `bridge_destination_universal_location` is not a universal location.
+pub fn bridge_locations(
+ here_universal_location: Box,
+ bridge_origin_relative_location: Box,
+ bridge_destination_universal_location: Box,
+ expected_remote_network: NetworkId,
+) -> Result, BridgeLocationsError> {
+ fn strip_low_level_junctions(
+ location: InteriorLocation,
+ ) -> Result {
+ let mut junctions = location.into_iter();
+
+ let global_consensus = junctions
+ .next()
+ .filter(|junction| matches!(junction, GlobalConsensus(_)))
+ .ok_or(BridgeLocationsError::NonUniversalLocation)?;
+
+ // we only expect `Parachain` junction here. There are other junctions that
+ // may need to be supported (like `GeneralKey` and `OnlyChild`), but now we
+ // only support bridges with relay and parachans
+ //
+ // if there's something other than parachain, let's strip it
+ let maybe_parachain = junctions.next().filter(|junction| matches!(junction, Parachain(_)));
+ Ok(match maybe_parachain {
+ Some(parachain) => [global_consensus, parachain].into(),
+ None => [global_consensus].into(),
+ })
+ }
+
+ // ensure that the `here_universal_location` and `bridge_destination_universal_location`
+ // are universal locations within different consensus systems
+ let local_network = here_universal_location
+ .global_consensus()
+ .map_err(|_| BridgeLocationsError::NonUniversalLocation)?;
+ let remote_network = bridge_destination_universal_location
+ .global_consensus()
+ .map_err(|_| BridgeLocationsError::NonUniversalLocation)?;
+ ensure!(local_network != remote_network, BridgeLocationsError::DestinationIsLocal);
+ ensure!(
+ remote_network == expected_remote_network,
+ BridgeLocationsError::UnreachableDestination
+ );
+
+ // get universal location of endpoint, located at this side of the bridge
+ let bridge_origin_universal_location = here_universal_location
+ .within_global(*bridge_origin_relative_location.clone())
+ .map_err(|_| BridgeLocationsError::InvalidBridgeOrigin)?;
+ // strip low-level junctions within universal locations
+ let bridge_origin_universal_location =
+ strip_low_level_junctions(bridge_origin_universal_location)?;
+ let bridge_destination_universal_location =
+ strip_low_level_junctions(*bridge_destination_universal_location)?;
+
+ // we know that the `bridge_destination_universal_location` starts from the
+ // `GlobalConsensus` and we know that the `bridge_origin_universal_location`
+ // is also within the `GlobalConsensus`. So we know that the lane id will be
+ // the same on both ends of the bridge
+ let lane_id = LaneId::new(
+ bridge_origin_universal_location.clone(),
+ bridge_destination_universal_location.clone(),
+ );
+
+ Ok(Box::new(BridgeLocations {
+ bridge_origin_relative_location: *bridge_origin_relative_location,
+ bridge_origin_universal_location,
+ bridge_destination_universal_location,
+ lane_id,
+ }))
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ const LOCAL_NETWORK: NetworkId = Kusama;
+ const REMOTE_NETWORK: NetworkId = Polkadot;
+ const UNREACHABLE_NETWORK: NetworkId = Rococo;
+ const SIBLING_PARACHAIN: u32 = 1000;
+ const LOCAL_BRIDGE_HUB: u32 = 1001;
+ const REMOTE_PARACHAIN: u32 = 2000;
+
+ struct SuccessfulTest {
+ here_universal_location: InteriorLocation,
+ bridge_origin_relative_location: Location,
+
+ bridge_origin_universal_location: InteriorLocation,
+ bridge_destination_universal_location: InteriorLocation,
+ }
+
+ fn run_successful_test(test: SuccessfulTest) -> BridgeLocations {
+ let locations = bridge_locations(
+ Box::new(test.here_universal_location),
+ Box::new(test.bridge_origin_relative_location.clone()),
+ Box::new(test.bridge_destination_universal_location.clone()),
+ REMOTE_NETWORK,
+ );
+ assert_eq!(
+ locations,
+ Ok(Box::new(BridgeLocations {
+ bridge_origin_relative_location: test.bridge_origin_relative_location.clone(),
+ bridge_origin_universal_location: test.bridge_origin_universal_location.clone(),
+ bridge_destination_universal_location: test
+ .bridge_destination_universal_location
+ .clone(),
+ lane_id: LaneId::new(
+ test.bridge_origin_universal_location,
+ test.bridge_destination_universal_location,
+ ),
+ })),
+ );
+
+ *locations.unwrap()
+ }
+
+ // successful tests that with various origins and destinations
+
+ #[test]
+ fn at_relay_from_local_relay_to_remote_relay_works() {
+ run_successful_test(SuccessfulTest {
+ here_universal_location: [GlobalConsensus(LOCAL_NETWORK)].into(),
+ bridge_origin_relative_location: Here.into(),
+
+ bridge_origin_universal_location: [GlobalConsensus(LOCAL_NETWORK)].into(),
+ bridge_destination_universal_location: [GlobalConsensus(REMOTE_NETWORK)].into(),
+ });
+ }
+
+ #[test]
+ fn at_relay_from_sibling_parachain_to_remote_relay_works() {
+ run_successful_test(SuccessfulTest {
+ here_universal_location: [GlobalConsensus(LOCAL_NETWORK)].into(),
+ bridge_origin_relative_location: [Parachain(SIBLING_PARACHAIN)].into(),
+
+ bridge_origin_universal_location: [
+ GlobalConsensus(LOCAL_NETWORK),
+ Parachain(SIBLING_PARACHAIN),
+ ]
+ .into(),
+ bridge_destination_universal_location: [GlobalConsensus(REMOTE_NETWORK)].into(),
+ });
+ }
+
+ #[test]
+ fn at_relay_from_local_relay_to_remote_parachain_works() {
+ run_successful_test(SuccessfulTest {
+ here_universal_location: [GlobalConsensus(LOCAL_NETWORK)].into(),
+ bridge_origin_relative_location: Here.into(),
+
+ bridge_origin_universal_location: [GlobalConsensus(LOCAL_NETWORK)].into(),
+ bridge_destination_universal_location: [
+ GlobalConsensus(REMOTE_NETWORK),
+ Parachain(REMOTE_PARACHAIN),
+ ]
+ .into(),
+ });
+ }
+
+ #[test]
+ fn at_relay_from_sibling_parachain_to_remote_parachain_works() {
+ run_successful_test(SuccessfulTest {
+ here_universal_location: [GlobalConsensus(LOCAL_NETWORK)].into(),
+ bridge_origin_relative_location: [Parachain(SIBLING_PARACHAIN)].into(),
+
+ bridge_origin_universal_location: [
+ GlobalConsensus(LOCAL_NETWORK),
+ Parachain(SIBLING_PARACHAIN),
+ ]
+ .into(),
+ bridge_destination_universal_location: [
+ GlobalConsensus(REMOTE_NETWORK),
+ Parachain(REMOTE_PARACHAIN),
+ ]
+ .into(),
+ });
+ }
+
+ #[test]
+ fn at_bridge_hub_from_local_relay_to_remote_relay_works() {
+ run_successful_test(SuccessfulTest {
+ here_universal_location: [GlobalConsensus(LOCAL_NETWORK), Parachain(LOCAL_BRIDGE_HUB)]
+ .into(),
+ bridge_origin_relative_location: Parent.into(),
+
+ bridge_origin_universal_location: [GlobalConsensus(LOCAL_NETWORK)].into(),
+ bridge_destination_universal_location: [GlobalConsensus(REMOTE_NETWORK)].into(),
+ });
+ }
+
+ #[test]
+ fn at_bridge_hub_from_sibling_parachain_to_remote_relay_works() {
+ run_successful_test(SuccessfulTest {
+ here_universal_location: [GlobalConsensus(LOCAL_NETWORK), Parachain(LOCAL_BRIDGE_HUB)]
+ .into(),
+ bridge_origin_relative_location: ParentThen([Parachain(SIBLING_PARACHAIN)].into())
+ .into(),
+
+ bridge_origin_universal_location: [
+ GlobalConsensus(LOCAL_NETWORK),
+ Parachain(SIBLING_PARACHAIN),
+ ]
+ .into(),
+ bridge_destination_universal_location: [GlobalConsensus(REMOTE_NETWORK)].into(),
+ });
+ }
+
+ #[test]
+ fn at_bridge_hub_from_local_relay_to_remote_parachain_works() {
+ run_successful_test(SuccessfulTest {
+ here_universal_location: [GlobalConsensus(LOCAL_NETWORK), Parachain(LOCAL_BRIDGE_HUB)]
+ .into(),
+ bridge_origin_relative_location: Parent.into(),
+
+ bridge_origin_universal_location: [GlobalConsensus(LOCAL_NETWORK)].into(),
+ bridge_destination_universal_location: [
+ GlobalConsensus(REMOTE_NETWORK),
+ Parachain(REMOTE_PARACHAIN),
+ ]
+ .into(),
+ });
+ }
+
+ #[test]
+ fn at_bridge_hub_from_sibling_parachain_to_remote_parachain_works() {
+ run_successful_test(SuccessfulTest {
+ here_universal_location: [GlobalConsensus(LOCAL_NETWORK), Parachain(LOCAL_BRIDGE_HUB)]
+ .into(),
+ bridge_origin_relative_location: ParentThen([Parachain(SIBLING_PARACHAIN)].into())
+ .into(),
+
+ bridge_origin_universal_location: [
+ GlobalConsensus(LOCAL_NETWORK),
+ Parachain(SIBLING_PARACHAIN),
+ ]
+ .into(),
+ bridge_destination_universal_location: [
+ GlobalConsensus(REMOTE_NETWORK),
+ Parachain(REMOTE_PARACHAIN),
+ ]
+ .into(),
+ });
+ }
+
+ // successful tests that show that we are ignoring low-level junctions of bridge origins
+
+ #[test]
+ fn low_level_junctions_at_bridge_origin_are_stripped() {
+ let locations1 = run_successful_test(SuccessfulTest {
+ here_universal_location: [GlobalConsensus(LOCAL_NETWORK)].into(),
+ bridge_origin_relative_location: Here.into(),
+
+ bridge_origin_universal_location: [GlobalConsensus(LOCAL_NETWORK)].into(),
+ bridge_destination_universal_location: [GlobalConsensus(REMOTE_NETWORK)].into(),
+ });
+ let locations2 = run_successful_test(SuccessfulTest {
+ here_universal_location: [GlobalConsensus(LOCAL_NETWORK)].into(),
+ bridge_origin_relative_location: [PalletInstance(0)].into(),
+
+ bridge_origin_universal_location: [GlobalConsensus(LOCAL_NETWORK)].into(),
+ bridge_destination_universal_location: [GlobalConsensus(REMOTE_NETWORK)].into(),
+ });
+
+ assert_eq!(locations1.lane_id, locations2.lane_id);
+ }
+
+ #[test]
+ fn low_level_junctions_at_bridge_destination_are_stripped() {
+ let locations1 = run_successful_test(SuccessfulTest {
+ here_universal_location: [GlobalConsensus(LOCAL_NETWORK)].into(),
+ bridge_origin_relative_location: Here.into(),
+
+ bridge_origin_universal_location: [GlobalConsensus(LOCAL_NETWORK)].into(),
+ bridge_destination_universal_location: [GlobalConsensus(REMOTE_NETWORK)].into(),
+ });
+ let locations2 = run_successful_test(SuccessfulTest {
+ here_universal_location: [GlobalConsensus(LOCAL_NETWORK)].into(),
+ bridge_origin_relative_location: Here.into(),
+
+ bridge_origin_universal_location: [GlobalConsensus(LOCAL_NETWORK)].into(),
+ bridge_destination_universal_location: [GlobalConsensus(REMOTE_NETWORK)].into(),
+ });
+
+ assert_eq!(locations1.lane_id, locations2.lane_id);
+ }
+
+ // negative tests
+
+ #[test]
+ fn bridge_locations_fails_when_here_is_not_universal_location() {
+ assert_eq!(
+ bridge_locations(
+ Box::new([Parachain(1000)].into()),
+ Box::new(Here.into()),
+ Box::new([GlobalConsensus(REMOTE_NETWORK)].into()),
+ REMOTE_NETWORK,
+ ),
+ Err(BridgeLocationsError::NonUniversalLocation),
+ );
+ }
+
+ #[test]
+ fn bridge_locations_fails_when_computed_destination_is_not_universal_location() {
+ assert_eq!(
+ bridge_locations(
+ Box::new([GlobalConsensus(LOCAL_NETWORK)].into()),
+ Box::new(Here.into()),
+ Box::new([OnlyChild].into()),
+ REMOTE_NETWORK,
+ ),
+ Err(BridgeLocationsError::NonUniversalLocation),
+ );
+ }
+
+ #[test]
+ fn bridge_locations_fails_when_computed_destination_is_local() {
+ assert_eq!(
+ bridge_locations(
+ Box::new([GlobalConsensus(LOCAL_NETWORK)].into()),
+ Box::new(Here.into()),
+ Box::new([GlobalConsensus(LOCAL_NETWORK), OnlyChild].into()),
+ REMOTE_NETWORK,
+ ),
+ Err(BridgeLocationsError::DestinationIsLocal),
+ );
+ }
+
+ #[test]
+ fn bridge_locations_fails_when_computed_destination_is_unreachable() {
+ assert_eq!(
+ bridge_locations(
+ Box::new([GlobalConsensus(LOCAL_NETWORK)].into()),
+ Box::new(Here.into()),
+ Box::new([GlobalConsensus(UNREACHABLE_NETWORK)].into()),
+ REMOTE_NETWORK,
+ ),
+ Err(BridgeLocationsError::UnreachableDestination),
+ );
+ }
+}