diff --git a/Cargo.lock b/Cargo.lock index bfb003cc6ea..b550e2f218e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1143,6 +1143,12 @@ dependencies = [ name = "bp-xcm-bridge-hub" version = "0.1.0" dependencies = [ + "bp-messages", + "bp-runtime", + "frame-support", + "parity-scale-codec", + "scale-info", + "sp-std 8.0.0 (git+https://github.com/paritytech/substrate?branch=master)", "xcm", ] @@ -7649,6 +7655,30 @@ dependencies = [ "xcm-executor", ] +[[package]] +name = "pallet-xcm-bridge-hub" +version = "0.1.0" +dependencies = [ + "bp-messages", + "bp-runtime", + "bp-xcm-bridge-hub", + "frame-support", + "frame-system", + "log", + "pallet-balances", + "pallet-bridge-messages", + "parity-scale-codec", + "polkadot-parachain", + "scale-info", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std 8.0.0 (git+https://github.com/paritytech/substrate?branch=master)", + "xcm", + "xcm-builder", + "xcm-executor", +] + [[package]] name = "parachain-info" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 4bb46976560..5c232f7907a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ members = [ "modules/parachains", "modules/relayers", "modules/shift-session-manager", + "modules/xcm-bridge-hub", "primitives/beefy", "primitives/chain-bridge-hub-cumulus", "primitives/chain-bridge-hub-kusama", diff --git a/modules/xcm-bridge-hub/Cargo.toml b/modules/xcm-bridge-hub/Cargo.toml new file mode 100644 index 00000000000..3d50b04c354 --- /dev/null +++ b/modules/xcm-bridge-hub/Cargo.toml @@ -0,0 +1,57 @@ +[package] +name = "pallet-xcm-bridge-hub" +description = "Module that adds dynamic bridges/lanes support to XCM infrastucture at the bridge hub." +version = "0.1.0" +authors = ["Parity Technologies "] +edition = "2021" +license = "GPL-3.0-or-later WITH Classpath-exception-2.0" + +[dependencies] +codec = { package = "parity-scale-codec", version = "3.1.5", default-features = false } +log = { version = "0.4.19", default-features = false } +scale-info = { version = "2.7.0", default-features = false, features = ["derive"] } + +# Bridge Dependencies + +bp-messages = { path = "../../primitives/messages", default-features = false } +bp-runtime = { path = "../../primitives/runtime", default-features = false } +bp-xcm-bridge-hub = { path = "../../primitives/xcm-bridge-hub", default-features = false } +pallet-bridge-messages = { path = "../messages", default-features = false } + +# Substrate Dependencies + +frame-support = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false } +frame-system = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false } +sp-core = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false } +sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false } +sp-std = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false } + +# Polkadot Dependencies + +xcm = { git = "https://github.com/paritytech/polkadot", branch = "master", default-features = false } +xcm-executor = { git = "https://github.com/paritytech/polkadot", branch = "master", default-features = false } + +[dev-dependencies] +pallet-balances = { git = "https://github.com/paritytech/substrate", branch = "master" } +polkadot-parachain = { git = "https://github.com/paritytech/polkadot", branch = "master" } +sp-io = { git = "https://github.com/paritytech/substrate", branch = "master" } +xcm-builder = { git = "https://github.com/paritytech/polkadot", branch = "master" } + +[features] +default = ["std"] +std = [ + "bp-messages/std", + "bp-runtime/std", + "codec/std", + "frame-support/std", + "frame-system/std", + "log/std", + "pallet-bridge-messages/std", + "scale-info/std", + "sp-core/std", + "sp-runtime/std", + "sp-std/std", + "xcm/std", + "xcm-executor/std", +] +runtime-benchmarks = [] diff --git a/modules/xcm-bridge-hub/src/lib.rs b/modules/xcm-bridge-hub/src/lib.rs new file mode 100644 index 00000000000..142039a2302 --- /dev/null +++ b/modules/xcm-bridge-hub/src/lib.rs @@ -0,0 +1,959 @@ +// 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. +//! +//! 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 bp_messages::{LaneId, LaneState, MessageNonce}; +use bp_runtime::{AccountIdOf, BalanceOf, BlockNumberOf, 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; + +pub use pallet::*; + +mod mock; + +/// The target that will be used when publishing logs related to this pallet. +pub const LOG_TARGET: &str = "runtime::bridge-xcm"; + +#[frame_support::pallet] +pub mod pallet { + use super::*; + use frame_support::pallet_prelude::*; + use frame_system::pallet_prelude::*; + + #[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 + // replace it with the `NetworkId` - then we'll be able to use + // `T as pallet_bridge_messages::Config::BridgedChain::NetworkId` + /// Bridged network id. + #[pallet::constant] + type BridgedNetworkId: Get; + /// Associated messages pallet instance that bridges us with the + /// `BridgedNetworkId` consensus. + type BridgeMessagesPalletInstance: 'static; + + /// 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 `MultiLocation`??? + type OpenBridgeOrigin: EnsureOrigin< + ::RuntimeOrigin, + Success = MultiLocation, + >; + /// 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)>); + + #[pallet::call] + impl, I: 'static> Pallet + where + T: frame_system::Config< + AccountId = AccountIdOf>, + BlockNumber = BlockNumberOf>, + >, + 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::BridgedNetworkId` 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)?; + + // 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.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< + AccountId = AccountIdOf>, + BlockNumber = BlockNumberOf>, + >, + 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)?, + ), + T::BridgedNetworkId::get(), + ) + .map_err(|e| Error::::BridgeLocations(e).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, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use mock::*; + + use frame_support::{assert_noop, assert_ok, traits::fungible::Mutate}; + 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: InteriorMultiLocation, + ) -> (BridgeOf, BridgeLocations) { + let reserve = BridgeReserve::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.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(vec![42]) + .unwrap(); + } + + #[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( + X2(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( + X2( + 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, + BridgeReserve::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, + BridgeReserve::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 = BridgeReserve::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.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.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.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/modules/xcm-bridge-hub/src/mock.rs b/modules/xcm-bridge-hub/src/mock.rs new file mode 100644 index 00000000000..54c6627ad7b --- /dev/null +++ b/modules/xcm-bridge-hub/src/mock.rs @@ -0,0 +1,305 @@ +// 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 . + +#![cfg(test)] + +use crate as pallet_xcm_bridge_hub; + +use bp_messages::{target_chain::ForbidInboundMessages, ChainWithMessages, MessageNonce}; +use bp_runtime::{Chain, ChainId}; +use codec::Encode; +use frame_support::{ + parameter_types, + traits::{EnsureOrigin, OriginTrait}, + weights::RuntimeDbWeight, + StateVersion, +}; +use polkadot_parachain::primitives::Sibling; +use sp_core::H256; +use sp_runtime::{ + testing::Header as SubstrateHeader, + traits::{BlakeTwo256, ConstU32, IdentityLookup}, + AccountId32, +}; +use xcm::prelude::*; +use xcm_builder::{ParentIsPreset, SiblingParachainConvertsVia}; + +pub type AccountId = AccountId32; +pub type Balance = u64; +pub type BlockNumber = u64; + +type Block = frame_system::mocking::MockBlock; +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; + +pub const SIBLING_ASSET_HUB_ID: u32 = 2001; +pub const THIS_BRIDGE_HUB_ID: u32 = 2002; +pub const BRIDGED_ASSET_HUB_ID: u32 = 1001; + +frame_support::construct_runtime! { + pub enum TestRuntime where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + 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, Call, Event}, + } +} + +parameter_types! { + pub const DbWeight: RuntimeDbWeight = RuntimeDbWeight { read: 1, write: 2 }; + pub const ExistentialDeposit: Balance = 1; +} + +impl frame_system::Config for TestRuntime { + type RuntimeOrigin = RuntimeOrigin; + type Index = u64; + type RuntimeCall = RuntimeCall; + type BlockNumber = BlockNumber; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type Header = SubstrateHeader; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = frame_support::traits::ConstU64<250>; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type BaseCallFilter = frame_support::traits::Everything; + type SystemWeightInfo = (); + type BlockWeights = (); + type BlockLength = (); + type DbWeight = DbWeight; + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; +} + +impl pallet_balances::Config for TestRuntime { + type MaxLocks = (); + type Balance = Balance; + type DustRemoval = (); + type RuntimeEvent = RuntimeEvent; + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = frame_system::Pallet; + type WeightInfo = (); + type MaxReserves = ConstU32<1>; + type ReserveIdentifier = [u8; 8]; + type RuntimeHoldReason = RuntimeHoldReason; + type FreezeIdentifier = (); + type MaxHolds = ConstU32<0>; + type MaxFreezes = ConstU32<0>; +} + +impl pallet_bridge_messages::Config for TestRuntime { + type RuntimeEvent = RuntimeEvent; + type WeightInfo = (); + + type ThisChain = ThisChain; + type BridgedChain = BridgedChain; + type BridgedHeaderChain = (); + + type OutboundPayload = Vec; + type InboundPayload = Vec; + type DeliveryPayments = (); + type DeliveryConfirmationPayments = (); + type MessageDispatch = ForbidInboundMessages>; +} + +parameter_types! { + pub const RelayNetwork: NetworkId = NetworkId::Kusama; + pub const BridgedRelayNetwork: NetworkId = NetworkId::Polkadot; + pub const NonBridgedRelayNetwork: NetworkId = NetworkId::Rococo; + pub const BridgeReserve: Balance = 100_000; + pub UniversalLocation: InteriorMultiLocation = X2( + GlobalConsensus(RelayNetwork::get()), + Parachain(THIS_BRIDGE_HUB_ID), + ); +} + +/// Type for specifying how a `MultiLocation` 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 = MultiLocation; + + fn try_origin(o: RuntimeOrigin) -> Result { + let signer = o.clone().into_signer(); + if signer == Self::parent_relay_chain_origin().into_signer() { + return Ok(MultiLocation { parents: 1, interior: Here }) + } else if signer == Self::parent_relay_chain_universal_origin().into_signer() { + return Ok(MultiLocation { + parents: 2, + interior: X1(GlobalConsensus(RelayNetwork::get())), + }) + } else if signer == Self::sibling_parachain_universal_origin().into_signer() { + return Ok(MultiLocation { + parents: 2, + interior: X2(GlobalConsensus(RelayNetwork::get()), Parachain(SIBLING_ASSET_HUB_ID)), + }) + } else if signer == Self::origin_without_sovereign_account().into_signer() { + return Ok(MultiLocation { + parents: 1, + interior: X2(Parachain(SIBLING_ASSET_HUB_ID), OnlyChild), + }) + } + + 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(MultiLocation { parents: 1, interior: X1(Parachain(SIBLING_ASSET_HUB_ID)) }) + } + + Err(o) + } + + #[cfg(feature = "runtime-benchmarks")] + fn try_successful_origin() -> Result { + Ok(Self::parent_relay_chain_origin()) + } +} + +impl pallet_xcm_bridge_hub::Config for TestRuntime { + type RuntimeEvent = RuntimeEvent; + + type UniversalLocation = UniversalLocation; + type BridgedNetworkId = BridgedRelayNetwork; + type BridgeMessagesPalletInstance = (); + + type OpenBridgeOrigin = OpenBridgeOrigin; + type BridgeOriginAccountIdConverter = LocationToAccountId; + + type BridgeReserve = BridgeReserve; + type NativeCurrency = Balances; +} + +pub struct ThisChain; + +impl Chain for ThisChain { + const ID: ChainId = *b"ttch"; + + type BlockNumber = u64; + type Hash = H256; + type Hasher = BlakeTwo256; + type Header = SubstrateHeader; + type AccountId = AccountId; + type Balance = Balance; + type Index = u64; + type Signature = sp_runtime::MultiSignature; + const STATE_VERSION: StateVersion = StateVersion::V1; + + fn max_extrinsic_size() -> u32 { + u32::MAX + } + + fn max_extrinsic_weight() -> Weight { + Weight::MAX + } +} + +impl ChainWithMessages for ThisChain { + 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 = 128; +} + +pub struct BridgedChain; + +pub type BridgedHeaderHash = H256; +pub type BridgedChainHeader = SubstrateHeader; + +impl Chain for BridgedChain { + const ID: ChainId = *b"tbch"; + + type BlockNumber = u64; + type Hash = BridgedHeaderHash; + type Hasher = BlakeTwo256; + type Header = BridgedChainHeader; + type AccountId = AccountId; + type Balance = Balance; + type Index = u64; + type Signature = sp_runtime::MultiSignature; + const STATE_VERSION: StateVersion = StateVersion::V1; + + fn max_extrinsic_size() -> u32 { + 4096 + } + + fn max_extrinsic_weight() -> Weight { + Weight::MAX + } +} + +impl ChainWithMessages for BridgedChain { + 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 = 128; +} + +/// Location of bridged asset hub. +pub fn bridged_asset_hub_location() -> InteriorMultiLocation { + X2(GlobalConsensus(BridgedRelayNetwork::get()), Parachain(BRIDGED_ASSET_HUB_ID)) +} + +/// Run pallet test. +pub fn run_test(test: impl FnOnce() -> T) -> T { + sp_io::TestExternalities::new( + frame_system::GenesisConfig::default().build_storage::().unwrap(), + ) + .execute_with(test) +} diff --git a/primitives/messages/src/lib.rs b/primitives/messages/src/lib.rs index 34c40fff68f..50f64852ce4 100644 --- a/primitives/messages/src/lib.rs +++ b/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/primitives/xcm-bridge-hub/Cargo.toml b/primitives/xcm-bridge-hub/Cargo.toml index dcd65545faa..5eda536f76e 100644 --- a/primitives/xcm-bridge-hub/Cargo.toml +++ b/primitives/xcm-bridge-hub/Cargo.toml @@ -7,6 +7,19 @@ edition = "2018" license = "GPL-3.0-or-later WITH Classpath-exception-2.0" [dependencies] +codec = { package = "parity-scale-codec", version = "3.1.5", default-features = false, features = ["derive"] } +scale-info = { version = "2.6.0", default-features = false, features = ["derive"] } + +# Bridge Dependencies + +bp-messages = { path = "../messages", default-features = false } +bp-runtime = { path = "../runtime", default-features = false } + +# Substrate Dependencies + +frame-support = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false } +sp-std = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false } + # Polkadot Dependencies xcm = { git = "https://github.com/paritytech/polkadot", branch = "master", default-features = false } @@ -14,5 +27,11 @@ xcm = { git = "https://github.com/paritytech/polkadot", branch = "master", defau [features] default = ["std"] std = [ + "bp-messages/std", + "bp-runtime/std", + "codec/std", + "frame-support/std", + "scale-info/std", + "sp-std/std", "xcm/std", -] \ No newline at end of file +] diff --git a/primitives/xcm-bridge-hub/src/lib.rs b/primitives/xcm-bridge-hub/src/lib.rs index fa37e2aa6c0..a9f3b88c9ba 100644 --- a/primitives/xcm-bridge-hub/src/lib.rs +++ b/primitives/xcm-bridge-hub/src/lib.rs @@ -16,9 +16,18 @@ //! 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, CloneNoBound, PalletError, PartialEqNoBound, RuntimeDebug, RuntimeDebugNoBound, +}; +use scale_info::TypeInfo; +use sp_std::boxed::Box; +use xcm::{latest::prelude::*, VersionedMultiLocation}; /// A manager of XCM communication channels between the bridge hub and parent/sibling chains /// that have opened bridges at this bridge hub. @@ -41,8 +50,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; @@ -56,9 +64,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: MultiLocation) -> Result<(), ()>; @@ -73,3 +80,410 @@ 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: MultiLocation, + /// Universal (unique) location of this side of the bridge. + pub bridge_origin_universal_location: InteriorMultiLocation, + /// Universal (unique) location of other side of the bridge. + pub bridge_destination_universal_location: InteriorMultiLocation, + /// 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: InteriorMultiLocation, + ) -> 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) => X2(global_consensus, parachain), + None => X1(global_consensus), + }) + } + + // 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) + .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, bridge_destination_universal_location); + + 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: InteriorMultiLocation, + bridge_origin_relative_location: MultiLocation, + + bridge_origin_universal_location: InteriorMultiLocation, + bridge_destination_universal_location: InteriorMultiLocation, + } + + fn run_successful_test(test: SuccessfulTest) -> BridgeLocations { + let locations = bridge_locations( + Box::new(test.here_universal_location), + Box::new(test.bridge_origin_relative_location), + Box::new(test.bridge_destination_universal_location), + REMOTE_NETWORK, + ); + assert_eq!( + locations, + Ok(Box::new(BridgeLocations { + bridge_origin_relative_location: test.bridge_origin_relative_location, + bridge_origin_universal_location: test.bridge_origin_universal_location, + bridge_destination_universal_location: test.bridge_destination_universal_location, + 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: X1(GlobalConsensus(LOCAL_NETWORK)), + bridge_origin_relative_location: Here.into(), + + bridge_origin_universal_location: X1(GlobalConsensus(LOCAL_NETWORK)), + bridge_destination_universal_location: X1(GlobalConsensus(REMOTE_NETWORK)), + }); + } + + #[test] + fn at_relay_from_sibling_parachain_to_remote_relay_works() { + run_successful_test(SuccessfulTest { + here_universal_location: X1(GlobalConsensus(LOCAL_NETWORK)), + bridge_origin_relative_location: X1(Parachain(SIBLING_PARACHAIN)).into(), + + bridge_origin_universal_location: X2( + GlobalConsensus(LOCAL_NETWORK), + Parachain(SIBLING_PARACHAIN), + ), + bridge_destination_universal_location: X1(GlobalConsensus(REMOTE_NETWORK)), + }); + } + + #[test] + fn at_relay_from_local_relay_to_remote_parachain_works() { + run_successful_test(SuccessfulTest { + here_universal_location: X1(GlobalConsensus(LOCAL_NETWORK)), + bridge_origin_relative_location: Here.into(), + + bridge_origin_universal_location: X1(GlobalConsensus(LOCAL_NETWORK)), + bridge_destination_universal_location: X2( + GlobalConsensus(REMOTE_NETWORK), + Parachain(REMOTE_PARACHAIN), + ), + }); + } + + #[test] + fn at_relay_from_sibling_parachain_to_remote_parachain_works() { + run_successful_test(SuccessfulTest { + here_universal_location: X1(GlobalConsensus(LOCAL_NETWORK)), + bridge_origin_relative_location: X1(Parachain(SIBLING_PARACHAIN)).into(), + + bridge_origin_universal_location: X2( + GlobalConsensus(LOCAL_NETWORK), + Parachain(SIBLING_PARACHAIN), + ), + bridge_destination_universal_location: X2( + GlobalConsensus(REMOTE_NETWORK), + Parachain(REMOTE_PARACHAIN), + ), + }); + } + + #[test] + fn at_bridge_hub_from_local_relay_to_remote_relay_works() { + run_successful_test(SuccessfulTest { + here_universal_location: X2( + GlobalConsensus(LOCAL_NETWORK), + Parachain(LOCAL_BRIDGE_HUB), + ), + bridge_origin_relative_location: Parent.into(), + + bridge_origin_universal_location: X1(GlobalConsensus(LOCAL_NETWORK)), + bridge_destination_universal_location: X1(GlobalConsensus(REMOTE_NETWORK)), + }); + } + + #[test] + fn at_bridge_hub_from_sibling_parachain_to_remote_relay_works() { + run_successful_test(SuccessfulTest { + here_universal_location: X2( + GlobalConsensus(LOCAL_NETWORK), + Parachain(LOCAL_BRIDGE_HUB), + ), + bridge_origin_relative_location: ParentThen(X1(Parachain(SIBLING_PARACHAIN))).into(), + + bridge_origin_universal_location: X2( + GlobalConsensus(LOCAL_NETWORK), + Parachain(SIBLING_PARACHAIN), + ), + bridge_destination_universal_location: X1(GlobalConsensus(REMOTE_NETWORK)), + }); + } + + #[test] + fn at_bridge_hub_from_local_relay_to_remote_parachain_works() { + run_successful_test(SuccessfulTest { + here_universal_location: X2( + GlobalConsensus(LOCAL_NETWORK), + Parachain(LOCAL_BRIDGE_HUB), + ), + bridge_origin_relative_location: Parent.into(), + + bridge_origin_universal_location: X1(GlobalConsensus(LOCAL_NETWORK)), + bridge_destination_universal_location: X2( + GlobalConsensus(REMOTE_NETWORK), + Parachain(REMOTE_PARACHAIN), + ), + }); + } + + #[test] + fn at_bridge_hub_from_sibling_parachain_to_remote_parachain_works() { + run_successful_test(SuccessfulTest { + here_universal_location: X2( + GlobalConsensus(LOCAL_NETWORK), + Parachain(LOCAL_BRIDGE_HUB), + ), + bridge_origin_relative_location: ParentThen(X1(Parachain(SIBLING_PARACHAIN))).into(), + + bridge_origin_universal_location: X2( + GlobalConsensus(LOCAL_NETWORK), + Parachain(SIBLING_PARACHAIN), + ), + bridge_destination_universal_location: X2( + GlobalConsensus(REMOTE_NETWORK), + Parachain(REMOTE_PARACHAIN), + ), + }); + } + + // 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: X1(GlobalConsensus(LOCAL_NETWORK)), + bridge_origin_relative_location: Here.into(), + + bridge_origin_universal_location: X1(GlobalConsensus(LOCAL_NETWORK)), + bridge_destination_universal_location: X1(GlobalConsensus(REMOTE_NETWORK)), + }); + let locations2 = run_successful_test(SuccessfulTest { + here_universal_location: X1(GlobalConsensus(LOCAL_NETWORK)), + bridge_origin_relative_location: X1(PalletInstance(0)).into(), + + bridge_origin_universal_location: X1(GlobalConsensus(LOCAL_NETWORK)), + bridge_destination_universal_location: X1(GlobalConsensus(REMOTE_NETWORK)), + }); + + 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: X1(GlobalConsensus(LOCAL_NETWORK)), + bridge_origin_relative_location: Here.into(), + + bridge_origin_universal_location: X1(GlobalConsensus(LOCAL_NETWORK)), + bridge_destination_universal_location: X1(GlobalConsensus(REMOTE_NETWORK)), + }); + let locations2 = run_successful_test(SuccessfulTest { + here_universal_location: X1(GlobalConsensus(LOCAL_NETWORK)), + bridge_origin_relative_location: Here.into(), + + bridge_origin_universal_location: X1(GlobalConsensus(LOCAL_NETWORK)), + bridge_destination_universal_location: X1(GlobalConsensus(REMOTE_NETWORK)), + }); + + 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(X1(Parachain(1000))), + Box::new(Here.into()), + Box::new(X1(GlobalConsensus(REMOTE_NETWORK))), + REMOTE_NETWORK, + ), + Err(BridgeLocationsError::NonUniversalLocation), + ); + } + + #[test] + fn bridge_locations_fails_when_computed_destination_is_not_universal_location() { + assert_eq!( + bridge_locations( + Box::new(X1(GlobalConsensus(LOCAL_NETWORK))), + Box::new(Here.into()), + Box::new(X1(OnlyChild)), + REMOTE_NETWORK, + ), + Err(BridgeLocationsError::NonUniversalLocation), + ); + } + + #[test] + fn bridge_locations_fails_when_computed_destination_is_local() { + assert_eq!( + bridge_locations( + Box::new(X1(GlobalConsensus(LOCAL_NETWORK))), + Box::new(Here.into()), + Box::new(X2(GlobalConsensus(LOCAL_NETWORK), OnlyChild)), + REMOTE_NETWORK, + ), + Err(BridgeLocationsError::DestinationIsLocal), + ); + } + + #[test] + fn bridge_locations_fails_when_computed_destination_is_unreachable() { + assert_eq!( + bridge_locations( + Box::new(X1(GlobalConsensus(LOCAL_NETWORK))), + Box::new(Here.into()), + Box::new(X1(GlobalConsensus(UNREACHABLE_NETWORK))), + REMOTE_NETWORK, + ), + Err(BridgeLocationsError::UnreachableDestination), + ); + } +}