From 2b8173e2d7e429dd4087f42cb0910335eb2a356d Mon Sep 17 00:00:00 2001 From: Svyatoslav Nikolsky Date: Mon, 17 Jul 2023 12:50:38 +0300 Subject: [PATCH] XCM bridge hub pallet (will be deployed at bridge hubs) (#2213) * XCM over bridge pallet (won't ever be merged to this repo): initial commit * try both bridge-id and lane-id * flush * flush * flush * flush * first prototype * started working on xcm-over-bridge tests * proper open_bridge_works test * more open_bridge tests * request_bridge_closure tests * tests for close_bridge * report_misbehavior tests * renaming xcm-over-bridge ad xcm-bridge-hub: we'll have two similar pallets for dynamic lanes/fees support. One will be deployed at the bridge hub (xcm-bridge-hub) and another one (xcm-bridge-hub-router) at sibling/parent chains to send messages to remote network over the bridge hub * added couple of TODOs * moved BridgeQueuesState here + TODO for implementing report_bridge_queues_state + fmt * fixing TODOs * fixing TODOs * moved bridge locations to primitives * added a couple of TODOs * ref issue from TODO * clippy and spelling * fix tests * another TODOs * typo * LaneState -> force_close_bridge * start_closing_the_bridge -> start_closing_bridge * removed Closing bridge state, so now we only have the Opened -> optional temporary Closed state -> None * spelling * started prototyping suspend+resume on misbehavior * continue prototyping * more tests for open_bridge * more tests for close_bridge * more tests for report_misbehavior * started tests for resume_misbehaving_bridges * implemented tests for resume_misbehaving_bridges * remove UnsuspendedChannelWithMisbehavingOwner because now, when all bridges are resumed at once + we assume that the inbound XCM channel is suspended immediately it is no longer actual * added TODO * add comment re misbehavior reporter if he's associated with the bridge owner * ignored clippy * fmt * use versioned XCM structs in public interfaces and storage + Box XCM locations where possible * spelling * removed TODO in favor of https://github.com/paritytech/parity-bridges-common/pull/2257 * LocalChannelManager -> LocalXcmChannelManager * removed code specific to #2233, because it isn't the only option now * removemisbehavior mentions * also temporary (?) remove BridgesByLocalOrigin because the storage format will likely change to be able to resume bridges from the on_iniitalize/on_idle * AllowedOpenBridgeOrigin -> OpenBridgeOrigin * - TooManyBridgesForLocalOrigin * use bridge_destination_relative_location in args * better impl of strip_low_level_junctions * get rid of xcm_into_latest * clippy * added #![warn(missing_docs)] to new crates --- Cargo.lock | 6 + bridges/modules/xcm-bridge-hub/Cargo.toml | 7 +- bridges/modules/xcm-bridge-hub/src/lib.rs | 929 ++++++++++++++++++- bridges/modules/xcm-bridge-hub/src/mock.rs | 111 ++- bridges/primitives/header-chain/src/lib.rs | 13 - bridges/primitives/messages/src/lib.rs | 6 +- bridges/primitives/xcm-bridge-hub/Cargo.toml | 14 +- bridges/primitives/xcm-bridge-hub/src/lib.rs | 433 ++++++++- 8 files changed, 1470 insertions(+), 49 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8d585954c6f01..9ce5af13be74c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2007,6 +2007,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", ] @@ -12002,6 +12007,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 8fbf61a0a5354..2cb8d89722e76 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 60b988497fc59..af3709b5a2526 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 2306174f63fe3..673a5790ec082 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 509e2d0627043..26295dee1801a 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 540b6a37d7943..b6d8660c08d26 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 b990a232bff1a..fa8f9ef0b76f2 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 78f913273370d..44dbb96982414 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), + ); + } +}