// 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 .
//! 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.
//!
//! The `pezpallet_xcm_bridge_hub` pallet is used to manage (open, close) bridges between chains from
//! different consensuses. The new extrinsics `fn open_bridge` and `fn close_bridge` are introduced.
//! Other chains can manage channels with different bridged global consensuses.
//!
//! # Concept of `lane` and `LaneId`
//!
//! There is another `pezpallet_bridge_messages` pallet that handles inbound/outbound lanes for
//! messages. Each lane is a unique connection between two chains from different consensuses and is
//! identified by `LaneId`. `LaneId` is generated once when a new bridge is requested by `fn
//! open_bridge`. It is generated by `BridgeLocations::calculate_lane_id` based on the following
//! parameters:
//! - Source `bridge_origin_universal_location` (latest XCM)
//! - Destination `bridge_destination_universal_location` (latest XCM)
//! - XCM version (both sides of the bridge must use the same parameters to generate the same
//! `LaneId`)
//! - `bridge_origin_universal_location`, `bridge_destination_universal_location` is converted to
//! the `Versioned*` structs
//!
//! `LaneId` is expected to never change because:
//! - We need the same `LaneId` on both sides of the bridge, as `LaneId` is part of the message key
//! proofs.
//! - Runtime upgrades are entirely asynchronous.
//! - We already have a running production Pezkuwi/Kusama bridge that uses `LaneId([0, 0, 0, 0])`.
//!
//! `LaneId` is backward compatible, meaning it can be encoded/decoded from the older format `[u8;
//! 4]` used for static lanes, as well as the new format `H256` generated by
//! `BridgeLocations::calculate_lane_id`.
//!
//! # Concept of `bridge` and `BridgeId`
//!
//! The `pezpallet_xcm_bridge_hub` pallet needs to store some metadata about opened bridges. The bridge
//! (or bridge metadata) is stored under the `BridgeId` key.
//!
//! `BridgeId` is generated from `bridge_origin_relative_location` and
//! `bridge_origin_universal_location` using the `latest` XCM structs. `BridgeId` is not transferred
//! over the bridge; it is only important for local consensus. It essentially serves as an index/key
//! to bridge metadata. All the XCM infrastructure around `XcmExecutor`, `SendXcm`, `ExportXcm` use
//! the `latest` XCM, so `BridgeId` must remain compatible with the `latest` XCM. For example, we
//! have an `ExportXcm` implementation in `exporter.rs` that handles the `ExportMessage` instruction
//! with `universal_source` and `destination` (latest XCM), so we need to create `BridgeId` and the
//! corresponding `LaneId`.
//!
//! # Migrations and State
//!
//! This pallet implements `try_state`, ensuring compatibility and checking everything so we know if
//! any migration is needed. `do_try_state` checks for `BridgeId` compatibility, which is
//! recalculated on runtime upgrade. Upgrading to a new XCM version should not break anything,
//! except removing older XCM versions. In such cases, we need to add migration for `BridgeId` and
//! stored `Versioned*` structs and update `LaneToBridge` mapping, but this won't affect `LaneId`
//! over the bridge.
//!
//! # How to Open a Bridge?
//!
//! The `pezpallet_xcm_bridge_hub` pallet has the extrinsic `fn open_bridge` and an important
//! configuration `pezpallet_xcm_bridge_hub::Config::OpenBridgeOrigin`, which translates the call's
//! origin to the XCM `Location` and converts it to the `bridge_origin_universal_location`. With the
//! current setup, this origin/location is expected to be either the relay chain or a sibling
//! teyrchain as one side of the bridge. Another parameter is
//! `bridge_destination_universal_location`, which is the other side of the bridge from a different
//! global consensus.
//!
//! Every bridge between two XCM locations has a dedicated lane in associated
//! messages pallet. Assuming that this pallet is deployed at the bridge hub
//! teyrchain and there's a similar pallet at the bridged network, the dynamic
//! bridge lifetime is as follows:
//!
//! 1) the sibling teyrchain opens a XCMP channel with this bridge hub;
//!
//! 2) the sibling teyrchain funds its sovereign teyrchain account at this bridge hub. It shall hold
//! enough funds to pay for the bridge (see `BridgeDeposit`);
//!
//! 3) the sibling teyrchain opens the bridge by sending XCM `Transact` instruction with the
//! `open_bridge` call. The `BridgeDeposit` amount is reserved on the sovereign account of
//! sibling teyrchain;
//!
//! 4) at the other side of the bridge, the same thing (1, 2, 3) happens. Teyrchains 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.
//!
//! # Example
//!
//! Example of opening a bridge between some random teyrchains from Pezkuwi and Kusama:
//!
//! 0. Let's have:
//! - BridgeHubPezkuwi with `UniversalLocation` = `[GlobalConsensus(Pezkuwi), Teyrchain(1002)]`
//! - BridgeHubKusama with `UniversalLocation` = `[GlobalConsensus(Kusama), Teyrchain(1002)]`
//! 1. The Pezkuwi local sibling teyrchain `Location::new(1, Teyrchain(1234))` must send some DOTs
//! to its sovereign account on BridgeHubPezkuwi to cover `BridgeDeposit`, fees for `Transact`,
//! and the existential deposit.
//! 2. Send a call to the BridgeHubPezkuwi from the local sibling teyrchain: `Location::new(1,
//! Teyrchain(1234))` ``` xcm::Transact( origin_kind: OriginKind::Xcm,
//! XcmOverBridgeHubKusama::open_bridge( VersionedInteriorLocation::V4([GlobalConsensus(Kusama),
//! Teyrchain(4567)].into()), ); ) ```
//! 3. Check the stored bridge metadata and generated `LaneId`.
//! 4. The Kusama local sibling teyrchain `Location::new(1, Teyrchain(4567))` must send some KSMs to
//! its sovereign account
//! on BridgeHubKusama to cover `BridgeDeposit`, fees for `Transact`, and the existential deposit.
//! 5. Send a call to the BridgeHubKusama from the local sibling teyrchain: `Location::new(1,
//! Teyrchain(4567))` ``` xcm::Transact( origin_kind: OriginKind::Xcm,
//! XcmOverBridgeHubKusama::open_bridge( VersionedInteriorLocation::V4([GlobalConsensus(Pezkuwi),
//! Teyrchain(1234)].into()), ); ) ```
//! 6. Check the stored bridge metadata and generated `LaneId`.
//! 7. Both `LaneId`s from steps 3 and 6 must be the same (see above _Concept of `lane` and
//! `LaneId`_).
//! 8. Run the bridge messages relayer for `LaneId`.
//! 9. Send messages from both sides.
//!
//! The opening bridge holds the configured `BridgeDeposit` from the origin's sovereign account, but
//! this deposit is returned when the bridge is closed with `fn close_bridge`.
#![warn(missing_docs)]
#![cfg_attr(not(feature = "std"), no_std)]
use bp_messages::{LaneState, MessageNonce};
use pezbp_runtime::{AccountIdOf, BalanceOf, RangeInclusiveExt};
use bp_xcm_bridge_hub::BridgeLocationsError;
pub use bp_xcm_bridge_hub::{
Bridge, BridgeId, BridgeLocations, BridgeState, LocalXcmChannelManager,
};
use pezframe_support::{traits::fungible::MutateHold, DefaultNoBound};
use pezframe_system::Config as SystemConfig;
use pezpallet_bridge_messages::{Config as BridgeMessagesConfig, LanesManagerError};
use pezsp_runtime::traits::Zero;
use pezsp_std::{boxed::Box, vec::Vec};
use xcm::prelude::*;
use xcm_builder::DispatchBlob;
use xcm_executor::traits::ConvertLocation;
pub use bp_xcm_bridge_hub::XcmAsPlainPayload;
pub use dispatcher::XcmBlobMessageDispatchResult;
pub use exporter::PalletAsHaulBlobExporter;
pub use pallet::*;
mod dispatcher;
mod exporter;
pub mod migration;
mod mock;
/// The target that will be used when publishing logs related to this pallet.
pub const LOG_TARGET: &str = "runtime::bridge-xcm";
#[pezframe_support::pallet]
pub mod pallet {
use super::*;
use pezframe_support::{
pezpallet_prelude::*,
traits::{tokens::Precision, Contains},
};
use pezframe_system::pezpallet_prelude::{BlockNumberFor, *};
/// The reason for this pallet placing a hold on funds.
#[pallet::composite_enum]
pub enum HoldReason {
/// The funds are held as a deposit for opened bridge.
#[codec(index = 0)]
BridgeDeposit,
}
#[pallet::config]
#[pallet::disable_pezframe_system_supertrait_check]
pub trait Config:
BridgeMessagesConfig
{
/// The overarching event type.
#[allow(deprecated)]
type RuntimeEvent: From>
+ IsType<::RuntimeEvent>;
/// Runtime's universal location.
type UniversalLocation: Get;
// TODO: https://github.com/pezkuwichain/kurdistan-sdk/issues/80 remove `ChainId` and
// replace it with the `NetworkId` - then we'll be able to use
// `T as pezpallet_bridge_messages::Config::BridgedChain::NetworkId`
/// Bridged network as relative location of bridged `GlobalConsensus`.
#[pallet::constant]
type BridgedNetwork: Get;
/// Associated messages pallet instance that bridges us with the
/// `BridgedNetworkId` consensus.
type BridgeMessagesPalletInstance: 'static;
/// Price of single message export to the bridged consensus (`Self::BridgedNetwork`).
type MessageExportPrice: Get;
/// Checks the XCM version for the destination.
type DestinationVersion: GetVersion;
/// The origin that is allowed to call privileged operations on the pallet, e.g. open/close
/// bridge for locations.
type ForceOrigin: EnsureOrigin<::RuntimeOrigin>;
/// A set of XCM locations within local consensus system that are allowed to open
/// bridges with remote destinations.
type OpenBridgeOrigin: EnsureOrigin<
::RuntimeOrigin,
Success = Location,
>;
/// A converter between a location and a sovereign account.
type BridgeOriginAccountIdConverter: ConvertLocation>>;
/// Amount of this chain native tokens that is reserved on the sibling teyrchain account
/// when bridge open request is registered.
#[pallet::constant]
type BridgeDeposit: Get>>;
/// Currency used to pay for bridge registration.
type Currency: MutateHold<
AccountIdOf>,
Balance = BalanceOf>,
Reason = Self::RuntimeHoldReason,
>;
/// The overarching runtime hold reason.
type RuntimeHoldReason: From>;
/// Do not hold `Self::BridgeDeposit` for the location of `Self::OpenBridgeOrigin`.
/// For example, it is possible to make an exception for a system teyrchain or relay.
type AllowWithoutBridgeDeposit: Contains;
/// Local XCM channel manager.
type LocalXcmChannelManager: LocalXcmChannelManager;
/// XCM-level dispatcher for inbound bridge messages.
type BlobDispatcher: DispatchBlob;
}
/// An alias for the bridge metadata.
pub type BridgeOf = Bridge, LaneIdOf>;
/// An alias for this chain.
pub type ThisChainOf =
pezpallet_bridge_messages::ThisChainOf>::BridgeMessagesPalletInstance>;
/// An alias for lane identifier type.
pub type LaneIdOf =
>::BridgeMessagesPalletInstance>>::LaneId;
/// An alias for the associated lanes manager.
pub type LanesManagerOf =
pezpallet_bridge_messages::LanesManager>::BridgeMessagesPalletInstance>;
#[pallet::pallet]
#[pallet::storage_version(migration::STORAGE_VERSION)]
pub struct Pallet(PhantomData<(T, I)>);
#[pallet::hooks]
impl, I: 'static> Hooks> for Pallet {
fn integrity_test() {
assert!(
Self::bridged_network_id().is_ok(),
"Configured `T::BridgedNetwork`: {:?} does not contain `GlobalConsensus` junction with `NetworkId`",
T::BridgedNetwork::get()
)
}
#[cfg(feature = "try-runtime")]
fn try_state(_n: BlockNumberFor) -> Result<(), pezsp_runtime::TryRuntimeError> {
Self::do_try_state()
}
}
#[pallet::call]
impl, I: 'static> Pallet {
/// Open a bridge between two locations.
///
/// The caller must be within the `T::OpenBridgeOrigin` filter (presumably: a sibling
/// teyrchain or a parent relay chain). The `bridge_destination_universal_location` must be
/// a destination within the consensus of the `T::BridgedNetwork` network.
///
/// The `BridgeDeposit` amount is reserved on the caller account. This deposit
/// 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:(bridges-v2) - https://github.com/pezkuwichain/kurdistan-sdk/issues/87 - add benchmarks impl
pub fn open_bridge(
origin: OriginFor,
bridge_destination_universal_location: Box,
) -> DispatchResult {
// check and compute required bridge locations and laneId
let xcm_version = bridge_destination_universal_location.identify_version();
let locations =
Self::bridge_locations_from_origin(origin, bridge_destination_universal_location)?;
let lane_id = locations.calculate_lane_id(xcm_version).map_err(|e| {
tracing::trace!(
target: LOG_TARGET, error=?e,
"calculate_lane_id error"
);
Error::::BridgeLocations(e)
})?;
Self::do_open_bridge(locations, lane_id, true)
}
/// 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:(bridges-v2) - https://github.com/pezkuwichain/kurdistan-sdk/issues/87 - add benchmarks impl
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_from_origin(origin, bridge_destination_universal_location)?;
// TODO: https://github.com/pezkuwichain/kurdistan-sdk/issues/81 - 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.bridge_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(bridge.lane_id)
.map_err(Error::::LanesManager)?;
let mut outbound_lane = lanes_manager
.any_state_outbound_lane(bridge.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().saturating_len();
tracing::trace!(
target: LOG_TARGET,
bridge_id=?locations.bridge_id(),
bridge_origin=?locations.bridge_origin_universal_location(),
bridge_destination=?locations.bridge_destination_universal_location(),
lane_id=?bridge.lane_id,
messages_remaining=%enqueued_messages,
"Bridge is closing."
);
// deposit the `ClosingBridge` event
Self::deposit_event(Event::::ClosingBridge {
bridge_id: *locations.bridge_id(),
lane_id: bridge.lane_id.into(),
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.bridge_id());
LaneToBridge::::remove(bridge.lane_id);
// return deposit
let released_deposit = T::Currency::release(
&HoldReason::BridgeDeposit.into(),
&bridge.bridge_owner_account,
bridge.deposit,
Precision::BestEffort,
)
.inspect_err(|e| {
// 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
tracing::error!(
target: LOG_TARGET,
error=?e,
bridge_id=?locations.bridge_id(),
"Failed to unreserve during the bridge closure"
);
})
.ok()
.unwrap_or(BalanceOf::>::zero());
// write something to log
tracing::trace!(
target: LOG_TARGET,
bridge_id=?locations.bridge_id(),
bridge_origin=?locations.bridge_origin_universal_location(),
bridge_destination=?locations.bridge_destination_universal_location(),
lane_id=?bridge.lane_id,
bridge_deposit=?released_deposit,
"Bridge has closed, the bridge deposit was returned"
);
// deposit the `BridgePruned` event
Self::deposit_event(Event::::BridgePruned {
bridge_id: *locations.bridge_id(),
lane_id: bridge.lane_id.into(),
bridge_deposit: released_deposit,
pruned_messages,
});
Ok(())
}
}
impl, I: 'static> Pallet {
/// Open bridge for lane.
pub fn do_open_bridge(
locations: Box,
lane_id: T::LaneId,
create_lanes: bool,
) -> Result<(), DispatchError> {
// reserve balance on the origin's sovereign account (if needed)
let bridge_owner_account = T::BridgeOriginAccountIdConverter::convert_location(
locations.bridge_origin_relative_location(),
)
.ok_or(Error::::InvalidBridgeOriginAccount)?;
let deposit = if T::AllowWithoutBridgeDeposit::contains(
locations.bridge_origin_relative_location(),
) {
BalanceOf::>::zero()
} else {
let deposit = T::BridgeDeposit::get();
T::Currency::hold(
&HoldReason::BridgeDeposit.into(),
&bridge_owner_account,
deposit,
)
.map_err(|e| {
tracing::error!(
target: LOG_TARGET,
error=?e,
?deposit,
?bridge_owner_account,
bridge_origin_relative_location=?locations.bridge_origin_relative_location(),
"Failed to hold bridge deposit"
);
Error::::FailedToReserveBridgeDeposit
})?;
deposit
};
// save bridge metadata
Bridges::::try_mutate(locations.bridge_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(),
),
bridge_origin_universal_location: Box::new(
locations.bridge_origin_universal_location().clone().into(),
),
bridge_destination_universal_location: Box::new(
locations.bridge_destination_universal_location().clone().into(),
),
state: BridgeState::Opened,
bridge_owner_account,
deposit,
lane_id,
});
Ok(())
},
})?;
// save lane to bridge mapping
LaneToBridge::::try_mutate(lane_id, |bridge| match bridge {
Some(_) => Err(Error::::BridgeAlreadyExists),
None => {
*bridge = Some(*locations.bridge_id());
Ok(())
},
})?;
if create_lanes {
// create new lanes. Under normal circumstances, following calls shall never fail
let lanes_manager = LanesManagerOf::::new();
lanes_manager
.create_inbound_lane(lane_id)
.map_err(Error::::LanesManager)?;
lanes_manager
.create_outbound_lane(lane_id)
.map_err(Error::::LanesManager)?;
}
// write something to log
tracing::trace!(
target: LOG_TARGET,
bridge_id=?locations.bridge_id(),
bridge_origin=?locations.bridge_origin_universal_location(),
bridge_destination=?locations.bridge_destination_universal_location(),
lane_id=?lane_id,
"Bridge has been opened"
);
// deposit `BridgeOpened` event
Self::deposit_event(Event::::BridgeOpened {
bridge_id: *locations.bridge_id(),
bridge_deposit: deposit,
local_endpoint: Box::new(locations.bridge_origin_universal_location().clone()),
remote_endpoint: Box::new(
locations.bridge_destination_universal_location().clone(),
),
lane_id: lane_id.into(),
});
Ok(())
}
}
impl, I: 'static> Pallet {
/// Return bridge endpoint locations and dedicated lane identifier. This method converts
/// runtime `origin` argument to relative `Location` using the `T::OpenBridgeOrigin`
/// converter.
pub fn bridge_locations_from_origin(
origin: OriginFor,
bridge_destination_universal_location: Box,
) -> Result, pezsp_runtime::DispatchError> {
Self::bridge_locations(
T::OpenBridgeOrigin::ensure_origin(origin)?,
(*bridge_destination_universal_location)
.try_into()
.map_err(|_| Error::::UnsupportedXcmVersion)?,
)
}
/// Return bridge endpoint locations and dedicated **bridge** identifier (`BridgeId`).
pub fn bridge_locations(
bridge_origin_relative_location: Location,
bridge_destination_universal_location: InteriorLocation,
) -> Result, pezsp_runtime::DispatchError> {
BridgeLocations::bridge_locations(
T::UniversalLocation::get(),
bridge_origin_relative_location,
bridge_destination_universal_location,
Self::bridged_network_id()?,
)
.map_err(|e| {
tracing::trace!(
target: LOG_TARGET, error=?e,
"bridge_locations error"
);
Error::::BridgeLocations(e).into()
})
}
/// Return bridge metadata by bridge_id
pub fn bridge(bridge_id: &BridgeId) -> Option> {
Bridges::::get(bridge_id)
}
/// Return bridge metadata by lane_id
pub fn bridge_by_lane_id(lane_id: &T::LaneId) -> Option<(BridgeId, BridgeOf)> {
LaneToBridge::::get(lane_id)
.and_then(|bridge_id| Self::bridge(&bridge_id).map(|bridge| (bridge_id, bridge)))
}
}
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()),
}
}
}
#[cfg(any(test, feature = "try-runtime", feature = "std"))]
impl, I: 'static> Pallet {
/// Ensure the correctness of the state of this pallet.
pub fn do_try_state() -> Result<(), pezsp_runtime::TryRuntimeError> {
use pezsp_std::collections::btree_set::BTreeSet;
let mut lanes = BTreeSet::new();
// check all known bridge configurations
for (bridge_id, bridge) in Bridges::::iter() {
lanes.insert(Self::do_try_state_for_bridge(bridge_id, bridge)?);
}
ensure!(
lanes.len() == Bridges::::iter().count(),
"Invalid `Bridges` configuration, probably two bridges handle the same laneId!"
);
ensure!(
lanes.len() == LaneToBridge::::iter().count(),
"Invalid `LaneToBridge` configuration, probably missing or not removed laneId!"
);
// check connected `pezpallet_bridge_messages` state.
Self::do_try_state_for_messages()
}
/// Ensure the correctness of the state of the bridge.
pub fn do_try_state_for_bridge(
bridge_id: BridgeId,
bridge: BridgeOf,
) -> Result {
tracing::info!(target: LOG_TARGET, ?bridge_id, ?bridge, "Checking `do_try_state_for_bridge`");
// check `BridgeId` points to the same `LaneId` and vice versa.
ensure!(
Some(bridge_id) == LaneToBridge::::get(bridge.lane_id),
"Found `LaneToBridge` inconsistency for bridge_id - missing mapping!"
);
// check `pezpallet_bridge_messages` state for that `LaneId`.
let lanes_manager = LanesManagerOf::::new();
ensure!(
lanes_manager.any_state_inbound_lane(bridge.lane_id).is_ok(),
"Inbound lane not found!",
);
ensure!(
lanes_manager.any_state_outbound_lane(bridge.lane_id).is_ok(),
"Outbound lane not found!",
);
// check that `locations` are convertible to the `latest` XCM.
let bridge_origin_relative_location_as_latest: &Location = &(*bridge.bridge_origin_relative_location).try_into()
.map_err(|_| "`bridge.bridge_origin_relative_location` cannot be converted to the `latest` XCM, needs migration!")?;
let bridge_origin_universal_location_as_latest: &InteriorLocation = &(*bridge.bridge_origin_universal_location).try_into()
.map_err(|_| "`bridge.bridge_origin_universal_location` cannot be converted to the `latest` XCM, needs migration!")?;
let bridge_destination_universal_location_as_latest: &InteriorLocation = &(*bridge.bridge_destination_universal_location).try_into()
.map_err(|_| "`bridge.bridge_destination_universal_location` cannot be converted to the `latest` XCM, needs migration!")?;
// check `BridgeId` does not change
ensure!(
bridge_id == BridgeId::new(bridge_origin_universal_location_as_latest, bridge_destination_universal_location_as_latest),
"`bridge_id` is different than calculated from `bridge_origin_universal_location_as_latest` and `bridge_destination_universal_location_as_latest`, needs migration!"
);
// check bridge account owner
ensure!(
T::BridgeOriginAccountIdConverter::convert_location(bridge_origin_relative_location_as_latest) == Some(bridge.bridge_owner_account),
"`bridge.bridge_owner_account` is different than calculated from `bridge.bridge_origin_relative_location`, needs migration!"
);
Ok(bridge.lane_id)
}
/// Ensure the correctness of the state of the connected `pezpallet_bridge_messages` instance.
pub fn do_try_state_for_messages() -> Result<(), pezsp_runtime::TryRuntimeError> {
// check that all `InboundLanes` laneIds have mapping to some bridge.
for lane_id in pezpallet_bridge_messages::InboundLanes::::iter_keys() {
tracing::info!(target: LOG_TARGET, ?lane_id, "Checking `do_try_state_for_messages` for `InboundLanes`...");
ensure!(
LaneToBridge::::get(lane_id).is_some(),
"Found `LaneToBridge` inconsistency for `InboundLanes`'s lane_id - missing mapping!"
);
}
// check that all `OutboundLanes` laneIds have mapping to some bridge.
for lane_id in pezpallet_bridge_messages::OutboundLanes::::iter_keys() {
tracing::info!(target: LOG_TARGET, ?lane_id, "Checking `do_try_state_for_messages` for `OutboundLanes`");
ensure!(
LaneToBridge::::get(lane_id).is_some(),
"Found `LaneToBridge` inconsistency for `OutboundLanes`'s lane_id - missing mapping!"
);
}
Ok(())
}
}
/// All registered bridges.
#[pallet::storage]
pub type Bridges, I: 'static = ()> =
StorageMap<_, Identity, BridgeId, BridgeOf>;
/// All registered `lane_id` and `bridge_id` mappings.
#[pallet::storage]
pub type LaneToBridge, I: 'static = ()> =
StorageMap<_, Identity, T::LaneId, BridgeId>;
#[pallet::genesis_config]
#[derive(DefaultNoBound)]
pub struct GenesisConfig, I: 'static = ()> {
/// Opened bridges.
///
/// Keep in mind that we are **NOT** reserving any amount for the bridges opened at
/// genesis. We are **NOT** opening lanes, used by this bridge. It all must be done using
/// other pallets genesis configuration or some other means.
pub opened_bridges: Vec<(Location, InteriorLocation, Option)>,
/// Dummy marker.
#[serde(skip)]
pub _phantom: pezsp_std::marker::PhantomData<(T, I)>,
}
#[pallet::genesis_build]
impl, I: 'static> BuildGenesisConfig for GenesisConfig
where
T: pezframe_system::Config>>,
{
fn build(&self) {
for (
bridge_origin_relative_location,
bridge_destination_universal_location,
maybe_lane_id,
) in &self.opened_bridges
{
let locations = Pallet::::bridge_locations(
bridge_origin_relative_location.clone(),
bridge_destination_universal_location.clone().into(),
)
.expect("Invalid genesis configuration");
let lane_id = match maybe_lane_id {
Some(lane_id) => *lane_id,
None =>
locations.calculate_lane_id(xcm::latest::VERSION).expect("Valid locations"),
};
Pallet::::do_open_bridge(locations, lane_id, true)
.expect("Valid opened bridge!");
}
}
}
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event, I: 'static = ()> {
/// The bridge between two locations has been opened.
BridgeOpened {
/// Bridge identifier.
bridge_id: BridgeId,
/// Amount of deposit held.
bridge_deposit: BalanceOf>,
/// Universal location of local bridge endpoint.
local_endpoint: Box,
/// Universal location of remote bridge endpoint.
remote_endpoint: Box,
/// Lane identifier.
lane_id: T::LaneId,
},
/// Bridge is going to be closed, but not yet fully pruned from the runtime storage.
ClosingBridge {
/// Bridge identifier.
bridge_id: BridgeId,
/// Lane identifier.
lane_id: T::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 identifier.
bridge_id: BridgeId,
/// Lane identifier.
lane_id: T::LaneId,
/// Amount of deposit released.
bridge_deposit: BalanceOf>,
/// 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,
/// The local origin already owns a maximal number of bridges.
TooManyBridgesForLocalOrigin,
/// 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.
FailedToReserveBridgeDeposit,
/// The version of XCM location argument is unsupported.
UnsupportedXcmVersion,
}
}
#[cfg(test)]
mod tests {
use super::*;
use bp_messages::LaneIdType;
use mock::*;
use pezframe_support::{assert_err, assert_noop, assert_ok, traits::fungible::Mutate, BoundedVec};
use pezframe_system::{EventRecord, Phase};
use pezsp_runtime::TryRuntimeError;
fn fund_origin_sovereign_account(locations: &BridgeLocations, balance: Balance) -> AccountId {
let bridge_owner_account =
LocationToAccountId::convert_location(locations.bridge_origin_relative_location())
.unwrap();
assert_ok!(Balances::mint_into(&bridge_owner_account, balance));
bridge_owner_account
}
fn mock_open_bridge_from_with(
origin: RuntimeOrigin,
deposit: Balance,
with: InteriorLocation,
) -> (BridgeOf, BridgeLocations) {
let locations =
XcmOverBridge::bridge_locations_from_origin(origin, Box::new(with.into())).unwrap();
let lane_id = locations.calculate_lane_id(xcm::latest::VERSION).unwrap();
let bridge_owner_account =
fund_origin_sovereign_account(&locations, deposit + ExistentialDeposit::get());
Balances::hold(&HoldReason::BridgeDeposit.into(), &bridge_owner_account, deposit).unwrap();
let bridge = Bridge {
bridge_origin_relative_location: Box::new(
locations.bridge_origin_relative_location().clone().into(),
),
bridge_origin_universal_location: Box::new(
locations.bridge_origin_universal_location().clone().into(),
),
bridge_destination_universal_location: Box::new(
locations.bridge_destination_universal_location().clone().into(),
),
state: BridgeState::Opened,
bridge_owner_account,
deposit,
lane_id,
};
Bridges::::insert(locations.bridge_id(), bridge.clone());
LaneToBridge::::insert(bridge.lane_id, locations.bridge_id());
let lanes_manager = LanesManagerOf::::new();
lanes_manager.create_inbound_lane(bridge.lane_id).unwrap();
lanes_manager.create_outbound_lane(bridge.lane_id).unwrap();
assert_ok!(XcmOverBridge::do_try_state());
(bridge, *locations)
}
fn mock_open_bridge_from(
origin: RuntimeOrigin,
deposit: Balance,
) -> (BridgeOf, BridgeLocations) {
mock_open_bridge_from_with(origin, deposit, bridged_asset_hub_universal_location())
}
fn enqueue_message(lane: TestLaneIdType) {
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_universal_location().into()),
),
pezsp_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_universal_location().into()),
),
Error::::BridgeLocations(
BridgeLocationsError::InvalidBridgeOrigin
),
);
assert_noop!(
XcmOverBridge::open_bridge(
OpenBridgeOrigin::sibling_teyrchain_universal_origin(),
Box::new(bridged_asset_hub_universal_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()), Teyrchain(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()),
Teyrchain(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_universal_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::sibling_teyrchain_origin(),
Box::new(bridged_asset_hub_universal_location().into()),
),
Error::::FailedToReserveBridgeDeposit,
);
});
}
#[test]
fn open_bridge_fails_if_it_already_exists() {
run_test(|| {
let origin = OpenBridgeOrigin::parent_relay_chain_origin();
let locations = XcmOverBridge::bridge_locations_from_origin(
origin.clone(),
Box::new(bridged_asset_hub_universal_location().into()),
)
.unwrap();
let lane_id = locations.calculate_lane_id(xcm::latest::VERSION).unwrap();
fund_origin_sovereign_account(
&locations,
BridgeDeposit::get() + ExistentialDeposit::get(),
);
Bridges::::insert(
locations.bridge_id(),
Bridge {
bridge_origin_relative_location: Box::new(
locations.bridge_origin_relative_location().clone().into(),
),
bridge_origin_universal_location: Box::new(
locations.bridge_origin_universal_location().clone().into(),
),
bridge_destination_universal_location: Box::new(
locations.bridge_destination_universal_location().clone().into(),
),
state: BridgeState::Opened,
bridge_owner_account: [0u8; 32].into(),
deposit: 0,
lane_id,
},
);
assert_noop!(
XcmOverBridge::open_bridge(
origin,
Box::new(bridged_asset_hub_universal_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_from_origin(
origin.clone(),
Box::new(bridged_asset_hub_universal_location().into()),
)
.unwrap();
let lane_id = locations.calculate_lane_id(xcm::latest::VERSION).unwrap();
fund_origin_sovereign_account(
&locations,
BridgeDeposit::get() + ExistentialDeposit::get(),
);
let lanes_manager = LanesManagerOf::::new();
lanes_manager.create_inbound_lane(lane_id).unwrap();
assert_noop!(
XcmOverBridge::open_bridge(
origin.clone(),
Box::new(bridged_asset_hub_universal_location().into()),
),
Error::::LanesManager(LanesManagerError::InboundLaneAlreadyExists),
);
lanes_manager.active_inbound_lane(lane_id).unwrap().purge();
lanes_manager.create_outbound_lane(lane_id).unwrap();
assert_noop!(
XcmOverBridge::open_bridge(
origin,
Box::new(bridged_asset_hub_universal_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 teyrchain
let origins = [
(OpenBridgeOrigin::parent_relay_chain_origin(), 0),
(OpenBridgeOrigin::sibling_teyrchain_origin(), BridgeDeposit::get()),
];
// check that every origin may open the bridge
let lanes_manager = LanesManagerOf::::new();
let existential_deposit = ExistentialDeposit::get();
for (origin, expected_deposit) in origins {
// reset events
System::set_block_number(1);
System::reset_events();
// compute all other locations
let xcm_version = xcm::latest::VERSION;
let locations = XcmOverBridge::bridge_locations_from_origin(
origin.clone(),
Box::new(
VersionedInteriorLocation::from(bridged_asset_hub_universal_location())
.into_version(xcm_version)
.expect("valid conversion"),
),
)
.unwrap();
let lane_id = locations.calculate_lane_id(xcm_version).unwrap();
// ensure that there's no bridge and lanes in the storage
assert_eq!(Bridges::::get(locations.bridge_id()), None);
assert_eq!(
lanes_manager.active_inbound_lane(lane_id).map(drop),
Err(LanesManagerError::UnknownInboundLane)
);
assert_eq!(
lanes_manager.active_outbound_lane(lane_id).map(drop),
Err(LanesManagerError::UnknownOutboundLane)
);
assert_eq!(LaneToBridge::::get(lane_id), None);
// give enough funds to the sovereign account of the bridge origin
let bridge_owner_account = fund_origin_sovereign_account(
&locations,
expected_deposit + existential_deposit,
);
assert_eq!(
Balances::free_balance(&bridge_owner_account),
expected_deposit + 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.bridge_id()),
Some(Bridge {
bridge_origin_relative_location: Box::new(
locations.bridge_origin_relative_location().clone().into()
),
bridge_origin_universal_location: Box::new(
locations.bridge_origin_universal_location().clone().into(),
),
bridge_destination_universal_location: Box::new(
locations.bridge_destination_universal_location().clone().into(),
),
state: BridgeState::Opened,
bridge_owner_account: bridge_owner_account.clone(),
deposit: expected_deposit,
lane_id
}),
);
assert_eq!(
lanes_manager.active_inbound_lane(lane_id).map(|l| l.state()),
Ok(LaneState::Opened)
);
assert_eq!(
lanes_manager.active_outbound_lane(lane_id).map(|l| l.state()),
Ok(LaneState::Opened)
);
assert_eq!(
LaneToBridge::::get(lane_id),
Some(*locations.bridge_id())
);
assert_eq!(Balances::free_balance(&bridge_owner_account), existential_deposit);
assert_eq!(Balances::reserved_balance(&bridge_owner_account), expected_deposit);
// ensure that the proper event is deposited
assert_eq!(
System::events().last(),
Some(&EventRecord {
phase: Phase::Initialization,
event: RuntimeEvent::XcmOverBridge(Event::BridgeOpened {
bridge_id: *locations.bridge_id(),
bridge_deposit: expected_deposit,
local_endpoint: Box::new(
locations.bridge_origin_universal_location().clone()
),
remote_endpoint: Box::new(
locations.bridge_destination_universal_location().clone()
),
lane_id: lane_id.into()
}),
topics: vec![],
}),
);
// check state
assert_ok!(XcmOverBridge::do_try_state());
}
});
}
#[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_universal_location().into()),
0,
),
pezsp_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_universal_location().into()),
0,
),
Error::::BridgeLocations(
BridgeLocationsError::InvalidBridgeOrigin
),
);
assert_noop!(
XcmOverBridge::close_bridge(
OpenBridgeOrigin::sibling_teyrchain_universal_origin(),
Box::new(bridged_asset_hub_universal_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 (bridge, locations) = mock_open_bridge_from(origin.clone(), 0);
let lanes_manager = LanesManagerOf::::new();
lanes_manager.any_state_inbound_lane(bridge.lane_id).unwrap().purge();
assert_noop!(
XcmOverBridge::close_bridge(
origin.clone(),
Box::new(locations.bridge_destination_universal_location().clone().into()),
0,
),
Error::::LanesManager(LanesManagerError::UnknownInboundLane),
);
lanes_manager.any_state_outbound_lane(bridge.lane_id).unwrap().purge();
let (_, locations) = mock_open_bridge_from(origin.clone(), 0);
lanes_manager.any_state_outbound_lane(bridge.lane_id).unwrap().purge();
assert_noop!(
XcmOverBridge::close_bridge(
origin,
Box::new(locations.bridge_destination_universal_location().clone().into()),
0,
),
Error::::LanesManager(LanesManagerError::UnknownOutboundLane),
);
});
}
#[test]
fn close_bridge_works() {
run_test(|| {
let origin = OpenBridgeOrigin::parent_relay_chain_origin();
let expected_deposit = BridgeDeposit::get();
let (bridge, locations) = mock_open_bridge_from(origin.clone(), expected_deposit);
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(bridge.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.bridge_id()).map(|b| b.state),
Some(BridgeState::Closed)
);
assert_eq!(
lanes_manager.any_state_inbound_lane(bridge.lane_id).unwrap().state(),
LaneState::Closed
);
assert_eq!(
lanes_manager.any_state_outbound_lane(bridge.lane_id).unwrap().state(),
LaneState::Closed
);
assert_eq!(
lanes_manager
.any_state_outbound_lane(bridge.lane_id)
.unwrap()
.queued_messages()
.checked_len(),
Some(16)
);
assert_eq!(
LaneToBridge::::get(bridge.lane_id),
Some(*locations.bridge_id())
);
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 {
bridge_id: *locations.bridge_id(),
lane_id: bridge.lane_id.into(),
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.bridge_id()).map(|b| b.state),
Some(BridgeState::Closed)
);
assert_eq!(
lanes_manager.any_state_inbound_lane(bridge.lane_id).unwrap().state(),
LaneState::Closed
);
assert_eq!(
lanes_manager.any_state_outbound_lane(bridge.lane_id).unwrap().state(),
LaneState::Closed
);
assert_eq!(
lanes_manager
.any_state_outbound_lane(bridge.lane_id)
.unwrap()
.queued_messages()
.checked_len(),
Some(8)
);
assert_eq!(
LaneToBridge::::get(bridge.lane_id),
Some(*locations.bridge_id())
);
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 {
bridge_id: *locations.bridge_id(),
lane_id: bridge.lane_id.into(),
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().clone().into()),
9,
),);
// there's no traces of bridge in the runtime storage and funds are unreserved
assert_eq!(
Bridges::::get(locations.bridge_id()).map(|b| b.state),
None
);
assert_eq!(
lanes_manager.any_state_inbound_lane(bridge.lane_id).map(drop),
Err(LanesManagerError::UnknownInboundLane)
);
assert_eq!(
lanes_manager.any_state_outbound_lane(bridge.lane_id).map(drop),
Err(LanesManagerError::UnknownOutboundLane)
);
assert_eq!(LaneToBridge::::get(bridge.lane_id), None);
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 {
bridge_id: *locations.bridge_id(),
lane_id: bridge.lane_id.into(),
bridge_deposit: expected_deposit,
pruned_messages: 8,
}),
topics: vec![],
}),
);
});
}
#[test]
fn do_try_state_works() {
let bridge_origin_relative_location = SiblingLocation::get();
let bridge_origin_universal_location = SiblingUniversalLocation::get();
let bridge_destination_universal_location = BridgedUniversalDestination::get();
let bridge_owner_account =
LocationToAccountId::convert_location(&bridge_origin_relative_location)
.expect("valid accountId");
let bridge_owner_account_mismatch =
LocationToAccountId::convert_location(&Location::parent()).expect("valid accountId");
let bridge_id = BridgeId::new(
&bridge_origin_universal_location,
&bridge_destination_universal_location,
);
let bridge_id_mismatch = BridgeId::new(&InteriorLocation::Here, &InteriorLocation::Here);
let lane_id = TestLaneIdType::try_new(1, 2).unwrap();
let lane_id_mismatch = TestLaneIdType::try_new(3, 4).unwrap();
let test_bridge_state =
|id,
bridge,
(lane_id, bridge_id),
(inbound_lane_id, outbound_lane_id),
expected_error: Option| {
Bridges::::insert(id, bridge);
LaneToBridge::::insert(lane_id, bridge_id);
let lanes_manager = LanesManagerOf::::new();
lanes_manager.create_inbound_lane(inbound_lane_id).unwrap();
lanes_manager.create_outbound_lane(outbound_lane_id).unwrap();
let result = XcmOverBridge::do_try_state();
if let Some(e) = expected_error {
assert_err!(result, e);
} else {
assert_ok!(result);
}
};
let cleanup = |bridge_id, lane_ids| {
Bridges::::remove(bridge_id);
for lane_id in lane_ids {
LaneToBridge::::remove(lane_id);
let lanes_manager = LanesManagerOf::::new();
if let Ok(lane) = lanes_manager.any_state_inbound_lane(lane_id) {
lane.purge();
}
if let Ok(lane) = lanes_manager.any_state_outbound_lane(lane_id) {
lane.purge();
}
}
assert_ok!(XcmOverBridge::do_try_state());
};
run_test(|| {
// ok state
test_bridge_state(
bridge_id,
Bridge {
bridge_origin_relative_location: Box::new(VersionedLocation::from(
bridge_origin_relative_location.clone(),
)),
bridge_origin_universal_location: Box::new(VersionedInteriorLocation::from(
bridge_origin_universal_location.clone(),
)),
bridge_destination_universal_location: Box::new(
VersionedInteriorLocation::from(
bridge_destination_universal_location.clone(),
),
),
state: BridgeState::Opened,
bridge_owner_account: bridge_owner_account.clone(),
deposit: Zero::zero(),
lane_id,
},
(lane_id, bridge_id),
(lane_id, lane_id),
None,
);
cleanup(bridge_id, vec![lane_id]);
// error - missing `LaneToBridge` mapping
test_bridge_state(
bridge_id,
Bridge {
bridge_origin_relative_location: Box::new(VersionedLocation::from(
bridge_origin_relative_location.clone(),
)),
bridge_origin_universal_location: Box::new(VersionedInteriorLocation::from(
bridge_origin_universal_location.clone(),
)),
bridge_destination_universal_location: Box::new(
VersionedInteriorLocation::from(
bridge_destination_universal_location.clone(),
),
),
state: BridgeState::Opened,
bridge_owner_account: bridge_owner_account.clone(),
deposit: Zero::zero(),
lane_id,
},
(lane_id, bridge_id_mismatch),
(lane_id, lane_id),
Some(TryRuntimeError::Other(
"Found `LaneToBridge` inconsistency for bridge_id - missing mapping!",
)),
);
cleanup(bridge_id, vec![lane_id]);
// error bridge owner account cannot be calculated
test_bridge_state(
bridge_id,
Bridge {
bridge_origin_relative_location: Box::new(VersionedLocation::from(
bridge_origin_relative_location.clone(),
)),
bridge_origin_universal_location: Box::new(VersionedInteriorLocation::from(
bridge_origin_universal_location.clone(),
)),
bridge_destination_universal_location: Box::new(VersionedInteriorLocation::from(
bridge_destination_universal_location.clone(),
)),
state: BridgeState::Opened,
bridge_owner_account: bridge_owner_account_mismatch.clone(),
deposit: Zero::zero(),
lane_id,
},
(lane_id, bridge_id),
(lane_id, lane_id),
Some(TryRuntimeError::Other("`bridge.bridge_owner_account` is different than calculated from `bridge.bridge_origin_relative_location`, needs migration!")),
);
cleanup(bridge_id, vec![lane_id]);
// error when (bridge_origin_universal_location + bridge_destination_universal_location)
// produces different `BridgeId`
test_bridge_state(
bridge_id_mismatch,
Bridge {
bridge_origin_relative_location: Box::new(VersionedLocation::from(
bridge_origin_relative_location.clone(),
)),
bridge_origin_universal_location: Box::new(VersionedInteriorLocation::from(
bridge_origin_universal_location.clone(),
)),
bridge_destination_universal_location: Box::new(VersionedInteriorLocation::from(
bridge_destination_universal_location.clone(),
)),
state: BridgeState::Opened,
bridge_owner_account: bridge_owner_account_mismatch.clone(),
deposit: Zero::zero(),
lane_id,
},
(lane_id, bridge_id_mismatch),
(lane_id, lane_id),
Some(TryRuntimeError::Other("`bridge_id` is different than calculated from `bridge_origin_universal_location_as_latest` and `bridge_destination_universal_location_as_latest`, needs migration!")),
);
cleanup(bridge_id_mismatch, vec![lane_id]);
// missing inbound lane for a bridge
test_bridge_state(
bridge_id,
Bridge {
bridge_origin_relative_location: Box::new(VersionedLocation::from(
bridge_origin_relative_location.clone(),
)),
bridge_origin_universal_location: Box::new(VersionedInteriorLocation::from(
bridge_origin_universal_location.clone(),
)),
bridge_destination_universal_location: Box::new(
VersionedInteriorLocation::from(
bridge_destination_universal_location.clone(),
),
),
state: BridgeState::Opened,
bridge_owner_account: bridge_owner_account.clone(),
deposit: Zero::zero(),
lane_id,
},
(lane_id, bridge_id),
(lane_id_mismatch, lane_id),
Some(TryRuntimeError::Other("Inbound lane not found!")),
);
cleanup(bridge_id, vec![lane_id, lane_id_mismatch]);
// missing outbound lane for a bridge
test_bridge_state(
bridge_id,
Bridge {
bridge_origin_relative_location: Box::new(VersionedLocation::from(
bridge_origin_relative_location.clone(),
)),
bridge_origin_universal_location: Box::new(VersionedInteriorLocation::from(
bridge_origin_universal_location.clone(),
)),
bridge_destination_universal_location: Box::new(
VersionedInteriorLocation::from(
bridge_destination_universal_location.clone(),
),
),
state: BridgeState::Opened,
bridge_owner_account: bridge_owner_account.clone(),
deposit: Zero::zero(),
lane_id,
},
(lane_id, bridge_id),
(lane_id, lane_id_mismatch),
Some(TryRuntimeError::Other("Outbound lane not found!")),
);
cleanup(bridge_id, vec![lane_id, lane_id_mismatch]);
// ok state with old XCM version
test_bridge_state(
bridge_id,
Bridge {
bridge_origin_relative_location: Box::new(
VersionedLocation::from(bridge_origin_relative_location.clone())
.into_version(XCM_VERSION - 1)
.unwrap(),
),
bridge_origin_universal_location: Box::new(
VersionedInteriorLocation::from(bridge_origin_universal_location.clone())
.into_version(XCM_VERSION - 1)
.unwrap(),
),
bridge_destination_universal_location: Box::new(
VersionedInteriorLocation::from(
bridge_destination_universal_location.clone(),
)
.into_version(XCM_VERSION - 1)
.unwrap(),
),
state: BridgeState::Opened,
bridge_owner_account: bridge_owner_account.clone(),
deposit: Zero::zero(),
lane_id,
},
(lane_id, bridge_id),
(lane_id, lane_id),
None,
);
cleanup(bridge_id, vec![lane_id]);
// missing bridge for inbound lane
let lanes_manager = LanesManagerOf::::new();
assert!(lanes_manager.create_inbound_lane(lane_id).is_ok());
assert_err!(XcmOverBridge::do_try_state(), TryRuntimeError::Other("Found `LaneToBridge` inconsistency for `InboundLanes`'s lane_id - missing mapping!"));
cleanup(bridge_id, vec![lane_id]);
// missing bridge for outbound lane
let lanes_manager = LanesManagerOf::::new();
assert!(lanes_manager.create_outbound_lane(lane_id).is_ok());
assert_err!(XcmOverBridge::do_try_state(), TryRuntimeError::Other("Found `LaneToBridge` inconsistency for `OutboundLanes`'s lane_id - missing mapping!"));
cleanup(bridge_id, vec![lane_id]);
});
}
#[test]
fn ensure_encoding_compatibility() {
use codec::Encode;
let bridge_destination_universal_location = BridgedUniversalDestination::get();
let may_prune_messages = 13;
assert_eq!(
bp_xcm_bridge_hub::XcmBridgeHubCall::open_bridge {
bridge_destination_universal_location: Box::new(
bridge_destination_universal_location.clone().into()
)
}
.encode(),
Call::::open_bridge {
bridge_destination_universal_location: Box::new(
bridge_destination_universal_location.clone().into()
)
}
.encode()
);
assert_eq!(
bp_xcm_bridge_hub::XcmBridgeHubCall::close_bridge {
bridge_destination_universal_location: Box::new(
bridge_destination_universal_location.clone().into()
),
may_prune_messages,
}
.encode(),
Call::::close_bridge {
bridge_destination_universal_location: Box::new(
bridge_destination_universal_location.clone().into()
),
may_prune_messages,
}
.encode()
);
}
}