diff --git a/Cargo.lock b/Cargo.lock index 0ccf4a46dddff..a3aa1c4fc9e8a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1055,6 +1055,7 @@ dependencies = [ "bp-asset-hub-westend", "bp-bridge-hub-rococo", "bp-bridge-hub-westend", + "bp-xcm-bridge", "cumulus-pallet-aura-ext", "cumulus-pallet-parachain-system", "cumulus-pallet-session-benchmarking", @@ -1096,7 +1097,7 @@ dependencies = [ "pallet-utility", "pallet-xcm", "pallet-xcm-benchmarks", - "pallet-xcm-bridge-hub-router", + "pallet-xcm-bridge-router", "parachains-common", "parachains-runtimes-test-utils", "parity-scale-codec", @@ -1190,6 +1191,7 @@ dependencies = [ "bp-asset-hub-westend", "bp-bridge-hub-rococo", "bp-bridge-hub-westend", + "bp-xcm-bridge", "cumulus-pallet-aura-ext", "cumulus-pallet-parachain-system", "cumulus-pallet-session-benchmarking", @@ -1258,7 +1260,7 @@ dependencies = [ "pallet-whitelist", "pallet-xcm", "pallet-xcm-benchmarks", - "pallet-xcm-bridge-hub-router", + "pallet-xcm-bridge-router", "parachains-common", "parachains-runtimes-test-utils", "parity-scale-codec", @@ -1316,6 +1318,7 @@ dependencies = [ "pallet-timestamp", "pallet-xcm", "pallet-xcm-bridge-hub-router", + "pallet-xcm-bridge-router", "parachains-common", "parachains-runtimes-test-utils", "parity-scale-codec", @@ -2095,7 +2098,7 @@ dependencies = [ "bp-bridge-hub-cumulus", "bp-messages", "bp-runtime", - "bp-xcm-bridge-hub-router", + "bp-xcm-bridge-router", "frame-support", "parity-scale-codec", "scale-info", @@ -2112,7 +2115,7 @@ dependencies = [ "bp-bridge-hub-cumulus", "bp-messages", "bp-runtime", - "bp-xcm-bridge-hub-router", + "bp-xcm-bridge-router", "frame-support", "parity-scale-codec", "scale-info", @@ -2354,6 +2357,23 @@ dependencies = [ "sp-std 14.0.0", ] +[[package]] +name = "bp-xcm-bridge" +version = "0.2.0" +dependencies = [ + "bp-messages", + "bp-runtime", + "frame-support", + "impl-trait-for-tuples", + "parity-scale-codec", + "scale-info", + "serde", + "sp-core 28.0.0", + "sp-io", + "sp-std 14.0.0", + "staging-xcm", +] + [[package]] name = "bp-xcm-bridge-hub" version = "0.2.0" @@ -2381,6 +2401,17 @@ dependencies = [ "staging-xcm", ] +[[package]] +name = "bp-xcm-bridge-router" +version = "0.1.0" +dependencies = [ + "parity-scale-codec", + "scale-info", + "sp-core 28.0.0", + "sp-runtime", + "staging-xcm", +] + [[package]] name = "bridge-hub-common" version = "0.1.0" @@ -2576,6 +2607,7 @@ dependencies = [ "pallet-timestamp", "pallet-utility", "pallet-xcm", + "pallet-xcm-bridge", "pallet-xcm-bridge-hub", "parachains-common", "parachains-runtimes-test-utils", @@ -4570,6 +4602,7 @@ version = "0.7.1" dependencies = [ "approx", "bounded-collections 0.3.2", + "bp-xcm-bridge", "bp-xcm-bridge-hub-router", "cumulus-pallet-parachain-system", "cumulus-primitives-core", @@ -5731,7 +5764,7 @@ version = "3.0.0" dependencies = [ "asset-test-utils", "bp-messages", - "bp-xcm-bridge-hub", + "bp-xcm-bridge", "cumulus-pallet-parachain-system", "cumulus-pallet-xcmp-queue", "cumulus-primitives-core", @@ -5745,6 +5778,7 @@ dependencies = [ "pallet-message-queue", "pallet-whitelist", "pallet-xcm", + "pallet-xcm-bridge", "pallet-xcm-bridge-hub", "parachains-common", "parity-scale-codec", @@ -13827,6 +13861,35 @@ dependencies = [ "staging-xcm-executor", ] +[[package]] +name = "pallet-xcm-bridge" +version = "0.1.0" +dependencies = [ + "bp-header-chain", + "bp-messages", + "bp-runtime", + "bp-xcm-bridge", + "bp-xcm-bridge-router", + "frame-benchmarking", + "frame-support", + "frame-system", + "log", + "pallet-balances", + "pallet-bridge-messages", + "pallet-xcm-bridge-router", + "parity-scale-codec", + "polkadot-parachain-primitives", + "polkadot-runtime-common", + "scale-info", + "sp-core 28.0.0", + "sp-io", + "sp-runtime", + "sp-std 14.0.0", + "staging-xcm", + "staging-xcm-builder", + "staging-xcm-executor", +] + [[package]] name = "pallet-xcm-bridge-hub" version = "0.2.0" @@ -13874,6 +13937,27 @@ dependencies = [ "tracing", ] +[[package]] +name = "pallet-xcm-bridge-router" +version = "0.1.0" +dependencies = [ + "bp-xcm-bridge", + "bp-xcm-bridge-router", + "frame-benchmarking", + "frame-support", + "frame-system", + "log", + "parity-scale-codec", + "polkadot-runtime-common", + "scale-info", + "sp-core 28.0.0", + "sp-io", + "sp-runtime", + "sp-std 14.0.0", + "staging-xcm", + "staging-xcm-builder", +] + [[package]] name = "parachain-template" version = "0.0.0" @@ -15929,8 +16013,10 @@ dependencies = [ "bp-relayers", "bp-runtime", "bp-test-utils", + "bp-xcm-bridge", "bp-xcm-bridge-hub", "bp-xcm-bridge-hub-router", + "bp-xcm-bridge-router", "bridge-hub-common", "bridge-hub-test-utils", "bridge-runtime-common", @@ -16109,8 +16195,10 @@ dependencies = [ "pallet-whitelist", "pallet-xcm", "pallet-xcm-benchmarks", + "pallet-xcm-bridge", "pallet-xcm-bridge-hub", "pallet-xcm-bridge-hub-router", + "pallet-xcm-bridge-router", "parachains-common", "parachains-runtimes-test-utils", "polkadot-approval-distribution", diff --git a/Cargo.toml b/Cargo.toml index c05aabc5bb4a8..0708428c17f57 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,8 +17,10 @@ members = [ "bridges/modules/messages", "bridges/modules/parachains", "bridges/modules/relayers", + "bridges/modules/xcm-bridge", "bridges/modules/xcm-bridge-hub", "bridges/modules/xcm-bridge-hub-router", + "bridges/modules/xcm-bridge-router", "bridges/primitives/beefy", "bridges/primitives/header-chain", "bridges/primitives/messages", @@ -27,8 +29,10 @@ members = [ "bridges/primitives/relayers", "bridges/primitives/runtime", "bridges/primitives/test-utils", + "bridges/primitives/xcm-bridge", "bridges/primitives/xcm-bridge-hub", "bridges/primitives/xcm-bridge-hub-router", + "bridges/primitives/xcm-bridge-router", "bridges/relays/client-substrate", "bridges/relays/equivocation", "bridges/relays/finality", @@ -693,8 +697,10 @@ bp-rococo = { path = "polkadot/runtime/rococo/bridge-primitives", default-featur bp-runtime = { path = "bridges/primitives/runtime", default-features = false } bp-test-utils = { path = "bridges/primitives/test-utils", default-features = false } bp-westend = { path = "polkadot/runtime/westend/bridge-primitives", default-features = false } +bp-xcm-bridge = { path = "bridges/primitives/xcm-bridge", default-features = false } bp-xcm-bridge-hub = { path = "bridges/primitives/xcm-bridge-hub", default-features = false } bp-xcm-bridge-hub-router = { path = "bridges/primitives/xcm-bridge-hub-router", default-features = false } +bp-xcm-bridge-router = { path = "bridges/primitives/xcm-bridge-router", default-features = false } bridge-hub-common = { path = "cumulus/parachains/runtimes/bridge-hubs/common", default-features = false } bridge-hub-rococo-emulated-chain = { path = "cumulus/parachains/integration-tests/emulated/chains/parachains/bridges/bridge-hub-rococo" } bridge-hub-rococo-runtime = { path = "cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo", default-features = false } @@ -1077,8 +1083,10 @@ pallet-vesting = { path = "substrate/frame/vesting", default-features = false } pallet-whitelist = { path = "substrate/frame/whitelist", default-features = false } pallet-xcm = { path = "polkadot/xcm/pallet-xcm", default-features = false } pallet-xcm-benchmarks = { path = "polkadot/xcm/pallet-xcm-benchmarks", default-features = false } +pallet-xcm-bridge = { path = "bridges/modules/xcm-bridge", default-features = false } pallet-xcm-bridge-hub = { path = "bridges/modules/xcm-bridge-hub", default-features = false } pallet-xcm-bridge-hub-router = { path = "bridges/modules/xcm-bridge-hub-router", default-features = false } +pallet-xcm-bridge-router = { path = "bridges/modules/xcm-bridge-router", default-features = false } parachain-info = { path = "cumulus/parachains/pallets/parachain-info", default-features = false, package = "staging-parachain-info" } parachain-template-runtime = { path = "templates/parachain/runtime" } parachains-common = { path = "cumulus/parachains/common", default-features = false } diff --git a/bridges/modules/messages/src/lanes_manager.rs b/bridges/modules/messages/src/lanes_manager.rs index c785ed4e01efa..9e7c404a0a741 100644 --- a/bridges/modules/messages/src/lanes_manager.rs +++ b/bridges/modules/messages/src/lanes_manager.rs @@ -156,7 +156,7 @@ impl, I: 'static> RuntimeInboundLaneStorage { // apart from the explicit closure, the lane may be unable to receive any messages. // Right now we do an additional check here, but it may be done later (e.g. by // explicitly closing the lane and reopening it from - // `pallet-xcm-bridge-hub::on-initialize`) + // `pallet-xcm-bridge::on-initialize`) // // The fact that we only check it here, means that the `MessageDispatch` may switch // to inactive state during some message dispatch in the middle of message delivery diff --git a/bridges/modules/xcm-bridge-router/Cargo.toml b/bridges/modules/xcm-bridge-router/Cargo.toml new file mode 100644 index 0000000000000..0afb79c21c22d --- /dev/null +++ b/bridges/modules/xcm-bridge-router/Cargo.toml @@ -0,0 +1,71 @@ +[package] +name = "pallet-xcm-bridge-router" +description = "Bridge hub interface for sibling/parent chains with dynamic fees support." +version = "0.1.0" +authors.workspace = true +edition.workspace = true +license = "GPL-3.0-or-later WITH Classpath-exception-2.0" +repository.workspace = true + +[lints] +workspace = true + +[dependencies] +codec = { workspace = true } +log = { workspace = true } +scale-info = { features = ["bit-vec", "derive", "serde"], workspace = true } + +# Bridge dependencies +bp-xcm-bridge = { workspace = true } +bp-xcm-bridge-router = { workspace = true } + +# Substrate Dependencies +frame-benchmarking = { optional = true, workspace = true } +frame-support = { workspace = true } +frame-system = { workspace = true } +sp-core = { workspace = true } +sp-runtime = { workspace = true } +sp-std = { workspace = true } + +# Polkadot Dependencies +polkadot-runtime-common = { workspace = true } +xcm = { workspace = true } +xcm-builder = { workspace = true } + +[dev-dependencies] +sp-io = { workspace = true, default-features = true } +sp-std = { workspace = true, default-features = true } + +[features] +default = ["std"] +std = [ + "bp-xcm-bridge-router/std", + "bp-xcm-bridge/std", + "codec/std", + "frame-benchmarking/std", + "frame-support/std", + "frame-system/std", + "log/std", + "polkadot-runtime-common/std", + "scale-info/std", + "sp-core/std", + "sp-runtime/std", + "sp-std/std", + "xcm-builder/std", + "xcm/std", +] +runtime-benchmarks = [ + "frame-benchmarking/runtime-benchmarks", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "polkadot-runtime-common/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", + "xcm-builder/runtime-benchmarks", + "xcm/runtime-benchmarks", +] +try-runtime = [ + "frame-support/try-runtime", + "frame-system/try-runtime", + "polkadot-runtime-common/try-runtime", + "sp-runtime/try-runtime", +] diff --git a/bridges/modules/xcm-bridge-router/src/benchmarking.rs b/bridges/modules/xcm-bridge-router/src/benchmarking.rs new file mode 100644 index 0000000000000..c6817ccf44d04 --- /dev/null +++ b/bridges/modules/xcm-bridge-router/src/benchmarking.rs @@ -0,0 +1,64 @@ +// Copyright (C) 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 . + +//! XCM bridge hub router pallet benchmarks. + +#![cfg(feature = "runtime-benchmarks")] + +use crate::{BridgeState, Bridges, Call, ResolveBridgeId, MINIMAL_DELIVERY_FEE_FACTOR}; +use frame_benchmarking::v2::*; +use frame_support::traits::EnsureOriginWithArg; +use xcm::prelude::*; + +/// Pallet we're benchmarking here. +pub struct Pallet, I: 'static = ()>(crate::Pallet); + +/// Trait that must be implemented by runtime to be able to benchmark pallet properly. +pub trait Config: crate::Config { + /// Returns destination which is valid for this router instance. + fn ensure_bridged_target_destination() -> Result; + /// Returns valid origin for `update_bridge_status` (if `T::UpdateBridgeStatusOrigin` is + /// supported). + fn update_bridge_status_origin() -> Option; +} + +#[instance_benchmarks] +mod benchmarks { + use super::*; + + #[benchmark] + fn update_bridge_status() -> Result<(), BenchmarkError> { + let bridge_id = + T::BridgeIdResolver::resolve_for_dest(&T::ensure_bridged_target_destination()?) + .ok_or(BenchmarkError::Override(BenchmarkResult::from_weight(Weight::MAX)))?; + let origin = T::update_bridge_status_origin() + .ok_or(BenchmarkError::Override(BenchmarkResult::from_weight(Weight::MAX)))?; + let _ = T::UpdateBridgeStatusOrigin::try_origin(origin.clone(), &bridge_id) + .map_err(|_| BenchmarkError::Override(BenchmarkResult::from_weight(Weight::MAX)))?; + let is_congested = true; + + #[extrinsic_call] + update_bridge_status(origin as T::RuntimeOrigin, bridge_id.clone(), is_congested); + + assert_eq!( + Bridges::::get(&bridge_id), + BridgeState { delivery_fee_factor: MINIMAL_DELIVERY_FEE_FACTOR, is_congested } + ); + Ok(()) + } + + impl_benchmark_test_suite!(Pallet, crate::mock::new_test_ext(), crate::mock::TestRuntime); +} diff --git a/bridges/modules/xcm-bridge-router/src/impls.rs b/bridges/modules/xcm-bridge-router/src/impls.rs new file mode 100644 index 0000000000000..80b6d3b60225c --- /dev/null +++ b/bridges/modules/xcm-bridge-router/src/impls.rs @@ -0,0 +1,395 @@ +// Copyright (C) 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 . + +//! Various implementations supporting easier configuration of the pallet. + +use crate::{BridgeIdOf, Bridges, Config, Pallet, LOG_TARGET}; +use bp_xcm_bridge_router::ResolveBridgeId; +use codec::Encode; +use frame_support::{ensure, pallet_prelude::PhantomData, traits::Get}; +use xcm::prelude::*; +use xcm_builder::{ensure_is_remote, ExporterFor}; + +/// Implementation of [`bp_xcm_bridge::LocalXcmChannelManager`] which tracks and updates +/// `is_congested` for a given `BridgeId`. This implementation is useful for managing congestion and +/// dynamic fees with the local `ExportXcm` implementation. +impl, I: 'static> bp_xcm_bridge::LocalXcmChannelManager> + for Pallet +{ + type Error = (); + + /// Suspends the given bridge. + /// + /// This function ensures that the `local_origin` matches the expected `Location::here()`. If + /// the check passes, it updates the bridge status to congested. + fn suspend_bridge( + local_origin: &Location, + bridge: BridgeIdOf, + ) -> Result<(), Self::Error> { + log::trace!( + target: LOG_TARGET, + "LocalXcmChannelManager::suspend_bridge(local_origin: {local_origin:?}, bridge: {bridge:?})", + ); + ensure!(local_origin.eq(&Location::here()), ()); + + // update status + Self::do_update_bridge_status(bridge, true); + + Ok(()) + } + + /// Resumes the given bridge. + /// + /// This function ensures that the `local_origin` matches the expected `Location::here()`. If + /// the check passes, it updates the bridge status to not congested. + fn resume_bridge(local_origin: &Location, bridge: BridgeIdOf) -> Result<(), Self::Error> { + log::trace!( + target: LOG_TARGET, + "LocalXcmChannelManager::resume_bridge(local_origin: {local_origin:?}, bridge: {bridge:?})", + ); + ensure!(local_origin.eq(&Location::here()), ()); + + // update status + Self::do_update_bridge_status(bridge, false); + + Ok(()) + } +} + +/// Calculates the price for message delivery across bridges with base price, dynamic fees +/// and size-based pricing. +pub struct GetPriceForBridge(PhantomData<(T, I, BasePrice)>); +impl, I: 'static, BasePrice: Get> + polkadot_runtime_common::xcm_sender::PriceForMessageDelivery + for GetPriceForBridge +{ + type Id = BridgeIdOf; + + fn price_for_delivery(bridge_id: Self::Id, message: &Xcm<()>) -> Assets { + // Get a base price. + let mut price = BasePrice::get(); + + // Apply message-size-based fees (if configured). + if let Some(message_size_fees) = + Pallet::::calculate_message_size_fee(message.encoded_size() as _) + { + price.push(message_size_fees); + } + + // Apply dynamic congestion fees based on bridge state (if needed). + let bridge_state = Bridges::::get(bridge_id); + let mut dynamic_fees = price.into_inner(); + for fee in dynamic_fees.iter_mut() { + Pallet::::apply_dynamic_fee_factor(&bridge_state, fee); + } + Assets::from(dynamic_fees) + } +} + +/// Adapter implementation for [`ExporterFor`] that allows exporting message size fee and/or dynamic +/// fees based on the `BridgeId` resolved by the `T::BridgeIdResolver` resolver, if and only if the +/// `E` exporter supports bridging. This adapter acts as an [`ExporterFor`], for example, for the +/// [`xcm_builder::SovereignPaidRemoteExporter`], enabling it to compute message and/or dynamic fees +/// using a fee factor. +pub struct ViaRemoteBridgeExporter(PhantomData<(T, I, E, BNF, BHLF)>); +impl, I: 'static, E, BridgedNetworkIdFilter, BridgeHubLocationFilter> ExporterFor + for ViaRemoteBridgeExporter +where + E: ExporterFor, + BridgedNetworkIdFilter: Get>, + BridgeHubLocationFilter: Get>, +{ + fn exporter_for( + network: &NetworkId, + remote_location: &InteriorLocation, + message: &Xcm<()>, + ) -> Option<(Location, Option)> { + log::trace!( + target: LOG_TARGET, + "exporter_for - network: {network:?}, remote_location: {remote_location:?}, msg: {message:?}", + ); + // ensure that the message is sent to the expected bridged network (if specified). + if let Some(bridged_network) = BridgedNetworkIdFilter::get() { + if *network != bridged_network { + log::trace!( + target: LOG_TARGET, + "Router with bridged_network_id filter({bridged_network:?}) does not support bridging to network {network:?}!", + ); + return None + } + } + + // ensure that the message is sent to the expected bridged network and location. + let (bridge_hub_location, maybe_payment) = match E::exporter_for( + network, + remote_location, + message, + ) { + Some((bridge_hub_location, maybe_payment)) => match BridgeHubLocationFilter::get() { + Some(expected_bridge_hub_location) + if expected_bridge_hub_location.eq(&bridge_hub_location) => + (bridge_hub_location, maybe_payment), + None => (bridge_hub_location, maybe_payment), + _ => { + log::trace!( + target: LOG_TARGET, + "Resolved bridge_hub_location: {:?} does not match expected one: {:?} for bridging to network {:?} and remote_location {:?}!", + bridge_hub_location, + BridgeHubLocationFilter::get(), + network, + remote_location, + ); + return None + }, + }, + _ => { + log::trace!( + target: LOG_TARGET, + "Inner `E` router does not support bridging to network {:?} and remote_location {:?}!", + network, + remote_location, + ); + return None + }, + }; + + // calculate message size fees (if configured) + let maybe_message_size_fees = + Pallet::::calculate_message_size_fee(message.encoded_size() as _); + + // compute actual fees - sum(actual payment, message size fees) if possible + let mut fees = match (maybe_payment, maybe_message_size_fees) { + (Some(payment), None) => Some(payment), + (None, Some(message_size_fees)) => Some(message_size_fees), + (None, None) => None, + ( + Some(Asset { id: payment_asset_id, fun: Fungible(payment_amount) }), + Some(Asset { + id: message_size_fees_asset_id, + fun: Fungible(message_size_fees_amount), + }), + ) if payment_asset_id.eq(&message_size_fees_asset_id) => { + // we can subsume two assets with the same asset_id and fungibility. + Some( + (payment_asset_id, payment_amount.saturating_add(message_size_fees_amount)) + .into(), + ) + }, + (Some(payment), Some(message_size_fees)) => { + log::error!( + target: LOG_TARGET, + "Router is configured for `T::FeeAsset` {:?} \ + but we have two different assets which cannot be calculated as one result asset: payment: {:?} and message_size_fees: {:?} for bridge_hub_location: {:?} for bridging to {:?}/{:?}!", + T::FeeAsset::get(), + payment, + message_size_fees, + bridge_hub_location, + network, + remote_location, + ); + return None + }, + }; + + // `fees` is populated with base bridge fees, now let's apply congestion/dynamic fees if + // required. + if let Some(bridge_id) = T::BridgeIdResolver::resolve_for(network, remote_location) { + let bridge_state = Bridges::::get(bridge_id); + if let Some(f) = fees.as_mut() { + Pallet::::apply_dynamic_fee_factor(&bridge_state, f); + } + } + + Some((bridge_hub_location, fees)) + } +} + +/// Adapter implementation for [`SendXcm`] that allows adding a message size fee and/or dynamic fees +/// based on the `BridgeId` resolved by the `T::BridgeIdResolver` resolver, if and only if `E` +/// supports routing. This adapter can be used, for example, as a wrapper over +/// [`xcm_builder::LocalExporter`], enabling it to compute message and/or dynamic fees using a +/// fee factor. +pub struct ViaLocalBridgeExporter(PhantomData<(T, I, E)>); +impl, I: 'static, E: SendXcm> SendXcm for ViaLocalBridgeExporter { + type Ticket = E::Ticket; + + fn validate( + destination: &mut Option, + message: &mut Option>, + ) -> SendResult { + let dest_clone = destination.clone().ok_or(SendError::MissingArgument)?; + let message_size = message.as_ref().map_or(0, |message| message.encoded_size()) as _; + + match E::validate(destination, message) { + Ok((ticket, mut fees)) => { + // calculate message size fees (if configured) + let maybe_message_size_fees = + Pallet::::calculate_message_size_fee(message_size); + if let Some(message_size_fees) = maybe_message_size_fees { + fees.push(message_size_fees); + } + + // Here, we have the actual result fees covering bridge fees, so now we need to + // check/apply the congestion and dynamic_fees features (if possible). + if let Some(bridge_id) = T::BridgeIdResolver::resolve_for_dest(&dest_clone) { + let bridge_state = Bridges::::get(bridge_id); + let mut dynamic_fees = fees.into_inner(); + for fee in dynamic_fees.iter_mut() { + Pallet::::apply_dynamic_fee_factor(&bridge_state, fee); + } + fees = Assets::from(dynamic_fees); + } + + // return original ticket with possibly extended fees + Ok((ticket, fees)) + }, + error => error, + } + } + + fn deliver(ticket: Self::Ticket) -> Result { + E::deliver(ticket) + } +} + +/// Implementation of [`ResolveBridgeId`] returning [`bp_xcm_bridge::BridgeId`] based on the +/// configured `UniversalLocation` and remote universal location. +pub struct EnsureIsRemoteBridgeIdResolver(PhantomData); +impl> ResolveBridgeId + for EnsureIsRemoteBridgeIdResolver +{ + type BridgeId = bp_xcm_bridge::BridgeId; + + fn resolve_for_dest(dest: &Location) -> Option { + let Ok((remote_network, remote_dest)) = + ensure_is_remote(UniversalLocation::get(), dest.clone()) + else { + log::trace!( + target: LOG_TARGET, + "EnsureIsRemoteBridgeIdResolver - does not recognize a remote destination for: {dest:?}!" + ); + return None + }; + Self::resolve_for(&remote_network, &remote_dest) + } + + fn resolve_for( + bridged_network: &NetworkId, + bridged_dest: &InteriorLocation, + ) -> Option { + let bridged_universal_location = if let Ok(network) = bridged_dest.global_consensus() { + if network.ne(bridged_network) { + log::error!( + target: LOG_TARGET, + "EnsureIsRemoteBridgeIdResolver - bridged_dest: {bridged_dest:?} contains invalid network: {network:?}, expected bridged_network: {bridged_network:?}!" + ); + return None + } else { + bridged_dest.clone() + } + } else { + // if `bridged_dest` does not contain `GlobalConsensus`, let's prepend one + match bridged_dest.clone().pushed_front_with(*bridged_network) { + Ok(bridged_universal_location) => bridged_universal_location, + Err((original, prepend_with)) => { + log::error!( + target: LOG_TARGET, + "EnsureIsRemoteBridgeIdResolver - bridged_dest: {original:?} cannot be prepended with: {prepend_with:?}!" + ); + return None + }, + } + }; + + match ( + UniversalLocation::get().global_consensus(), + bridged_universal_location.global_consensus(), + ) { + (Ok(local), Ok(remote)) if local != remote => (), + (local, remote) => { + log::error!( + target: LOG_TARGET, + "EnsureIsRemoteBridgeIdResolver - local: {local:?} and remote: {remote:?} must be different!" + ); + return None + }, + } + + // calculate `BridgeId` from universal locations + Some(Self::BridgeId::new(&UniversalLocation::get(), &bridged_universal_location)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn ensure_is_remote_bridge_id_resolver_works() { + frame_support::parameter_types! { + pub ThisNetwork: NetworkId = NetworkId::ByGenesis([0; 32]); + pub BridgedNetwork: NetworkId = NetworkId::ByGenesis([1; 32]); + pub UniversalLocation: InteriorLocation = [GlobalConsensus(ThisNetwork::get()), Parachain(1000)].into(); + } + assert_ne!(ThisNetwork::get(), BridgedNetwork::get()); + + type Resolver = EnsureIsRemoteBridgeIdResolver; + + // not remote dest + assert!(Resolver::resolve_for_dest(&Location::new(1, Here)).is_none()); + // not a valid remote dest + assert!(Resolver::resolve_for_dest(&Location::new(2, Here)).is_none()); + // the same network for remote dest + assert!(Resolver::resolve_for_dest(&Location::new(2, GlobalConsensus(ThisNetwork::get()))) + .is_none()); + assert!(Resolver::resolve_for(&ThisNetwork::get(), &Here.into()).is_none()); + + // ok + assert!(Resolver::resolve_for_dest(&Location::new( + 2, + GlobalConsensus(BridgedNetwork::get()) + )) + .is_some()); + assert!(Resolver::resolve_for_dest(&Location::new( + 2, + [GlobalConsensus(BridgedNetwork::get()), Parachain(2013)] + )) + .is_some()); + + // ok - resolves the same + assert_eq!( + Resolver::resolve_for_dest(&Location::new(2, GlobalConsensus(BridgedNetwork::get()))), + Resolver::resolve_for(&BridgedNetwork::get(), &Here.into()), + ); + assert_eq!( + Resolver::resolve_for_dest(&Location::new( + 2, + [GlobalConsensus(BridgedNetwork::get()), Parachain(2013)] + )), + Resolver::resolve_for(&BridgedNetwork::get(), &Parachain(2013).into()), + ); + assert_eq!( + Resolver::resolve_for_dest(&Location::new( + 2, + [GlobalConsensus(BridgedNetwork::get()), Parachain(2013)] + )), + Resolver::resolve_for( + &BridgedNetwork::get(), + &[GlobalConsensus(BridgedNetwork::get()), Parachain(2013)].into() + ), + ); + } +} diff --git a/bridges/modules/xcm-bridge-router/src/lib.rs b/bridges/modules/xcm-bridge-router/src/lib.rs new file mode 100644 index 0000000000000..10d2967fe0af0 --- /dev/null +++ b/bridges/modules/xcm-bridge-router/src/lib.rs @@ -0,0 +1,400 @@ +// Copyright (C) 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 . + +//! A pallet that can be used as an alternative in the XCM router configuration — see the `SendXcm` +//! implementation for details. +//! +//! ## Features +//! +//! This pallet offers several optional features to customize functionality: +//! +//! ### Message Size Fee +//! An optional fee based on `T::FeeAsset` and `T::ByteFee`. If `T::FeeAsset` is not specified, this +//! fee is not calculated. +//! +//! ### Dynamic Fees and Congestion +//! +//! This pallet supports storing the congestion status of bridge outbound queues. The fee increases +//! exponentially if the queue between this chain and a sibling or child bridge hub becomes +//! congested. All other bridge hub queues provide backpressure mechanisms, so if any of these +//! queues are congested, it will eventually lead to increased queuing on this chain. +//! +//! There are two methods for storing congestion status: +//! 1. A dedicated extrinsic `update_bridge_status`, which relies on `T::UpdateBridgeStatusOrigin`. +//! This allows the message exporter to send, for example, an XCM `Transact`. +//! 2. An implementation of `bp_xcm_bridge::LocalXcmChannelManager`. +//! +//! ## Usage +//! +//! This pallet provides several implementations, such as `ViaLocalBridgeExporter` and +//! `ViaRemoteBridgeExporter`, which can expose or access these features. +//! +//! This router can be used in two main scenarios, depending on where the router and message +//! exporter (e.g., `pallet_xcm_bridge_hub` or another pallet with an `ExportXcm` implementation) +//! are deployed: +//! +//! ### On the Same Chain as the Message Exporter +//! In this setup, the router directly calls an `ExportXcm` implementation. In this case, +//! `ViaLocalBridgeExporter` can be used as a wrapper with `T::MessageExporter`. +//! +//! ### On a Different Chain than the Message Exporter +//! In this setup, we need to provide a `SendXcm` implementation for `T::MessageExporter`, which +//! sends `ExportMessage`. For example, `SovereignPaidRemoteExporter` can be used with +//! `ViaRemoteBridgeExporter`. +//! +//! **Note on Terminology**: When we refer to the bridge hub, we mean the chain that has the +//! `pallet-bridge-messages` with an `ExportXcm` implementation deployed, such as +//! `pallet-xcm-bridge`. Depending on the deployment setup, `T::MessageExporter` can be +//! configured accordingly — see `T::MessageExporter` for additional documentation. + +#![cfg_attr(not(feature = "std"), no_std)] + +pub use bp_xcm_bridge_router::{BridgeState, ResolveBridgeId, MINIMAL_DELIVERY_FEE_FACTOR}; +use codec::Encode; +use frame_support::traits::{EnsureOriginWithArg, Get}; +use sp_runtime::{FixedPointNumber, FixedU128, Saturating}; +use sp_std::vec::Vec; +use xcm::prelude::*; +use xcm_builder::InspectMessageQueues; + +pub use pallet::*; +pub use weights::WeightInfo; + +pub mod benchmarking; +pub mod impls; +pub mod weights; + +mod mock; + +#[cfg(test)] +mod tests; + +/// The factor that is used to increase current message fee factor when bridge experiencing +/// some lags. +const EXPONENTIAL_FEE_BASE: FixedU128 = FixedU128::from_rational(105, 100); // 1.05 +/// The factor that is used to increase current message fee factor for every sent kilobyte. +const MESSAGE_SIZE_FEE_BASE: FixedU128 = FixedU128::from_rational(1, 1000); // 0.001 + +/// Maximal size of the XCM message that may be sent over bridge. +/// +/// This should be less than the maximal size, allowed by the messages pallet, because +/// the message itself is wrapped in other structs and is double encoded. +pub const HARD_MESSAGE_SIZE_LIMIT: u32 = 32 * 1024; + +/// The target that will be used when publishing logs related to this pallet. +pub const LOG_TARGET: &str = "xcm::bridge-router"; + +#[frame_support::pallet] +pub mod pallet { + use super::*; + use frame_support::pallet_prelude::*; + use frame_system::pallet_prelude::*; + + /// Default implementations of [`DefaultConfig`], which can be used to implement [`Config`]. + pub mod config_preludes { + use super::*; + use frame_support::{derive_impl, traits::ConstU128}; + + /// A type providing default configurations for this pallet in testing environment. + pub struct TestDefaultConfig; + + #[derive_impl(frame_system::config_preludes::TestDefaultConfig, no_aggregated_types)] + impl frame_system::DefaultConfig for TestDefaultConfig {} + + #[frame_support::register_default_impl(TestDefaultConfig)] + impl DefaultConfig for TestDefaultConfig { + type WeightInfo = (); + type DestinationVersion = AlwaysLatest; + + // We don't need (optional) message_size fees. + type ByteFee = ConstU128<0>; + // We don't need (optional) message_size fees. + type FeeAsset = (); + } + } + + #[pallet::config(with_default)] + pub trait Config: frame_system::Config { + /// Benchmarks results from runtime we're plugged into. + type WeightInfo: WeightInfo; + + /// Checks the XCM version for the destination. + type DestinationVersion: GetVersion; + + /// The bridge hub may be: + /// - A system (sibling) bridge hub parachain (or another chain), in which case we need an + /// implementation for `T::MessageExporter` that sends `ExportMessage`, e.g., + /// `SovereignPaidRemoteExporter`. + /// - The local chain, in which case we need an implementation for `T::MessageExporter` that + /// does not use `ExportMessage` but instead directly calls the `ExportXcm` + /// implementation. + #[pallet::no_default] + type MessageExporter: SendXcm; + + /// Resolves a specific `BridgeId` for `dest`, used for identifying the bridge in cases of + /// congestion and dynamic fees. If it resolves to `None`, it means no congestion or + /// dynamic fees are handled for `dest`. + #[pallet::no_default] + type BridgeIdResolver: ResolveBridgeId; + + /// Origin that is allowed to update bridge status, + /// e.g. the sibling bridge hub or governance as root. + #[pallet::no_default] + type UpdateBridgeStatusOrigin: EnsureOriginWithArg>; + + /// Additional fee that is paid for every byte of the outbound message. + /// See `calculate_message_size_fee` for more details. + type ByteFee: Get; + /// Asset used to pay the `ByteFee`. + /// If not specified, the `ByteFee` is ignored. + /// See `calculate_fees` for more details. + type FeeAsset: Get>; + } + + /// An alias for the `BridgeId` of configured `T::BridgeIdResolver`. + pub type BridgeIdOf = <>::BridgeIdResolver as ResolveBridgeId>::BridgeId; + + #[pallet::pallet] + pub struct Pallet(PhantomData<(T, I)>); + + #[pallet::call] + impl, I: 'static> Pallet { + /// Notification about congested bridge queue. + #[pallet::call_index(0)] + #[pallet::weight(T::WeightInfo::update_bridge_status())] + pub fn update_bridge_status( + origin: OriginFor, + bridge_id: BridgeIdOf, + is_congested: bool, + ) -> DispatchResult { + let _ = T::UpdateBridgeStatusOrigin::ensure_origin(origin, &bridge_id)?; + + log::info!( + target: LOG_TARGET, + "Received bridge status from {:?}: congested = {}", + bridge_id, + is_congested, + ); + + // update status + Self::do_update_bridge_status(bridge_id, is_congested); + + Ok(()) + } + } + + /// Stores `BridgeState` for congestion control and dynamic fees for each resolved bridge ID + /// associated with a destination. + #[pallet::storage] + pub type Bridges, I: 'static = ()> = + StorageMap<_, Blake2_128Concat, BridgeIdOf, BridgeState, ValueQuery>; + + impl, I: 'static> Pallet { + /// Called when new message is sent to the `dest` (queued to local outbound XCM queue). + pub(crate) fn on_message_sent_to(message_size: u32, dest: Location) { + let Some(bridge_id) = T::BridgeIdResolver::resolve_for_dest(&dest) else { + // not supported bridge id, so do nothing + return + }; + + // handle congestion and fee factor (if detected) + Bridges::::mutate(&bridge_id, |bridge_state| { + if !bridge_state.is_congested { + return; + } + + // ok - we need to increase the fee factor, let's do that + let message_size_factor = FixedU128::from_u32(message_size.saturating_div(1024)) + .saturating_mul(MESSAGE_SIZE_FEE_BASE); + let total_factor = EXPONENTIAL_FEE_BASE.saturating_add(message_size_factor); + + let previous_factor = bridge_state.delivery_fee_factor; + bridge_state.delivery_fee_factor = + bridge_state.delivery_fee_factor.saturating_mul(total_factor); + + log::info!( + target: LOG_TARGET, + "Bridge channel with id {:?} is congested. Increased fee factor from {} to {} for {:?}", + bridge_id, + previous_factor, + bridge_state.delivery_fee_factor, + dest + ); + Self::deposit_event(Event::DeliveryFeeFactorUpdated { + previous_value: previous_factor, + new_value: bridge_state.delivery_fee_factor, + bridge_id: bridge_id.clone(), + }); + }); + } + + /// Returns the recalculated dynamic fee for a given asset based on the bridge state. + /// + /// This function adjusts the amount of a fungible asset according to the delivery fee + /// factor specified in the `bridge_state`. If the asset is fungible, the + /// `delivery_fee_factor` is applied to the asset’s amount, potentially altering its + /// value. + pub(crate) fn apply_dynamic_fee_factor(bridge_state: &BridgeState, asset: &mut Asset) { + if let Fungible(amount) = &mut asset.fun { + *amount = bridge_state.delivery_fee_factor.saturating_mul_int(*amount); + } + } + + /// Calculates an (optional) fee for message size based on `T::ByteFee` and `T::FeeAsset`. + pub(crate) fn calculate_message_size_fee(message_size: u32) -> Option { + // Apply message size `T::ByteFee/T::FeeAsset` feature (if configured). + if let Some(asset_id) = T::FeeAsset::get() { + let message_fee = (message_size as u128).saturating_mul(T::ByteFee::get()); + if message_fee > 0 { + return Some((asset_id, message_fee).into()); + } + } + None + } + + /// Updates the congestion status of a bridge for a given `bridge_id`. + pub(crate) fn do_update_bridge_status(bridge_id: BridgeIdOf, is_congested: bool) { + Bridges::::mutate(&bridge_id, |bridge| { + let prev_fee_factor = bridge.delivery_fee_factor; + + bridge.is_congested = is_congested; + if !is_congested { + *bridge = Default::default(); + } + + if prev_fee_factor != bridge.delivery_fee_factor { + Self::deposit_event(Event::DeliveryFeeFactorUpdated { + previous_value: prev_fee_factor, + new_value: bridge.delivery_fee_factor, + bridge_id: bridge_id.clone(), + }); + } + }); + } + } + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event, I: 'static = ()> { + /// Delivery fee factor has been updated. + DeliveryFeeFactorUpdated { + /// Previous value of the `DeliveryFeeFactor`. + previous_value: FixedU128, + /// New value of the `DeliveryFeeFactor`. + new_value: FixedU128, + /// Bridge identifier. + bridge_id: BridgeIdOf, + }, + } +} + +// This pallet acts as the `SendXcm` to the sibling/child bridge hub instead of regular +// XCMP/DMP transport. This allows injecting dynamic message fees into XCM programs that +// are going to the bridged network. +impl, I: 'static> SendXcm for Pallet { + type Ticket = (u32, Location, ::Ticket); + + fn validate( + dest: &mut Option, + xcm: &mut Option>, + ) -> SendResult { + log::trace!(target: LOG_TARGET, "validate - msg: {xcm:?}, destination: {dest:?}"); + + // In case of success, the `T::MessageExporter` can modify XCM instructions and consume + // `dest` / `xcm`, so we retain the clone of original message and the destination for later + // `DestinationVersion` validation. + let xcm_to_dest_clone = xcm.clone(); + let dest_clone = dest.clone(); + + // First, use the inner exporter to validate the destination to determine if it is even + // routable. If it is not, return an error. If it is, then the XCM is extended with + // instructions to pay the message fee at the sibling/child bridge hub. The cost will + // include both the cost of (1) delivery to the sibling bridge hub (returned by + // `Config::MessageExporter`) and (2) delivery to the bridged bridge hub (returned by + // `Self::exporter_for`). + match T::MessageExporter::validate(dest, xcm) { + Ok((ticket, cost)) => { + // If the ticket is ok, it means we are routing with this router, so we need to + // apply more validations to the cloned `dest` and `xcm`, which are required here. + let xcm_to_dest_clone = xcm_to_dest_clone.ok_or(SendError::MissingArgument)?; + let dest_clone = dest_clone.ok_or(SendError::MissingArgument)?; + + // We won't have access to `dest` and `xcm` in the `deliver` method, so we need to + // precompute everything required here. However, `dest` and `xcm` were consumed by + // `T::MessageExporter`, so we need to use their clones. + let message_size = xcm_to_dest_clone.encoded_size() as _; + + // The bridge doesn't support oversized or overweight messages. Therefore, it's + // better to drop such messages here rather than at the bridge hub. Let's check the + // message size. + if message_size > HARD_MESSAGE_SIZE_LIMIT { + return Err(SendError::ExceedsMaxMessageSize) + } + + // We need to ensure that the known `dest`'s XCM version can comprehend the current + // `xcm` program. This may seem like an additional, unnecessary check, but it is + // not. A similar check is probably performed by the `T::MessageExporter`, which + // attempts to send a versioned message to the sibling bridge hub. However, the + // local bridge hub may have a higher XCM version than the remote `dest`. Once + // again, it is better to discard such messages here than at the bridge hub (e.g., + // to avoid losing funds). + let destination_version = T::DestinationVersion::get_version_for(&dest_clone) + .ok_or(SendError::DestinationUnsupported)?; + let _ = VersionedXcm::from(xcm_to_dest_clone) + .into_version(destination_version) + .map_err(|()| SendError::DestinationUnsupported)?; + + log::info!( + target: LOG_TARGET, + "Going to send message to {dest_clone:?} ({message_size:?} bytes) with actual cost: {cost:?}" + ); + + Ok(((message_size, dest_clone, ticket), cost)) + }, + Err(e) => { + log::trace!(target: LOG_TARGET, "`T::MessageExporter` validates for dest: {dest_clone:?} with error: {e:?}"); + Err(e) + }, + } + } + + fn deliver(ticket: Self::Ticket) -> Result { + // use router to enqueue message to the sibling/child bridge hub. This also should handle + // payment for passing through this queue. + let (message_size, dest, ticket) = ticket; + let xcm_hash = T::MessageExporter::deliver(ticket)?; + + log::trace!( + target: LOG_TARGET, + "deliver - message (size: {message_size:?}) sent to the dest: {dest:?}, xcm_hash: {xcm_hash:?}" + ); + + // increase delivery fee factor (if required) + Self::on_message_sent_to(message_size, dest); + + Ok(xcm_hash) + } +} + +impl, I: 'static> InspectMessageQueues for Pallet { + fn clear_messages() {} + + /// This router needs to implement `InspectMessageQueues` but doesn't have to + /// return any messages, since it just reuses the `XcmpQueue` router. + fn get_messages() -> Vec<(VersionedLocation, Vec>)> { + Vec::new() + } +} diff --git a/bridges/modules/xcm-bridge-router/src/mock.rs b/bridges/modules/xcm-bridge-router/src/mock.rs new file mode 100644 index 0000000000000..43aae7e72c5bf --- /dev/null +++ b/bridges/modules/xcm-bridge-router/src/mock.rs @@ -0,0 +1,231 @@ +// Copyright (C) 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_router; + +use crate::impls::EnsureIsRemoteBridgeIdResolver; +use bp_xcm_bridge_router::{BridgeState, ResolveBridgeId}; +use codec::Encode; +use frame_support::{ + construct_runtime, derive_impl, parameter_types, + traits::{Contains, Equals}, +}; +use frame_system::EnsureRoot; +use sp_runtime::{traits::ConstU128, BuildStorage}; +use sp_std::cell::RefCell; +use xcm::prelude::*; +use xcm_builder::{ + InspectMessageQueues, NetworkExportTable, NetworkExportTableItem, SovereignPaidRemoteExporter, +}; + +type Block = frame_system::mocking::MockBlock; + +/// HRMP fee. +pub const HRMP_FEE: u128 = 500; +/// Base bridge fee. +pub const BASE_FEE: u128 = 1_000_000; +/// Byte bridge fee. +pub const BYTE_FEE: u128 = 1_000; + +construct_runtime! { + pub enum TestRuntime + { + System: frame_system, + XcmBridgeHubRouter: pallet_xcm_bridge_router, + } +} + +parameter_types! { + pub ThisNetworkId: NetworkId = Polkadot; + pub BridgedNetworkId: NetworkId = Kusama; + pub UniversalLocation: InteriorLocation = [GlobalConsensus(ThisNetworkId::get()), Parachain(1000)].into(); + pub SiblingBridgeHubLocation: Location = ParentThen([Parachain(1002)].into()).into(); + pub BridgeFeeAsset: AssetId = Location::parent().into(); + pub BridgeTable: Vec + = vec![ + NetworkExportTableItem::new( + BridgedNetworkId::get(), + None, + SiblingBridgeHubLocation::get(), + Some((BridgeFeeAsset::get(), BASE_FEE).into()) + ) + ]; + pub UnknownXcmVersionForRoutableLocation: Location = Location::new(2, [GlobalConsensus(BridgedNetworkId::get()), Parachain(9999)]); +} + +#[derive_impl(frame_system::config_preludes::TestDefaultConfig)] +impl frame_system::Config for TestRuntime { + type Block = Block; +} + +/// Simple implementation where every dest resolves to the exact one `BridgeId`. +pub struct EveryDestinationToSameBridgeIdResolver; +impl ResolveBridgeId for EveryDestinationToSameBridgeIdResolver { + type BridgeId = (); + + fn resolve_for_dest(_dest: &Location) -> Option { + Some(()) + } + + fn resolve_for( + _bridged_network: &NetworkId, + _bridged_dest: &InteriorLocation, + ) -> Option { + Some(()) + } +} + +/// An instance of `pallet_xcm_bridge_hub_router` configured to use a remote exporter with the +/// `ExportMessage` instruction, which will be delivered to a sibling parachain using +/// `SiblingBridgeHubLocation`. +#[derive_impl(pallet_xcm_bridge_router::config_preludes::TestDefaultConfig)] +impl pallet_xcm_bridge_router::Config<()> for TestRuntime { + type DestinationVersion = + LatestOrNoneForLocationVersionChecker>; + + type MessageExporter = SovereignPaidRemoteExporter< + pallet_xcm_bridge_router::impls::ViaRemoteBridgeExporter< + TestRuntime, + (), + NetworkExportTable, + BridgedNetworkId, + SiblingBridgeHubLocation, + >, + TestXcmRouter, + UniversalLocation, + >; + + type BridgeIdResolver = EnsureIsRemoteBridgeIdResolver; + type UpdateBridgeStatusOrigin = EnsureRoot; + + type ByteFee = ConstU128; + type FeeAsset = BridgeFeeAsset; +} + +pub struct LatestOrNoneForLocationVersionChecker(sp_std::marker::PhantomData); +impl> GetVersion + for LatestOrNoneForLocationVersionChecker +{ + fn get_version_for(dest: &Location) -> Option { + if LocationValue::contains(dest) { + return None + } + Some(XCM_VERSION) + } +} + +pub struct TestXcmRouter; + +impl TestXcmRouter { + pub fn is_message_sent() -> bool { + !Self::get_messages().is_empty() + } +} + +thread_local! { + pub static SENT_XCM: RefCell)>> = RefCell::new(Vec::new()); +} + +impl SendXcm for TestXcmRouter { + type Ticket = (Location, Xcm<()>); + + fn validate( + destination: &mut Option, + message: &mut Option>, + ) -> SendResult { + let pair = (destination.take().unwrap(), message.take().unwrap()); + Ok((pair, (BridgeFeeAsset::get(), HRMP_FEE).into())) + } + + fn deliver(pair: Self::Ticket) -> Result { + let hash = fake_message_hash(&pair.1); + SENT_XCM.with(|q| q.borrow_mut().push(pair)); + Ok(hash) + } +} + +impl InspectMessageQueues for TestXcmRouter { + fn clear_messages() { + SENT_XCM.with(|q| q.borrow_mut().clear()); + } + + fn get_messages() -> Vec<(VersionedLocation, Vec>)> { + SENT_XCM.with(|q| { + (*q.borrow()) + .clone() + .iter() + .map(|(location, message)| { + ( + VersionedLocation::from(location.clone()), + vec![VersionedXcm::from(message.clone())], + ) + }) + .collect() + }) + } +} + +/// Return test externalities to use in tests. +pub fn new_test_ext() -> sp_io::TestExternalities { + let t = frame_system::GenesisConfig::::default().build_storage().unwrap(); + sp_io::TestExternalities::new(t) +} + +/// Run pallet test. +pub fn run_test(test: impl FnOnce() -> T) -> T { + new_test_ext().execute_with(|| { + System::set_block_number(1); + System::reset_events(); + + test() + }) +} + +pub(crate) fn fake_message_hash(message: &Xcm) -> XcmHash { + message.using_encoded(sp_io::hashing::blake2_256) +} + +pub(crate) fn set_bridge_state_for, I: 'static>( + dest: &Location, + bridge_state: Option, +) -> pallet_xcm_bridge_router::BridgeIdOf { + let bridge_id = ::resolve_for_dest(dest).unwrap(); + if let Some(bridge_state) = bridge_state { + pallet_xcm_bridge_router::Bridges::::insert(&bridge_id, bridge_state); + } else { + pallet_xcm_bridge_router::Bridges::::remove(&bridge_id); + } + bridge_id +} + +pub(crate) fn get_bridge_state_for, I: 'static>( + dest: &Location, +) -> BridgeState { + let bridge_id = ::resolve_for_dest(dest).unwrap(); + pallet_xcm_bridge_router::Bridges::::get(bridge_id) +} + +#[cfg(feature = "runtime-benchmarks")] +impl crate::benchmarking::Config<()> for TestRuntime { + fn ensure_bridged_target_destination() -> Result { + Ok(Location::new(2, [GlobalConsensus(BridgedNetworkId::get())])) + } + fn update_bridge_status_origin() -> Option { + Some(RuntimeOrigin::root()) + } +} diff --git a/bridges/modules/xcm-bridge-router/src/tests.rs b/bridges/modules/xcm-bridge-router/src/tests.rs new file mode 100644 index 0000000000000..7dd36a91eb4b3 --- /dev/null +++ b/bridges/modules/xcm-bridge-router/src/tests.rs @@ -0,0 +1,299 @@ +// Copyright (C) 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 super::*; +use bp_xcm_bridge_router::MINIMAL_DELIVERY_FEE_FACTOR; +use frame_support::assert_ok; +use mock::*; + +use frame_system::{EventRecord, Phase}; +use sp_runtime::traits::Dispatchable; + +#[test] +fn not_applicable_if_destination_is_within_other_network() { + run_test(|| { + // unroutable dest + let dest = Location::new(2, [GlobalConsensus(ByGenesis([0; 32])), Parachain(1000)]); + let xcm: Xcm<()> = vec![ClearOrigin].into(); + + // check that router does not consume when `NotApplicable` + let mut xcm_wrapper = Some(xcm.clone()); + assert_eq!( + XcmBridgeHubRouter::validate(&mut Some(dest.clone()), &mut xcm_wrapper), + Err(SendError::NotApplicable), + ); + // XCM is NOT consumed and untouched + assert_eq!(Some(xcm.clone()), xcm_wrapper); + + // check the full `send_xcm` + assert_eq!(send_xcm::(dest, xcm,), Err(SendError::NotApplicable),); + }); +} + +#[test] +fn exceeds_max_message_size_if_size_is_above_hard_limit() { + run_test(|| { + // routable dest with XCM version + let dest = Location::new(2, [GlobalConsensus(BridgedNetworkId::get()), Parachain(1000)]); + // oversized XCM + let xcm: Xcm<()> = vec![ClearOrigin; HARD_MESSAGE_SIZE_LIMIT as usize].into(); + + // dest is routable with the inner router + assert_ok!(>::MessageExporter::validate( + &mut Some(dest.clone()), + &mut Some(xcm.clone()) + )); + + // check for oversized message + let mut xcm_wrapper = Some(xcm.clone()); + assert_eq!( + XcmBridgeHubRouter::validate(&mut Some(dest.clone()), &mut xcm_wrapper), + Err(SendError::ExceedsMaxMessageSize), + ); + // XCM is consumed by the inner router + assert!(xcm_wrapper.is_none()); + + // check the full `send_xcm` + assert_eq!( + send_xcm::(dest, xcm,), + Err(SendError::ExceedsMaxMessageSize), + ); + }); +} + +#[test] +fn destination_unsupported_if_wrap_version_fails() { + run_test(|| { + // routable dest but we don't know XCM version + let dest = UnknownXcmVersionForRoutableLocation::get(); + let xcm: Xcm<()> = vec![ClearOrigin].into(); + + // dest is routable with the inner router + assert_ok!(>::MessageExporter::validate( + &mut Some(dest.clone()), + &mut Some(xcm.clone()) + )); + + // check that it does not pass XCM version check + let mut xcm_wrapper = Some(xcm.clone()); + assert_eq!( + XcmBridgeHubRouter::validate(&mut Some(dest.clone()), &mut xcm_wrapper), + Err(SendError::DestinationUnsupported), + ); + // XCM is consumed by the inner router + assert!(xcm_wrapper.is_none()); + + // check the full `send_xcm` + assert_eq!( + send_xcm::(dest, xcm,), + Err(SendError::DestinationUnsupported), + ); + }); +} + +#[test] +fn returns_proper_delivery_price() { + run_test(|| { + let dest = Location::new(2, [GlobalConsensus(BridgedNetworkId::get())]); + let xcm: Xcm<()> = vec![ClearOrigin].into(); + let msg_size = xcm.encoded_size(); + + // `BASE_FEE + BYTE_FEE * msg_size` (without `HRMP_FEE`) + let base_cost_formula = || BASE_FEE + BYTE_FEE * (msg_size as u128); + + // initially the base fee is used + let expected_fee = base_cost_formula() + HRMP_FEE; + assert_eq!( + XcmBridgeHubRouter::validate(&mut Some(dest.clone()), &mut Some(xcm.clone())) + .unwrap() + .1 + .get(0), + Some(&(BridgeFeeAsset::get(), expected_fee).into()), + ); + + // but when factor is larger than one, it increases the fee, so it becomes: + // `base_cost_formula() * F` + let factor = FixedU128::from_rational(125, 100); + + // make bridge congested + update fee factor + set_bridge_state_for::( + &dest, + Some(BridgeState { delivery_fee_factor: factor, is_congested: true }), + ); + + let expected_fee = (FixedU128::saturating_from_integer(base_cost_formula()) * factor) + .into_inner() / + FixedU128::DIV + + HRMP_FEE; + assert_eq!( + XcmBridgeHubRouter::validate(&mut Some(dest), &mut Some(xcm)).unwrap().1.get(0), + Some(&(BridgeFeeAsset::get(), expected_fee).into()), + ); + }); +} + +#[test] +fn sent_message_doesnt_increase_factor_if_bridge_is_uncongested() { + run_test(|| { + let dest = Location::new(2, [GlobalConsensus(BridgedNetworkId::get()), Parachain(1000)]); + + // bridge not congested + let old_delivery_fee_factor = FixedU128::from_rational(125, 100); + set_bridge_state_for::( + &dest, + Some(BridgeState { delivery_fee_factor: old_delivery_fee_factor, is_congested: false }), + ); + + assert_eq!( + send_xcm::(dest.clone(), vec![ClearOrigin].into(),).map(drop), + Ok(()), + ); + + assert!(TestXcmRouter::is_message_sent()); + assert_eq!( + old_delivery_fee_factor, + get_bridge_state_for::(&dest).delivery_fee_factor + ); + + assert_eq!(System::events(), vec![]); + }); +} + +#[test] +fn sent_message_increases_factor_if_bridge_is_congested() { + run_test(|| { + let dest = Location::new(2, [GlobalConsensus(BridgedNetworkId::get()), Parachain(1000)]); + + // make bridge congested + update fee factor + let old_delivery_fee_factor = FixedU128::from_rational(125, 100); + set_bridge_state_for::( + &dest, + Some(BridgeState { delivery_fee_factor: old_delivery_fee_factor, is_congested: true }), + ); + + assert_ok!( + send_xcm::(dest.clone(), vec![ClearOrigin].into(),).map(drop) + ); + + assert!(TestXcmRouter::is_message_sent()); + let _delivery_fee_factor = + get_bridge_state_for::(&dest).delivery_fee_factor; + assert!(old_delivery_fee_factor < _delivery_fee_factor); + + // check emitted event + let first_system_event = System::events().first().cloned(); + let _previous_value_ = old_delivery_fee_factor; + assert!(matches!( + first_system_event, + Some(EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::XcmBridgeHubRouter(Event::DeliveryFeeFactorUpdated { + previous_value: _previous_value, + new_value: _delivery_fee_factor, + .. + }), + .. + }) + )); + }); +} + +#[test] +fn get_messages_does_not_return_anything() { + run_test(|| { + assert_ok!(send_xcm::( + (Parent, Parent, GlobalConsensus(BridgedNetworkId::get()), Parachain(1000)).into(), + vec![ClearOrigin].into() + )); + assert_eq!(XcmBridgeHubRouter::get_messages(), vec![]); + }); +} + +#[test] +fn update_bridge_status_works() { + run_test(|| { + let dest = Location::new(2, [GlobalConsensus(BridgedNetworkId::get()), Parachain(1000)]); + let bridge_id = bp_xcm_bridge::BridgeId::new(&UniversalLocation::get(), dest.interior()); + let update_bridge_status = |bridge_id, is_congested| { + let call = RuntimeCall::XcmBridgeHubRouter(Call::update_bridge_status { + bridge_id, + is_congested, + }); + assert_ok!(call.dispatch(RuntimeOrigin::root())); + }; + + assert_eq!( + get_bridge_state_for::(&dest), + BridgeState { delivery_fee_factor: MINIMAL_DELIVERY_FEE_FACTOR, is_congested: false } + ); + + // make congested + update_bridge_status(bridge_id, true); + assert_eq!( + get_bridge_state_for::(&dest), + BridgeState { delivery_fee_factor: MINIMAL_DELIVERY_FEE_FACTOR, is_congested: true } + ); + + // make uncongested + update_bridge_status(bridge_id, false); + assert_eq!( + get_bridge_state_for::(&dest), + BridgeState { delivery_fee_factor: MINIMAL_DELIVERY_FEE_FACTOR, is_congested: false } + ); + }); +} + +#[test] +fn do_update_bridge_status_works() { + run_test(|| { + let dest = Location::new(2, [GlobalConsensus(BridgedNetworkId::get())]); + let bridge_id = bp_xcm_bridge::BridgeId::new(&UniversalLocation::get(), dest.interior()); + // by default is_congested is false + assert_eq!( + get_bridge_state_for::(&dest), + BridgeState { delivery_fee_factor: MINIMAL_DELIVERY_FEE_FACTOR, is_congested: false } + ); + + // update as is_congested=true + Pallet::::do_update_bridge_status(bridge_id, true); + assert_eq!( + get_bridge_state_for::(&dest), + BridgeState { delivery_fee_factor: MINIMAL_DELIVERY_FEE_FACTOR, is_congested: true } + ); + + // increase fee factor when congested + Pallet::::on_message_sent_to(5, dest.clone()); + assert!( + get_bridge_state_for::(&dest).delivery_fee_factor > + MINIMAL_DELIVERY_FEE_FACTOR + ); + // update as is_congested=true - should not reset fee factor + Pallet::::do_update_bridge_status(bridge_id, true); + assert!( + get_bridge_state_for::(&dest).delivery_fee_factor > + MINIMAL_DELIVERY_FEE_FACTOR + ); + + // update as is_congested=false when `Some(..)` + Pallet::::do_update_bridge_status(bridge_id, false); + assert_eq!( + get_bridge_state_for::(&dest), + BridgeState { delivery_fee_factor: MINIMAL_DELIVERY_FEE_FACTOR, is_congested: false } + ); + }) +} diff --git a/bridges/modules/xcm-bridge-router/src/weights.rs b/bridges/modules/xcm-bridge-router/src/weights.rs new file mode 100644 index 0000000000000..9455653c6958d --- /dev/null +++ b/bridges/modules/xcm-bridge-router/src/weights.rs @@ -0,0 +1,71 @@ +// Copyright (C) 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 . + +//! Autogenerated weights for pallet_xcm_bridge_hub_router +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2023-08-03, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `covid`, CPU: `11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz` +//! EXECUTION: , WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024 + +// Executed Command: +// target/release/rip-bridge-node +// benchmark +// pallet +// --chain=dev +// --steps=50 +// --repeat=20 +// --pallet=pallet_xcm_bridge_hub_router +// --extrinsic=* +// --execution=wasm +// --wasm-execution=Compiled +// --heap-pages=4096 +// --output=./modules/xcm-bridge-hub-router/src/weights.rs +// --template=./.maintain/bridge-weight-template.hbs + +#![allow(clippy::all)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{ + traits::Get, + weights::{constants::RocksDbWeight, Weight}, +}; +use sp_std::marker::PhantomData; + +/// Weight functions needed for pallet_xcm_bridge_hub_router. +pub trait WeightInfo { + fn update_bridge_status() -> Weight; +} + +// For backwards compatibility and tests +impl WeightInfo for () { + /// Storage: `ToUnknownXcmRouter::Bridges` (r:1 w:1) + /// Proof: `ToUnknownXcmRouter::Bridges` (`max_values`: None, `max_size`: Some(65), added: 2540, + /// mode: `MaxEncodedLen`) + fn update_bridge_status() -> Weight { + // Proof Size summary in bytes: + // Measured: `109` + // Estimated: `3530` + // Minimum execution time: 12_179_000 picoseconds. + Weight::from_parts(12_679_000, 0) + .saturating_add(Weight::from_parts(0, 3530)) + .saturating_add(RocksDbWeight::get().reads(1)) + .saturating_add(RocksDbWeight::get().writes(1)) + } +} diff --git a/bridges/modules/xcm-bridge/Cargo.toml b/bridges/modules/xcm-bridge/Cargo.toml new file mode 100644 index 0000000000000..6ab92e38a4599 --- /dev/null +++ b/bridges/modules/xcm-bridge/Cargo.toml @@ -0,0 +1,96 @@ +[package] +name = "pallet-xcm-bridge" +description = "Module that adds dynamic bridges/lanes support to XCM infrastructure at the bridge hub." +version = "0.1.0" +authors.workspace = true +edition.workspace = true +license = "GPL-3.0-or-later WITH Classpath-exception-2.0" +repository.workspace = true + +[lints] +workspace = true + +[dependencies] +codec = { workspace = true } +log = { workspace = true } +scale-info = { features = ["derive"], workspace = true } + +# Bridge Dependencies +bp-messages = { workspace = true } +bp-runtime = { workspace = true } +bp-xcm-bridge = { workspace = true } +pallet-bridge-messages = { workspace = true } + +# Substrate Dependencies +frame-benchmarking = { optional = true, workspace = true } +frame-support = { workspace = true } +frame-system = { workspace = true } +sp-core = { workspace = true } +sp-runtime = { workspace = true } +sp-std = { workspace = true } + +# Polkadot Dependencies +polkadot-runtime-common = { workspace = true } +xcm = { workspace = true } +xcm-builder = { workspace = true } +xcm-executor = { workspace = true } + +[dev-dependencies] +bp-header-chain = { workspace = true } +bp-runtime = { workspace = true } +bp-xcm-bridge-router = { workspace = true } +pallet-balances = { workspace = true } +pallet-xcm-bridge-router = { workspace = true } +polkadot-parachain-primitives = { workspace = true } +sp-io = { workspace = true } + +[features] +default = ["std"] +std = [ + "bp-header-chain/std", + "bp-messages/std", + "bp-runtime/std", + "bp-xcm-bridge-router/std", + "bp-xcm-bridge/std", + "codec/std", + "frame-benchmarking/std", + "frame-support/std", + "frame-system/std", + "log/std", + "pallet-balances/std", + "pallet-bridge-messages/std", + "pallet-xcm-bridge-router/std", + "polkadot-parachain-primitives/std", + "polkadot-runtime-common/std", + "scale-info/std", + "sp-core/std", + "sp-io/std", + "sp-runtime/std", + "sp-std/std", + "xcm-builder/std", + "xcm-executor/std", + "xcm/std", +] +runtime-benchmarks = [ + "frame-benchmarking/runtime-benchmarks", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "pallet-balances/runtime-benchmarks", + "pallet-bridge-messages/runtime-benchmarks", + "pallet-xcm-bridge-router/runtime-benchmarks", + "polkadot-parachain-primitives/runtime-benchmarks", + "polkadot-runtime-common/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", + "xcm-builder/runtime-benchmarks", + "xcm-executor/runtime-benchmarks", + "xcm/runtime-benchmarks", +] +try-runtime = [ + "frame-support/try-runtime", + "frame-system/try-runtime", + "pallet-balances/try-runtime", + "pallet-bridge-messages/try-runtime", + "pallet-xcm-bridge-router/try-runtime", + "polkadot-runtime-common/try-runtime", + "sp-runtime/try-runtime", +] diff --git a/bridges/modules/xcm-bridge/src/benchmarking.rs b/bridges/modules/xcm-bridge/src/benchmarking.rs new file mode 100644 index 0000000000000..aec23581c03cf --- /dev/null +++ b/bridges/modules/xcm-bridge/src/benchmarking.rs @@ -0,0 +1,147 @@ +// Copyright (C) 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 . + +//! XCM bridge hub pallet benchmarks. + +#![cfg(feature = "runtime-benchmarks")] + +use crate::{Call, Receiver, ThisChainOf}; +use bp_runtime::BalanceOf; +use bp_xcm_bridge::BridgeLocations; +use frame_benchmarking::v2::*; +use frame_support::{ + assert_ok, + traits::{fungible::Unbalanced, tokens::Precision, Contains, Get}, +}; +use sp_std::boxed::Box; +use xcm_executor::traits::ConvertLocation; + +use sp_runtime::Saturating; +use xcm::prelude::*; + +/// Pallet we're benchmarking here. +pub struct Pallet, I: 'static = ()>(crate::Pallet); + +/// Trait that must be implemented by runtime to be able to benchmark pallet properly. +pub trait Config: crate::Config { + /// Returns a valid origin along with the initial balance (e.g., existential deposit), + /// required for operation `open_bridge`. + /// If `None`, that means that `open_bridge` is not supported. + fn open_bridge_origin() -> Option<(Self::RuntimeOrigin, BalanceOf>)>; +} + +#[instance_benchmarks] +mod benchmarks { + use super::*; + + fn prepare_for_open_bridge, I: 'static>( + ) -> Result<(T::RuntimeOrigin, Box), BenchmarkError> { + let (origin, initial_balance) = T::open_bridge_origin() + .ok_or(BenchmarkError::Override(BenchmarkResult::from_weight(Weight::MAX)))?; + let bridge_destination_universal_location: Box = + Box::new([GlobalConsensus(crate::Pallet::::bridged_network_id()?)].into()); + let expected = crate::Pallet::::bridge_locations_from_origin( + origin.clone(), + bridge_destination_universal_location, + )?; + + if !T::AllowWithoutBridgeDeposit::contains(expected.bridge_origin_relative_location()) { + // fund origin's sovereign account + let bridge_owner_account = T::BridgeOriginAccountIdConverter::convert_location( + expected.bridge_origin_relative_location(), + ) + .ok_or(BenchmarkError::Stop("InvalidBridgeOriginAccount"))?; + + T::Currency::increase_balance( + &bridge_owner_account, + initial_balance.saturating_add(T::BridgeDeposit::get()), + Precision::BestEffort, + )?; + } + + Ok((origin, expected)) + } + + #[benchmark] + fn open_bridge() -> Result<(), BenchmarkError> { + let (origin, locations) = prepare_for_open_bridge::()?; + let bridge_destination_universal_location: Box = + Box::new(locations.bridge_destination_universal_location().clone().into()); + assert!(crate::Pallet::::bridge(locations.bridge_id()).is_none()); + + #[extrinsic_call] + _(origin as T::RuntimeOrigin, bridge_destination_universal_location, None); + + assert!(crate::Pallet::::bridge(locations.bridge_id()).is_some()); + Ok(()) + } + + #[benchmark] + fn close_bridge() -> Result<(), BenchmarkError> { + let (origin, locations) = prepare_for_open_bridge::()?; + let bridge_destination_universal_location: Box = + Box::new(locations.bridge_destination_universal_location().clone().into()); + assert!(crate::Pallet::::bridge(locations.bridge_id()).is_none()); + + // open bridge + assert_ok!(crate::Pallet::::open_bridge( + origin.clone(), + bridge_destination_universal_location.clone(), + None, + )); + assert!(crate::Pallet::::bridge(locations.bridge_id()).is_some()); + + #[extrinsic_call] + _(origin as T::RuntimeOrigin, bridge_destination_universal_location, 10); + + assert!(crate::Pallet::::bridge(locations.bridge_id()).is_none()); + Ok(()) + } + + #[benchmark] + fn update_notification_receiver() -> Result<(), BenchmarkError> { + let (origin, locations) = prepare_for_open_bridge::()?; + let bridge_destination_universal_location: Box = + Box::new(locations.bridge_destination_universal_location().clone().into()); + assert!(crate::Pallet::::bridge(locations.bridge_id()).is_none()); + + // open bridge with None + assert_ok!(crate::Pallet::::open_bridge( + origin.clone(), + bridge_destination_universal_location.clone(), + None, + )); + assert_eq!( + crate::Pallet::::bridge(locations.bridge_id()).map(|b| b.maybe_notify), + Some(None) + ); + + #[extrinsic_call] + _( + origin as T::RuntimeOrigin, + bridge_destination_universal_location, + Some(Receiver::new(1, 5)), + ); + + assert_eq!( + crate::Pallet::::bridge(locations.bridge_id()).map(|b| b.maybe_notify), + Some(Some(Receiver::new(1, 5))) + ); + Ok(()) + } + + impl_benchmark_test_suite!(Pallet, crate::mock::new_test_ext(), crate::mock::TestRuntime); +} diff --git a/bridges/modules/xcm-bridge/src/congestion.rs b/bridges/modules/xcm-bridge/src/congestion.rs new file mode 100644 index 0000000000000..32b0ca4889f6c --- /dev/null +++ b/bridges/modules/xcm-bridge/src/congestion.rs @@ -0,0 +1,231 @@ +// 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 . + +//! The module contains utilities for handling congestion between the bridge hub and routers. + +use crate::{Bridges, Config, DispatchChannelStatusProvider, LOG_TARGET}; +use bp_xcm_bridge::{BridgeId, LocalXcmChannelManager, Receiver}; +use codec::{Decode, Encode}; +use sp_runtime::traits::Convert; +use sp_std::{marker::PhantomData, vec::Vec}; +use xcm::latest::{send_xcm, Location, SendXcm, Xcm}; +use xcm_builder::{DispatchBlob, DispatchBlobError}; + +/// Limits for handling congestion. +#[derive(Debug, Decode, Encode)] +pub struct CongestionLimits { + /// Maximal number of messages in the outbound bridge queue. Once we reach this limit, we + /// suspend a bridge. + pub outbound_lane_congested_threshold: bp_messages::MessageNonce, + /// After we have suspended the bridge, we wait until number of messages in the outbound bridge + /// queue drops to this count, before sending resuming the bridge. + pub outbound_lane_uncongested_threshold: bp_messages::MessageNonce, + /// Maximal number of messages in the outbound bridge queue after we have suspended the bridge. + /// Once we reach this limit, we stop exporting more messages. + pub outbound_lane_stop_threshold: bp_messages::MessageNonce, +} + +impl CongestionLimits { + /// Checks if limits are valid. + pub fn is_valid(&self) -> bool { + self.outbound_lane_uncongested_threshold < self.outbound_lane_congested_threshold && + self.outbound_lane_stop_threshold > self.outbound_lane_congested_threshold + } +} + +impl Default for CongestionLimits { + fn default() -> Self { + Self { + outbound_lane_congested_threshold: 8_192, + outbound_lane_uncongested_threshold: 1_024, + outbound_lane_stop_threshold: 12_288, + } + } +} + +/// Switches the implementation of [`LocalXcmChannelManager`] based on the `local_origin`. +/// +/// - `HereXcmChannelManager` is applied when the origin is `Here`. +/// - Otherwise, `LocalConsensusXcmChannelManager` is used. +/// +/// This is useful when the `pallet-xcm-bridge` needs to support both: +/// - A local router deployed on the same chain as the `pallet-xcm-bridge`. +/// - A remote router deployed on a different chain than the `pallet-xcm-bridge`. +pub struct HereOrLocalConsensusXcmChannelManager< + Bridge, + HereXcmChannelManager, + LocalConsensusXcmChannelManager, +>(PhantomData<(Bridge, HereXcmChannelManager, LocalConsensusXcmChannelManager)>); +impl< + Bridge: Encode + sp_std::fmt::Debug + Copy, + HereXcmChannelManager: LocalXcmChannelManager, + LocalConsensusXcmChannelManager: LocalXcmChannelManager, + > LocalXcmChannelManager + for HereOrLocalConsensusXcmChannelManager< + Bridge, + HereXcmChannelManager, + LocalConsensusXcmChannelManager, + > +{ + type Error = (); + + fn suspend_bridge(local_origin: &Location, bridge: Bridge) -> Result<(), Self::Error> { + if local_origin.eq(&Location::here()) { + HereXcmChannelManager::suspend_bridge(local_origin, bridge).map_err(|e| { + log::error!( + target: LOG_TARGET, + "HereXcmChannelManager::suspend_bridge error: {e:?} for local_origin: {:?} and bridge: {:?}", + local_origin, + bridge, + ); + () + }) + } else { + LocalConsensusXcmChannelManager::suspend_bridge(local_origin, bridge).map_err(|e| { + log::error!( + target: LOG_TARGET, + "LocalConsensusXcmChannelManager::suspend_bridge error: {e:?} for local_origin: {:?} and bridge: {:?}", + local_origin, + bridge, + ); + () + }) + } + } + + fn resume_bridge(local_origin: &Location, bridge: Bridge) -> Result<(), Self::Error> { + if local_origin.eq(&Location::here()) { + HereXcmChannelManager::resume_bridge(local_origin, bridge).map_err(|e| { + log::error!( + target: LOG_TARGET, + "HereXcmChannelManager::resume_bridge error: {e:?} for local_origin: {:?} and bridge: {:?}", + local_origin, + bridge, + ); + () + }) + } else { + LocalConsensusXcmChannelManager::resume_bridge(local_origin, bridge).map_err(|e| { + log::error!( + target: LOG_TARGET, + "LocalConsensusXcmChannelManager::resume_bridge error: {e:?} for local_origin: {:?} and bridge: {:?}", + local_origin, + bridge, + ); + () + }) + } + } +} + +/// Manages the local XCM channels by sending XCM messages with the `update_bridge_status` extrinsic +/// to the `local_origin`. The `XcmProvider` type converts the encoded call to `XCM`, which is then +/// sent by `XcmSender` to the `local_origin`. This is useful, for example, when a router with +/// [`xcm::prelude::ExportMessage`] is deployed on a different chain, and we want to control +/// congestion by sending XCMs. +pub struct UpdateBridgeStatusXcmChannelManager( + PhantomData<(T, I, XcmProvider, XcmSender)>, +); +impl, I: 'static, XcmProvider: Convert, Xcm<()>>, XcmSender: SendXcm> + UpdateBridgeStatusXcmChannelManager +{ + fn update_bridge_status( + local_origin: &Location, + bridge_id: BridgeId, + is_congested: bool, + ) -> Result<(), ()> { + // check the bridge and get `maybe_notify` callback. + let bridge = Bridges::::get(&bridge_id).ok_or(())?; + let Some(Receiver { pallet_index, call_index }) = bridge.maybe_notify else { + // `local_origin` did not set `maybe_notify`, so nothing to notify, so it is ok. + return Ok(()) + }; + + // constructing expected call + let remote_runtime_call = (pallet_index, call_index, bridge_id, is_congested); + // construct XCM + let xcm = XcmProvider::convert(remote_runtime_call.encode()); + log::trace!( + target: LOG_TARGET, + "UpdateBridgeStatusXcmChannelManager is going to send status with is_congested: {:?} to the local_origin: {:?} and bridge: {:?} as xcm: {:?}", + is_congested, + local_origin, + bridge, + xcm, + ); + + // send XCM + send_xcm::(local_origin.clone(), xcm) + .map(|result| { + log::warn!( + target: LOG_TARGET, + "UpdateBridgeStatusXcmChannelManager successfully sent status with is_congested: {:?} to the local_origin: {:?} and bridge: {:?} with result: {:?}", + is_congested, + local_origin, + bridge, + result, + ); + () + }) + .map_err(|e| { + log::error!( + target: LOG_TARGET, + "UpdateBridgeStatusXcmChannelManager failed to send status with is_congested: {:?} to the local_origin: {:?} and bridge: {:?} with error: {:?}", + is_congested, + local_origin, + bridge, + e, + ); + () + }) + } +} +impl, I: 'static, XcmProvider: Convert, Xcm<()>>, XcmSender: SendXcm> + LocalXcmChannelManager + for UpdateBridgeStatusXcmChannelManager +{ + type Error = (); + + fn suspend_bridge(local_origin: &Location, bridge: BridgeId) -> Result<(), Self::Error> { + Self::update_bridge_status(local_origin, bridge, true) + } + + fn resume_bridge(local_origin: &Location, bridge: BridgeId) -> Result<(), Self::Error> { + Self::update_bridge_status(local_origin, bridge, false) + } +} + +/// Adapter that ties together the [`DispatchBlob`] trait with the [`DispatchChannelStatusProvider`] +/// trait. The idea is that [`DispatchBlob`] triggers message dispatch/delivery on the receiver +/// side, while [`DispatchChannelStatusProvider`] provides a status check to ensure the dispatch +/// channel is active (not congested). +pub struct BlobDispatcherWithChannelStatus( + PhantomData<(ChannelDispatch, ChannelStatus)>, +); +impl DispatchBlob + for BlobDispatcherWithChannelStatus +{ + fn dispatch_blob(blob: Vec) -> Result<(), DispatchBlobError> { + ChannelDispatch::dispatch_blob(blob) + } +} +impl DispatchChannelStatusProvider + for BlobDispatcherWithChannelStatus +{ + fn is_congested(with: &Location) -> bool { + ChannelStatus::is_congested(with) + } +} diff --git a/bridges/modules/xcm-bridge/src/dispatcher.rs b/bridges/modules/xcm-bridge/src/dispatcher.rs new file mode 100644 index 0000000000000..40535b832ebdf --- /dev/null +++ b/bridges/modules/xcm-bridge/src/dispatcher.rs @@ -0,0 +1,275 @@ +// 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 . + +//! The code that allows to use the pallet (`pallet-xcm-bridge`) as inbound +//! bridge messages dispatcher. Internally, it just forwards inbound blob to the +//! XCM-level blob dispatcher, which pushes message to some other queue (e.g. +//! to HRMP queue with the sibling target chain). +//! +//! This code is executed at the target bridge hub. + +use crate::{Config, DispatchChannelStatusProvider, Pallet, LOG_TARGET}; + +use bp_messages::target_chain::{DispatchMessage, MessageDispatch}; +use bp_runtime::messages::MessageDispatchResult; +use bp_xcm_bridge::XcmAsPlainPayload; +use codec::{Decode, DecodeWithMemTracking, Encode}; +use frame_support::{weights::Weight, CloneNoBound, EqNoBound, PartialEqNoBound}; +use pallet_bridge_messages::{Config as BridgeMessagesConfig, WeightInfoExt}; +use scale_info::TypeInfo; +use sp_runtime::SaturatedConversion; +use xcm::prelude::*; +use xcm_builder::{DispatchBlob, DispatchBlobError}; + +/// Message dispatch result type for single message. +#[derive( + CloneNoBound, + EqNoBound, + PartialEqNoBound, + Encode, + Decode, + DecodeWithMemTracking, + Debug, + TypeInfo, +)] +pub enum XcmBlobMessageDispatchResult { + /// We've been unable to decode message payload. + InvalidPayload, + /// Message has been dispatched. + Dispatched, + /// Message has **NOT** been dispatched because of given error. + NotDispatched(#[codec(skip)] Option), +} + +/// An easy way to access associated messages pallet weights. +type MessagesPalletWeights = + >::BridgeMessagesPalletInstance>>::WeightInfo; + +impl, I: 'static> MessageDispatch for Pallet +where + T: BridgeMessagesConfig, +{ + type DispatchPayload = XcmAsPlainPayload; + type DispatchLevelResult = XcmBlobMessageDispatchResult; + type LaneId = T::LaneId; + + fn is_active(lane: Self::LaneId) -> bool { + Pallet::::bridge_by_lane_id(&lane) + .and_then(|(_, bridge)| (*bridge.bridge_origin_relative_location).try_into().ok()) + .map(|recipient: Location| !T::BlobDispatcher::is_congested(&recipient)) + .unwrap_or(false) + } + + fn dispatch_weight( + message: &mut DispatchMessage, + ) -> Weight { + match message.data.payload { + Ok(ref payload) => { + let payload_size = payload.encoded_size().saturated_into(); + MessagesPalletWeights::::message_dispatch_weight(payload_size) + }, + Err(_) => Weight::zero(), + } + } + + fn dispatch( + message: DispatchMessage, + ) -> MessageDispatchResult { + let payload = match message.data.payload { + Ok(payload) => payload, + Err(e) => { + log::error!( + target: LOG_TARGET, + "dispatch - payload error: {e:?} for lane_id: {:?} and message_nonce: {:?}", + message.key.lane_id, + message.key.nonce + ); + return MessageDispatchResult { + unspent_weight: Weight::zero(), + dispatch_level_result: XcmBlobMessageDispatchResult::InvalidPayload, + } + }, + }; + let dispatch_level_result = match T::BlobDispatcher::dispatch_blob(payload) { + Ok(_) => { + log::debug!( + target: LOG_TARGET, + "dispatch - `DispatchBlob::dispatch_blob` was ok for lane_id: {:?} and message_nonce: {:?}", + message.key.lane_id, + message.key.nonce + ); + XcmBlobMessageDispatchResult::Dispatched + }, + Err(e) => { + log::error!( + target: LOG_TARGET, + "dispatch - `DispatchBlob::dispatch_blob` failed with error: {e:?} for lane_id: {:?} and message_nonce: {:?}", + message.key.lane_id, + message.key.nonce + ); + XcmBlobMessageDispatchResult::NotDispatched(Some(e)) + }, + }; + MessageDispatchResult { unspent_weight: Weight::zero(), dispatch_level_result } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{mock::*, Bridges, LaneToBridge, LanesManagerOf}; + + use bp_messages::{target_chain::DispatchMessageData, LaneIdType, MessageKey}; + use bp_xcm_bridge::{Bridge, BridgeLocations, BridgeState}; + use frame_support::assert_ok; + use pallet_bridge_messages::InboundLaneStorage; + + fn bridge() -> (Box, TestLaneIdType) { + let origin = OpenBridgeOrigin::sibling_parachain_origin(); + let with = bridged_asset_hub_universal_location(); + let locations = + XcmOverBridge::bridge_locations_from_origin(origin, Box::new(with.into())).unwrap(); + let lane_id = locations.calculate_lane_id(xcm::latest::VERSION).unwrap(); + (locations, lane_id) + } + + fn run_test_with_opened_bridge(test: impl FnOnce()) { + run_test(|| { + let (bridge, lane_id) = bridge(); + + if !Bridges::::contains_key(bridge.bridge_id()) { + // insert bridge + Bridges::::insert( + bridge.bridge_id(), + Bridge { + bridge_origin_relative_location: Box::new( + bridge.bridge_origin_relative_location().clone().into(), + ), + bridge_origin_universal_location: Box::new( + bridge.bridge_origin_universal_location().clone().into(), + ), + bridge_destination_universal_location: Box::new( + bridge.bridge_destination_universal_location().clone().into(), + ), + state: BridgeState::Opened, + deposit: None, + lane_id, + maybe_notify: None, + }, + ); + LaneToBridge::::insert(lane_id, bridge.bridge_id()); + + // create lanes + let lanes_manager = LanesManagerOf::::new(); + if lanes_manager.create_inbound_lane(lane_id).is_ok() { + assert_eq!( + 0, + lanes_manager + .active_inbound_lane(lane_id) + .unwrap() + .storage() + .data() + .last_confirmed_nonce + ); + } + if lanes_manager.create_outbound_lane(lane_id).is_ok() { + assert!(lanes_manager + .active_outbound_lane(lane_id) + .unwrap() + .queued_messages() + .is_empty()); + } + } + assert_ok!(XcmOverBridge::do_try_state()); + + test(); + }); + } + + fn invalid_message() -> DispatchMessage, TestLaneIdType> { + DispatchMessage { + key: MessageKey { lane_id: TestLaneIdType::try_new(1, 2).unwrap(), nonce: 1 }, + data: DispatchMessageData { payload: Err(codec::Error::from("test")) }, + } + } + + fn valid_message() -> DispatchMessage, TestLaneIdType> { + DispatchMessage { + key: MessageKey { lane_id: TestLaneIdType::try_new(1, 2).unwrap(), nonce: 1 }, + data: DispatchMessageData { payload: Ok(vec![42]) }, + } + } + + #[test] + fn dispatcher_is_inactive_when_channel_with_target_chain_is_congested() { + run_test_with_opened_bridge(|| { + let bridge = bridge(); + let with = bridge.0.bridge_origin_relative_location(); + let lane_id = bridge.1; + TestBlobDispatcher::make_congested(with); + assert!(!XcmOverBridge::is_active(lane_id)); + }); + } + + #[test] + fn dispatcher_is_active_when_channel_with_target_chain_is_not_congested() { + run_test_with_opened_bridge(|| { + assert!(XcmOverBridge::is_active(bridge().1)); + }); + } + + #[test] + fn dispatch_weight_is_zero_if_we_have_failed_to_decode_message() { + run_test(|| { + assert_eq!(XcmOverBridge::dispatch_weight(&mut invalid_message()), Weight::zero()); + }); + } + + #[test] + fn dispatch_weight_is_non_zero_if_we_have_decoded_message() { + run_test(|| { + assert_ne!(XcmOverBridge::dispatch_weight(&mut valid_message()), Weight::zero()); + }); + } + + #[test] + fn message_is_not_dispatched_when_we_have_failed_to_decode_message() { + run_test(|| { + assert_eq!( + XcmOverBridge::dispatch(invalid_message()), + MessageDispatchResult { + unspent_weight: Weight::zero(), + dispatch_level_result: XcmBlobMessageDispatchResult::InvalidPayload, + }, + ); + assert!(!TestBlobDispatcher::is_dispatched()); + }); + } + + #[test] + fn message_is_dispatched_when_we_have_decoded_message() { + run_test(|| { + assert_eq!( + XcmOverBridge::dispatch(valid_message()), + MessageDispatchResult { + unspent_weight: Weight::zero(), + dispatch_level_result: XcmBlobMessageDispatchResult::Dispatched, + }, + ); + assert!(TestBlobDispatcher::is_dispatched()); + }); + } +} diff --git a/bridges/modules/xcm-bridge/src/exporter.rs b/bridges/modules/xcm-bridge/src/exporter.rs new file mode 100644 index 0000000000000..deb9fd9b2e51a --- /dev/null +++ b/bridges/modules/xcm-bridge/src/exporter.rs @@ -0,0 +1,1184 @@ +// 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 . + +//! The code that allows to use the pallet (`pallet-xcm-bridge`) as XCM message +//! exporter at the sending bridge hub. Internally, it just enqueues outbound blob +//! in the messages pallet queue. +//! +//! This code is executed at the source bridge hub. + +use crate::{Config, Pallet, LOG_TARGET}; + +use crate::{BridgeOf, Bridges}; + +use bp_messages::{ + source_chain::{MessagesBridge, OnMessagesDelivered}, + MessageNonce, +}; +use bp_xcm_bridge::{BridgeId, BridgeState, LocalXcmChannelManager, XcmAsPlainPayload}; +use frame_support::{ensure, traits::Get}; +use pallet_bridge_messages::{ + Config as BridgeMessagesConfig, Error, Pallet as BridgeMessagesPallet, +}; +use polkadot_runtime_common::xcm_sender::PriceForMessageDelivery; +use xcm::prelude::*; +use xcm_builder::{HaulBlob, HaulBlobError, HaulBlobExporter}; +use xcm_executor::traits::ExportXcm; + +/// An easy way to access `HaulBlobExporter`. +/// +/// Note: Set no price for `HaulBlobExporter`, because `ExportXcm for Pallet` handles the fees. +pub type PalletAsHaulBlobExporter = HaulBlobExporter< + DummyHaulBlob, + >::BridgedNetwork, + >::DestinationVersion, + (), +>; +/// An easy way to access associated messages pallet. +type MessagesPallet = BridgeMessagesPallet>::BridgeMessagesPalletInstance>; + +impl, I: 'static> ExportXcm for Pallet +where + T: BridgeMessagesConfig, +{ + type Ticket = ( + BridgeId, + BridgeOf, + as MessagesBridge>::SendMessageArgs, + XcmHash, + ); + + fn validate( + network: NetworkId, + channel: u32, + universal_source: &mut Option, + destination: &mut Option, + message: &mut Option>, + ) -> Result<(Self::Ticket, Assets), SendError> { + log::trace!( + target: LOG_TARGET, + "Validate for network: {network:?}, channel: {channel:?}, universal_source: {universal_source:?}, destination: {destination:?}" + ); + + // `HaulBlobExporter` may consume the `universal_source` and `destination` arguments, so + // let's save them before + let bridge_origin_universal_location = + universal_source.clone().ok_or(SendError::MissingArgument)?; + // Note: watch out this is `ExportMessage::destination`, which is relative to the `network`, + // which means it does not contain `GlobalConsensus`, We need to find `BridgeId` with + // `Self::bridge_locations` which requires **universal** location for destination. + let bridge_destination_universal_location = { + let dest = destination.clone().ok_or(SendError::MissingArgument)?; + match dest.global_consensus() { + Ok(dest_network) => { + log::trace!( + target: LOG_TARGET, + "Destination: {dest:?} is already universal, checking dest_network: {dest_network:?} and network: {network:?} if matches: {:?}", + dest_network == network + ); + ensure!(dest_network == network, SendError::NotApplicable); + // ok, `dest` looks like a universal location, so let's use it + dest + }, + Err(_) => { + // `dest` is not a universal location, so we need to prepend it with + // `GlobalConsensus`. + dest.pushed_front_with(GlobalConsensus(network)).map_err(|error_data| { + log::error!( + target: LOG_TARGET, + "Destination: {:?} is not a universal and prepending with {:?} failed!", + error_data.0, + error_data.1, + ); + SendError::NotApplicable + })? + }, + } + }; + + // prepare the origin relative location + let bridge_origin_relative_location = + bridge_origin_universal_location.relative_to(&T::UniversalLocation::get()); + + // then we are able to compute the `BridgeId` and find `LaneId` used to send messages + let locations = Self::bridge_locations( + bridge_origin_relative_location, + bridge_destination_universal_location.into(), + ) + .map_err(|e| { + log::error!( + target: LOG_TARGET, + "Validate `bridge_locations` with error: {e:?}", + ); + SendError::NotApplicable + })?; + let bridge = Self::bridge(locations.bridge_id()).ok_or_else(|| { + log::error!( + target: LOG_TARGET, + "No opened bridge for requested bridge_origin_relative_location: {:?} (bridge_origin_universal_location: {:?}) and bridge_destination_universal_location: {:?}", + locations.bridge_origin_relative_location(), + locations.bridge_origin_universal_location(), + locations.bridge_destination_universal_location(), + ); + SendError::NotApplicable + })?; + + // Get the potential price for a message over the bridge. + let price_for_delivery = message + .as_ref() + .map(|msg| T::MessageExportPrice::price_for_delivery(*locations.bridge_id(), msg)); + + // check if we are able to route the message. We use the existing ` HaulBlobExporter ` for + // that. It will make all required changes and will encode a message properly, so that the + // `DispatchBlob` at the bridged xcm-bridge will be able to decode it. + let ((blob, id), mut price) = PalletAsHaulBlobExporter::::validate( + network, + channel, + universal_source, + destination, + message, + )?; + + // Add `price_for_delivery` to the `price`. + if let Some(delivery_prices) = price_for_delivery { + for dp in delivery_prices.into_inner() { + price.push(dp); + } + } + + // Here, we know that the message is relevant to this pallet instance, so let's check for + // congestion defense. + if bridge.state == BridgeState::HardSuspended { + log::error!( + target: LOG_TARGET, + "Bridge for requested bridge_origin_relative_location: {:?} (bridge_origin_universal_location: {:?}) and bridge_destination_universal_location: {:?} \ + is suspended and does not accept more messages!", + locations.bridge_origin_relative_location(), + locations.bridge_origin_universal_location(), + locations.bridge_destination_universal_location(), + ); + return Err(SendError::Transport("Exporter is suspended!")); + } + + let bridge_message = MessagesPallet::::validate_message(bridge.lane_id, &blob) + .map_err(|e| { + match e { + Error::LanesManager(ref ei) => + log::error!(target: LOG_TARGET, "LanesManager: {ei:?}"), + Error::MessageRejectedByPallet(ref ei) => + log::error!(target: LOG_TARGET, "MessageRejectedByPallet: {ei:?}"), + Error::ReceptionConfirmation(ref ei) => + log::error!(target: LOG_TARGET, "ReceptionConfirmation: {ei:?}"), + _ => (), + }; + + log::error!( + target: LOG_TARGET, + "XCM message {:?} cannot be exported because of bridge error: {:?} on bridge {:?} and laneId: {:?}", + id, + e, + locations, + bridge.lane_id, + ); + SendError::Transport("BridgeValidateError") + })?; + + Ok(((*locations.bridge_id(), bridge, bridge_message, id), price)) + } + + fn deliver( + (bridge_id, bridge, bridge_message, id): Self::Ticket, + ) -> Result { + let artifacts = MessagesPallet::::send_message(bridge_message); + + log::info!( + target: LOG_TARGET, + "XCM message {:?} has been enqueued at bridge {:?} and lane_id: {:?} with nonce {}", + id, + bridge_id, + bridge.lane_id, + artifacts.nonce, + ); + + // maybe we need switch to congested state + Self::on_bridge_message_enqueued(bridge_id, bridge, artifacts.enqueued_messages); + + Ok(id) + } +} + +impl, I: 'static> OnMessagesDelivered for Pallet { + fn on_messages_delivered(lane_id: T::LaneId, enqueued_messages: MessageNonce) { + Self::on_bridge_messages_delivered(lane_id, enqueued_messages); + } +} + +impl, I: 'static> Pallet { + /// Called when new message is pushed onto outbound bridge queue. + fn on_bridge_message_enqueued( + bridge_id: BridgeId, + bridge: BridgeOf, + enqueued_messages: MessageNonce, + ) { + // if the bridge queue is not congested, we don't want to do anything + let is_congested = + enqueued_messages > T::CongestionLimits::get().outbound_lane_congested_threshold; + if !is_congested { + return + } + + // check if the lane is already suspended or not. + match bridge.state { + BridgeState::SoftSuspended => { + if enqueued_messages > T::CongestionLimits::get().outbound_lane_stop_threshold { + // If its suspended and reached `outbound_lane_stop_threshold`, we stop + // accepting new messages (a.k.a. start dropping). + Bridges::::mutate_extant(bridge_id, |bridge| { + bridge.state = BridgeState::HardSuspended; + }); + return + } else { + // We still can accept new messages to the suspended bridge, hoping that it'll + // be actually resumed soon + return + } + }, + BridgeState::HardSuspended => { + // We cannot accept new messages and start dropping messages, until the outbound + // lane goes below the drop limit. + return + }, + _ => { + // otherwise, continue handling the suspension + }, + } + + // else - suspend the bridge + let result_bridge_origin_relative_location = + (*bridge.bridge_origin_relative_location).clone().try_into(); + let bridge_origin_relative_location = match &result_bridge_origin_relative_location { + Ok(bridge_origin_relative_location) => bridge_origin_relative_location, + Err(_) => { + log::error!( + target: LOG_TARGET, + "Failed to convert the bridge {:?} origin location {:?}", + bridge_id, + bridge.bridge_origin_relative_location, + ); + + return + }, + }; + let suspend_result = + T::LocalXcmChannelManager::suspend_bridge(bridge_origin_relative_location, bridge_id); + match suspend_result { + Ok(_) => { + log::debug!( + target: LOG_TARGET, + "Suspended the bridge {:?}, originated by the {:?}", + bridge_id, + bridge.bridge_origin_relative_location, + ); + }, + Err(e) => { + log::error!( + target: LOG_TARGET, + "Failed to suspended the bridge {:?}, originated by the {:?}: {:?}", + bridge_id, + bridge.bridge_origin_relative_location, + e, + ); + + return + }, + } + + // and remember that we have suspended the bridge + Bridges::::mutate_extant(bridge_id, |bridge| { + bridge.state = BridgeState::SoftSuspended; + }); + } + + /// Must be called whenever we receive a message delivery confirmation. + fn on_bridge_messages_delivered(lane_id: T::LaneId, enqueued_messages: MessageNonce) { + // if the bridge queue is still congested, we don't want to do anything + let is_congested = + enqueued_messages > T::CongestionLimits::get().outbound_lane_uncongested_threshold; + if is_congested { + // and if it is below the `stop_threshold` + if enqueued_messages < T::CongestionLimits::get().outbound_lane_stop_threshold { + if let Some((bridge_id, bridge)) = Self::bridge_by_lane_id(&lane_id) { + if let BridgeState::HardSuspended = bridge.state { + // we allow exporting again + Bridges::::mutate_extant(bridge_id, |b| { + b.state = BridgeState::SoftSuspended; + }); + } + } + } + return + } + + // if we have not suspended the bridge before (or it is closed), we don't want to do + // anything + let (bridge_id, bridge) = match Self::bridge_by_lane_id(&lane_id) { + Some(bridge) + if bridge.1.state == BridgeState::SoftSuspended || + bridge.1.state == BridgeState::HardSuspended => + bridge, + _ => { + // if there is no bridge, or it has been closed, then we don't need to send resume + // signal to the local origin - it has closed bridge itself, so it should have + // already pruned everything else + return + }, + }; + + // else - resume the bridge + let bridge_origin_relative_location = (*bridge.bridge_origin_relative_location).try_into(); + let bridge_origin_relative_location = match bridge_origin_relative_location { + Ok(bridge_origin_relative_location) => bridge_origin_relative_location, + Err(e) => { + log::error!( + target: LOG_TARGET, + "Failed to convert the bridge {:?} location for lane_id: {:?}, error {:?}", + bridge_id, + lane_id, + e, + ); + + return + }, + }; + + let resume_result = + T::LocalXcmChannelManager::resume_bridge(&bridge_origin_relative_location, bridge_id); + match resume_result { + Ok(_) => { + log::debug!( + target: LOG_TARGET, + "Resumed the bridge {:?} and lane_id: {:?}, originated by the {:?}", + bridge_id, + lane_id, + bridge_origin_relative_location, + ); + }, + Err(e) => { + log::error!( + target: LOG_TARGET, + "Failed to resume the bridge {:?} and lane_id: {:?}, originated by the {:?}: {:?}", + bridge_id, + lane_id, + bridge_origin_relative_location, + e, + ); + + return + }, + } + + // and forget that we have previously suspended the bridge + Bridges::::mutate_extant(bridge_id, |bridge| { + bridge.state = BridgeState::Opened; + }); + } +} + +/// Dummy implementation of the `HaulBlob` trait that is never called. +/// +/// We are using `HaulBlobExporter`, which requires `HaulBlob` implementation. It assumes that +/// there's a single channel between two bridge hubs - `HaulBlob` only accepts the blob and nothing +/// else. But bridge messages pallet may have a dedicated channel (lane) for every pair of bridged +/// chains. So we are using our own `ExportXcm` implementation, but to utilize `HaulBlobExporter` we +/// still need this `DummyHaulBlob`. +pub struct DummyHaulBlob; + +impl HaulBlob for DummyHaulBlob { + fn haul_blob(_blob: XcmAsPlainPayload) -> Result<(), HaulBlobError> { + Err(HaulBlobError::Transport("DummyHaulBlob")) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{mock::*, Bridges, LanesManagerOf}; + + use bp_runtime::RangeInclusiveExt; + use bp_xcm_bridge::{Bridge, BridgeLocations, BridgeState, Receiver}; + use bp_xcm_bridge_router::MINIMAL_DELIVERY_FEE_FACTOR; + use frame_support::{ + assert_err, assert_ok, + traits::{Contains, EnsureOrigin}, + }; + use pallet_xcm_bridge_router::ResolveBridgeId; + use xcm_builder::{NetworkExportTable, UnpaidRemoteExporter}; + use xcm_executor::traits::export_xcm; + + fn universal_source() -> InteriorLocation { + SiblingUniversalLocation::get() + } + + fn bridged_relative_destination() -> InteriorLocation { + BridgedRelativeDestination::get() + } + + fn bridged_universal_destination() -> InteriorLocation { + BridgedUniversalDestination::get() + } + + fn open_lane(origin: RuntimeOrigin) -> (BridgeLocations, TestLaneIdType) { + // open expected outbound lane + let with = bridged_asset_hub_universal_location(); + let locations = + XcmOverBridge::bridge_locations_from_origin(origin.clone(), Box::new(with.into())) + .unwrap(); + let lane_id = locations.calculate_lane_id(xcm::latest::VERSION).unwrap(); + + if !Bridges::::contains_key(locations.bridge_id()) { + // fund origin (if needed) + if !>::AllowWithoutBridgeDeposit::contains( + locations.bridge_origin_relative_location(), + ) { + fund_origin_sovereign_account( + &locations, + BridgeDeposit::get() + ExistentialDeposit::get(), + ); + } + + // open bridge + assert_ok!(XcmOverBridge::do_open_bridge(locations.clone(), lane_id, true, None)); + } + assert_ok!(XcmOverBridge::do_try_state()); + + (*locations, lane_id) + } + + fn open_lane_and_send_regular_message( + source_origin: RuntimeOrigin, + ) -> (BridgeId, TestLaneIdType) { + let (locations, lane_id) = open_lane(source_origin); + + // now let's try to enqueue message using our `ExportXcm` implementation + export_xcm::( + BridgedRelayNetwork::get(), + 0, + locations.bridge_origin_universal_location().clone(), + locations.bridge_destination_universal_location().clone(), + vec![Instruction::ClearOrigin].into(), + ) + .unwrap(); + + (*locations.bridge_id(), lane_id) + } + + #[test] + fn exporter_works() { + run_test(|| { + let (_, lane_id) = + open_lane_and_send_regular_message(OpenBridgeOrigin::sibling_parachain_origin()); + + // double check that the message has been pushed to the expected lane + // (it should already been checked during `send_message` call) + assert!(!LanesManagerOf::::new() + .active_outbound_lane(lane_id) + .unwrap() + .queued_messages() + .is_empty()); + }); + } + + #[test] + fn exporter_does_not_suspend_the_bridge_if_outbound_bridge_queue_is_not_congested() { + run_test(|| { + let (bridge_id, _) = + open_lane_and_send_regular_message(OpenBridgeOrigin::sibling_parachain_origin()); + assert!(!TestLocalXcmChannelManager::is_bridge_suspened(&bridge_id)); + assert_eq!(XcmOverBridge::bridge(&bridge_id).unwrap().state, BridgeState::Opened); + }); + } + + #[test] + fn exporter_does_not_suspend_the_bridge_if_it_is_already_suspended() { + run_test(|| { + let (bridge_id, _) = + open_lane_and_send_regular_message(OpenBridgeOrigin::sibling_parachain_origin()); + Bridges::::mutate_extant(bridge_id, |bridge| { + bridge.state = BridgeState::SoftSuspended; + }); + for _ in 1..TestCongestionLimits::get().outbound_lane_congested_threshold { + open_lane_and_send_regular_message(OpenBridgeOrigin::sibling_parachain_origin()); + } + + open_lane_and_send_regular_message(OpenBridgeOrigin::sibling_parachain_origin()); + assert!(!TestLocalXcmChannelManager::is_bridge_suspened(&bridge_id)); + }); + } + + #[test] + fn exporter_suspends_the_bridge_if_outbound_bridge_queue_is_congested() { + run_test(|| { + let (bridge_id, _) = + open_lane_and_send_regular_message(OpenBridgeOrigin::sibling_parachain_origin()); + for _ in 1..TestCongestionLimits::get().outbound_lane_congested_threshold { + open_lane_and_send_regular_message(OpenBridgeOrigin::sibling_parachain_origin()); + } + + assert!(!TestLocalXcmChannelManager::is_bridge_suspened(&bridge_id)); + assert_eq!(XcmOverBridge::bridge(&bridge_id).unwrap().state, BridgeState::Opened); + + open_lane_and_send_regular_message(OpenBridgeOrigin::sibling_parachain_origin()); + assert!(TestLocalXcmChannelManager::is_bridge_suspened(&bridge_id)); + assert_eq!( + XcmOverBridge::bridge(&bridge_id).unwrap().state, + BridgeState::SoftSuspended + ); + + // send more messages to reach `outbound_lane_stop_threshold` + for _ in TestCongestionLimits::get().outbound_lane_congested_threshold.. + TestCongestionLimits::get().outbound_lane_stop_threshold + { + open_lane_and_send_regular_message(OpenBridgeOrigin::sibling_parachain_origin()); + } + assert_eq!( + XcmOverBridge::bridge(&bridge_id).unwrap().state, + BridgeState::HardSuspended + ); + }); + } + + #[test] + fn bridge_is_not_resumed_if_outbound_bridge_queue_is_still_congested() { + run_test(|| { + let (bridge_id, lane_id) = + open_lane_and_send_regular_message(OpenBridgeOrigin::sibling_parachain_origin()); + Bridges::::mutate_extant(bridge_id, |bridge| { + bridge.state = BridgeState::SoftSuspended; + }); + XcmOverBridge::on_bridge_messages_delivered( + lane_id, + TestCongestionLimits::get().outbound_lane_uncongested_threshold + 1, + ); + + assert!(!TestLocalXcmChannelManager::is_bridge_resumed(&bridge_id)); + assert_eq!( + XcmOverBridge::bridge(&bridge_id).unwrap().state, + BridgeState::SoftSuspended + ); + }); + } + + #[test] + fn bridge_is_not_resumed_if_it_was_not_suspended_before() { + run_test(|| { + let (bridge_id, lane_id) = + open_lane_and_send_regular_message(OpenBridgeOrigin::sibling_parachain_origin()); + XcmOverBridge::on_bridge_messages_delivered( + lane_id, + TestCongestionLimits::get().outbound_lane_uncongested_threshold, + ); + + assert!(!TestLocalXcmChannelManager::is_bridge_resumed(&bridge_id)); + assert_eq!(XcmOverBridge::bridge(&bridge_id).unwrap().state, BridgeState::Opened); + }); + } + + #[test] + fn exporter_respects_stop_threshold() { + run_test(|| { + let (bridge_id, lane_id) = + open_lane_and_send_regular_message(OpenBridgeOrigin::sibling_parachain_origin()); + let xcm: Xcm<()> = vec![ClearOrigin].into(); + + // Opened - exporter works + assert_eq!(XcmOverBridge::bridge(&bridge_id).unwrap().state, BridgeState::Opened); + assert_ok!(XcmOverBridge::validate( + BridgedRelayNetwork::get(), + 0, + &mut Some(universal_source()), + &mut Some(bridged_relative_destination()), + &mut Some(xcm.clone()), + ),); + + // SoftSuspended - exporter still works + XcmOverBridge::on_bridge_message_enqueued( + bridge_id, + XcmOverBridge::bridge(&bridge_id).unwrap(), + TestCongestionLimits::get().outbound_lane_congested_threshold + 1, + ); + assert_eq!( + XcmOverBridge::bridge(&bridge_id).unwrap().state, + BridgeState::SoftSuspended + ); + assert_ok!(XcmOverBridge::validate( + BridgedRelayNetwork::get(), + 0, + &mut Some(universal_source()), + &mut Some(bridged_relative_destination()), + &mut Some(xcm.clone()), + ),); + + // HardSuspended - exporter stops working + XcmOverBridge::on_bridge_message_enqueued( + bridge_id, + XcmOverBridge::bridge(&bridge_id).unwrap(), + TestCongestionLimits::get().outbound_lane_stop_threshold + 1, + ); + assert_eq!( + XcmOverBridge::bridge(&bridge_id).unwrap().state, + BridgeState::HardSuspended + ); + assert_err!( + XcmOverBridge::validate( + BridgedRelayNetwork::get(), + 0, + &mut Some(universal_source()), + &mut Some(bridged_relative_destination()), + &mut Some(xcm.clone()), + ), + SendError::Transport("Exporter is suspended!"), + ); + + // Back to SoftSuspended - exporter again works + XcmOverBridge::on_bridge_messages_delivered( + lane_id, + TestCongestionLimits::get().outbound_lane_stop_threshold - 1, + ); + assert_eq!( + XcmOverBridge::bridge(&bridge_id).unwrap().state, + BridgeState::SoftSuspended + ); + assert_ok!(XcmOverBridge::validate( + BridgedRelayNetwork::get(), + 0, + &mut Some(universal_source()), + &mut Some(bridged_relative_destination()), + &mut Some(xcm.clone()), + ),); + + // Back to Opened - exporter works + XcmOverBridge::on_bridge_messages_delivered( + lane_id, + TestCongestionLimits::get().outbound_lane_uncongested_threshold - 1, + ); + assert_eq!(XcmOverBridge::bridge(&bridge_id).unwrap().state, BridgeState::Opened); + assert_ok!(XcmOverBridge::validate( + BridgedRelayNetwork::get(), + 0, + &mut Some(universal_source()), + &mut Some(bridged_relative_destination()), + &mut Some(xcm.clone()), + ),); + }); + } + + #[test] + fn bridge_is_resumed_when_enough_messages_are_delivered() { + run_test(|| { + let (bridge_id, lane_id) = + open_lane_and_send_regular_message(OpenBridgeOrigin::sibling_parachain_origin()); + Bridges::::mutate_extant(bridge_id, |bridge| { + bridge.state = BridgeState::SoftSuspended; + }); + XcmOverBridge::on_bridge_messages_delivered( + lane_id, + TestCongestionLimits::get().outbound_lane_uncongested_threshold, + ); + + assert!(TestLocalXcmChannelManager::is_bridge_resumed(&bridge_id)); + assert_eq!(XcmOverBridge::bridge(&bridge_id).unwrap().state, BridgeState::Opened); + }); + } + + #[test] + fn export_fails_if_argument_is_missing() { + run_test(|| { + assert_eq!( + XcmOverBridge::validate( + BridgedRelayNetwork::get(), + 0, + &mut None, + &mut Some(bridged_relative_destination()), + &mut Some(Vec::new().into()), + ), + Err(SendError::MissingArgument), + ); + + assert_eq!( + XcmOverBridge::validate( + BridgedRelayNetwork::get(), + 0, + &mut Some(universal_source()), + &mut None, + &mut Some(Vec::new().into()), + ), + Err(SendError::MissingArgument), + ); + }) + } + + #[test] + fn exporter_computes_correct_lane_id() { + run_test(|| { + assert_ne!(bridged_universal_destination(), bridged_relative_destination()); + + let locations = BridgeLocations::bridge_locations( + UniversalLocation::get(), + SiblingLocation::get(), + bridged_universal_destination(), + BridgedRelayNetwork::get(), + ) + .unwrap(); + let expected_bridge_id = locations.bridge_id(); + let expected_lane_id = locations.calculate_lane_id(xcm::latest::VERSION).unwrap(); + + if LanesManagerOf::::new() + .create_outbound_lane(expected_lane_id) + .is_ok() + { + Bridges::::insert( + expected_bridge_id, + Bridge { + bridge_origin_relative_location: Box::new( + locations.bridge_origin_relative_location().clone().into(), + ), + bridge_origin_universal_location: Box::new( + locations.bridge_origin_universal_location().clone().into(), + ), + bridge_destination_universal_location: Box::new( + locations.bridge_destination_universal_location().clone().into(), + ), + state: BridgeState::Opened, + deposit: None, + lane_id: expected_lane_id, + maybe_notify: None, + }, + ); + } + + let ticket = XcmOverBridge::validate( + BridgedRelayNetwork::get(), + 0, + &mut Some(universal_source()), + // Note: The `ExportMessage` expects relative `InteriorLocation` in the + // `BridgedRelayNetwork`. + &mut Some(bridged_relative_destination()), + &mut Some(Vec::new().into()), + ) + .unwrap() + .0; + assert_eq!(&ticket.0, expected_bridge_id); + assert_eq!(ticket.1.lane_id, expected_lane_id); + }); + } + + #[test] + fn pallet_as_exporter_is_compatible_with_pallet_xcm_bridge_hub_router_for_export_message() { + run_test(|| { + // valid routable destination + let dest = Location::new(2, BridgedUniversalDestination::get()); + + // open bridge + let origin = OpenBridgeOrigin::sibling_parachain_origin(); + let origin_as_location = + OpenBridgeOriginOf::::try_origin(origin.clone()).unwrap(); + let (bridge, expected_lane_id) = open_lane(origin); + + // we need to set `UniversalLocation` for `sibling_parachain_origin` for + // `XcmOverBridgeWrappedWithExportMessageRouterInstance`. + ExportMessageOriginUniversalLocation::set(Some(SiblingUniversalLocation::get())); + + // check compatible bridge_id + assert_eq!( + bridge.bridge_id(), + &>::BridgeIdResolver::resolve_for_dest(&dest) + .unwrap() + ); + + // check before - no messages + assert_eq!( + pallet_bridge_messages::Pallet::::outbound_lane_data( + expected_lane_id + ) + .unwrap() + .queued_messages() + .saturating_len(), + 0 + ); + + // send `ExportMessage(message)` by `UnpaidRemoteExporter`. + ExecuteXcmOverSendXcm::set_origin_for_execute(origin_as_location.clone()); + assert_ok!(send_xcm::< + UnpaidRemoteExporter< + NetworkExportTable, + ExecuteXcmOverSendXcm, + UniversalLocation, + >, + >(dest.clone(), Xcm::<()>::default())); + + // we need to set `UniversalLocation` for `sibling_parachain_origin` for + // `XcmOverBridgeWrappedWithExportMessageRouterInstance`. + ExportMessageOriginUniversalLocation::set(Some(SiblingUniversalLocation::get())); + // send `ExportMessage(message)` by `pallet_xcm_bridge_hub_router`. + ExecuteXcmOverSendXcm::set_origin_for_execute(origin_as_location); + assert_ok!(send_xcm::( + dest, + Xcm::<()>::default() + )); + + // check after - a message ready to be relayed + assert_eq!( + pallet_bridge_messages::Pallet::::outbound_lane_data( + expected_lane_id + ) + .unwrap() + .queued_messages() + .saturating_len(), + 2 + ); + }) + } + + #[test] + fn pallet_as_exporter_is_compatible_with_pallet_xcm_bridge_hub_router_for_export_xcm() { + run_test(|| { + // valid routable destination + let dest = Location::new(2, BridgedUniversalDestination::get()); + + // open bridge as a root on the local chain, which should be converted as + // `Location::here()` + let (bridge, expected_lane_id) = open_lane(RuntimeOrigin::root()); + + // check compatible bridge_id + assert_eq!( + bridge.bridge_id(), + &>::BridgeIdResolver::resolve_for_dest(&dest) + .unwrap() + ); + + // check before - no messages + assert_eq!( + pallet_bridge_messages::Pallet::::outbound_lane_data( + expected_lane_id + ) + .unwrap() + .queued_messages() + .saturating_len(), + 0 + ); + + // trigger `ExportXcm` by `pallet_xcm_bridge_hub_router`. + assert_ok!(send_xcm::(dest, Xcm::<()>::default())); + + // check after - a message ready to be relayed + assert_eq!( + pallet_bridge_messages::Pallet::::outbound_lane_data( + expected_lane_id + ) + .unwrap() + .queued_messages() + .saturating_len(), + 1 + ); + }) + } + + #[test] + fn validate_works() { + run_test(|| { + let xcm: Xcm<()> = vec![ClearOrigin].into(); + + // check that router does not consume when `NotApplicable` + let mut xcm_wrapper = Some(xcm.clone()); + let mut universal_source_wrapper = Some(universal_source()); + + // wrong `NetworkId` + let mut dest_wrapper = Some(bridged_relative_destination()); + assert_eq!( + XcmOverBridge::validate( + NetworkId::ByGenesis([0; 32]), + 0, + &mut universal_source_wrapper, + &mut dest_wrapper, + &mut xcm_wrapper, + ), + Err(SendError::NotApplicable), + ); + // dest and xcm is NOT consumed and untouched + assert_eq!(&Some(xcm.clone()), &xcm_wrapper); + assert_eq!(&Some(universal_source()), &universal_source_wrapper); + assert_eq!(&Some(bridged_relative_destination()), &dest_wrapper); + + // dest starts with wrong `NetworkId` + let mut invalid_dest_wrapper = Some( + [GlobalConsensus(NetworkId::ByGenesis([0; 32])), Parachain(BRIDGED_ASSET_HUB_ID)] + .into(), + ); + assert_eq!( + XcmOverBridge::validate( + BridgedRelayNetwork::get(), + 0, + &mut Some(universal_source()), + &mut invalid_dest_wrapper, + &mut xcm_wrapper, + ), + Err(SendError::NotApplicable), + ); + // dest and xcm is NOT consumed and untouched + assert_eq!(&Some(xcm.clone()), &xcm_wrapper); + assert_eq!(&Some(universal_source()), &universal_source_wrapper); + assert_eq!( + &Some( + [ + GlobalConsensus(NetworkId::ByGenesis([0; 32]),), + Parachain(BRIDGED_ASSET_HUB_ID) + ] + .into() + ), + &invalid_dest_wrapper + ); + + // no opened lane for dest + let mut dest_without_lane_wrapper = + Some([GlobalConsensus(BridgedRelayNetwork::get()), Parachain(5679)].into()); + assert_eq!( + XcmOverBridge::validate( + BridgedRelayNetwork::get(), + 0, + &mut Some(universal_source()), + &mut dest_without_lane_wrapper, + &mut xcm_wrapper, + ), + Err(SendError::NotApplicable), + ); + // dest and xcm is NOT consumed and untouched + assert_eq!(&Some(xcm.clone()), &xcm_wrapper); + assert_eq!(&Some(universal_source()), &universal_source_wrapper); + assert_eq!( + &Some([GlobalConsensus(BridgedRelayNetwork::get(),), Parachain(5679)].into()), + &dest_without_lane_wrapper + ); + + // ok + let (locations, _) = open_lane(OpenBridgeOrigin::sibling_parachain_origin()); + let mut dest_wrapper = Some(bridged_relative_destination()); + assert_ok!(XcmOverBridge::validate( + BridgedRelayNetwork::get(), + 0, + &mut Some(universal_source()), + &mut dest_wrapper, + &mut xcm_wrapper, + )); + // dest and xcm IS consumed + assert_eq!(None, xcm_wrapper); + assert_eq!(&Some(universal_source()), &universal_source_wrapper); + assert_eq!(None, dest_wrapper); + + // send more messages to reach `outbound_lane_congested_threshold` + for _ in 0..=TestCongestionLimits::get().outbound_lane_congested_threshold { + open_lane_and_send_regular_message(OpenBridgeOrigin::sibling_parachain_origin()); + } + // bridge is suspended but exporter accepts more messages + assert_eq!( + XcmOverBridge::bridge(locations.bridge_id()).unwrap().state, + BridgeState::SoftSuspended + ); + + // export still can accept more messages + assert_ok!(XcmOverBridge::validate( + BridgedRelayNetwork::get(), + 0, + &mut Some(universal_source()), + &mut Some(bridged_relative_destination()), + &mut Some(xcm.clone()), + )); + + // send more messages to reach `outbound_lane_stop_threshold` + for _ in TestCongestionLimits::get().outbound_lane_congested_threshold.. + TestCongestionLimits::get().outbound_lane_stop_threshold + { + open_lane_and_send_regular_message(OpenBridgeOrigin::sibling_parachain_origin()); + } + + // bridge is suspended but exporter CANNOT accept more messages + assert_eq!( + XcmOverBridge::bridge(locations.bridge_id()).unwrap().state, + BridgeState::HardSuspended + ); + + // export still can accept more messages + assert_err!( + XcmOverBridge::validate( + BridgedRelayNetwork::get(), + 0, + &mut Some(universal_source()), + &mut Some(bridged_relative_destination()), + &mut Some(xcm.clone()), + ), + SendError::Transport("Exporter is suspended!"), + ); + }); + } + + #[test] + fn congestion_with_pallet_xcm_bridge_hub_router_works() { + run_test(|| { + // valid routable destination + let dest = Location::new(2, BridgedUniversalDestination::get()); + + fn router_bridge_state, I: 'static>( + dest: &Location, + ) -> pallet_xcm_bridge_router::BridgeState { + let bridge_id = + ::resolve_for_dest(dest).unwrap(); + pallet_xcm_bridge_router::Bridges::::get(&bridge_id) + } + + // open two bridges + let origin = OpenBridgeOrigin::sibling_parachain_origin(); + let origin_as_location = + OpenBridgeOriginOf::::try_origin(origin.clone()).unwrap(); + let (bridge_1, expected_lane_id_1) = open_lane(origin); + let (bridge_2, expected_lane_id_2) = open_lane(RuntimeOrigin::root()); + assert_ne!(expected_lane_id_1, expected_lane_id_2); + assert_ne!(bridge_1.bridge_id(), bridge_2.bridge_id()); + + // we need to set `UniversalLocation` for `sibling_parachain_origin` for + // `XcmOverBridgeWrappedWithExportMessageRouterInstance`. + ExportMessageOriginUniversalLocation::set(Some(SiblingUniversalLocation::get())); + + // we need to update `maybe_notify` for `bridge_1` with `pallet_index` of + // `XcmOverBridgeWrappedWithExportMessageRouter`, + Bridges::::mutate_extant(bridge_1.bridge_id(), |bridge| { + bridge.maybe_notify = Some(Receiver::new(57, 0)); + }); + + // check before + // bridges are opened + assert_eq!( + XcmOverBridge::bridge(bridge_1.bridge_id()).unwrap().state, + BridgeState::Opened + ); + assert_eq!( + XcmOverBridge::bridge(bridge_2.bridge_id()).unwrap().state, + BridgeState::Opened + ); + // both routers are uncongested + assert!( + !router_bridge_state::< + TestRuntime, + XcmOverBridgeWrappedWithExportMessageRouterInstance, + >(&dest) + .is_congested + ); + assert!( + !router_bridge_state::(&dest) + .is_congested + ); + assert!(!TestLocalXcmChannelManager::is_bridge_suspened(bridge_1.bridge_id())); + assert!(!TestLocalXcmChannelManager::is_bridge_suspened(bridge_2.bridge_id())); + assert!(!TestLocalXcmChannelManager::is_bridge_resumed(bridge_1.bridge_id())); + assert!(!TestLocalXcmChannelManager::is_bridge_resumed(bridge_2.bridge_id())); + + // make bridges congested with sending too much messages + for _ in 1..(TestCongestionLimits::get().outbound_lane_congested_threshold + 2) { + // send `ExportMessage(message)` by `pallet_xcm_bridge_hub_router`. + ExecuteXcmOverSendXcm::set_origin_for_execute(origin_as_location.clone()); + assert_ok!(send_xcm::( + dest.clone(), + Xcm::<()>::default() + )); + + // call direct `ExportXcm` by `pallet_xcm_bridge_hub_router`. + assert_ok!(send_xcm::( + dest.clone(), + Xcm::<()>::default() + )); + } + + // checks after + // bridges are suspended + assert_eq!( + XcmOverBridge::bridge(bridge_1.bridge_id()).unwrap().state, + BridgeState::SoftSuspended + ); + assert_eq!( + XcmOverBridge::bridge(bridge_2.bridge_id()).unwrap().state, + BridgeState::SoftSuspended + ); + // both routers are congested + assert!( + router_bridge_state::< + TestRuntime, + XcmOverBridgeWrappedWithExportMessageRouterInstance, + >(&dest) + .is_congested + ); + assert!( + router_bridge_state::(&dest) + .is_congested + ); + assert!(TestLocalXcmChannelManager::is_bridge_suspened(bridge_1.bridge_id())); + assert!(TestLocalXcmChannelManager::is_bridge_suspened(bridge_2.bridge_id())); + assert!(!TestLocalXcmChannelManager::is_bridge_resumed(bridge_1.bridge_id())); + assert!(!TestLocalXcmChannelManager::is_bridge_resumed(bridge_2.bridge_id())); + + // make bridges uncongested to trigger resume signal + XcmOverBridge::on_bridge_messages_delivered( + expected_lane_id_1, + TestCongestionLimits::get().outbound_lane_uncongested_threshold, + ); + XcmOverBridge::on_bridge_messages_delivered( + expected_lane_id_2, + TestCongestionLimits::get().outbound_lane_uncongested_threshold, + ); + + // bridges are again opened + assert_eq!( + XcmOverBridge::bridge(bridge_1.bridge_id()).unwrap().state, + BridgeState::Opened + ); + assert_eq!( + XcmOverBridge::bridge(bridge_2.bridge_id()).unwrap().state, + BridgeState::Opened + ); + // both routers are uncongested + assert_eq!( + router_bridge_state::< + TestRuntime, + XcmOverBridgeWrappedWithExportMessageRouterInstance, + >(&dest), + pallet_xcm_bridge_router::BridgeState { + delivery_fee_factor: MINIMAL_DELIVERY_FEE_FACTOR, + is_congested: false + } + ); + assert_eq!( + router_bridge_state::(&dest), + pallet_xcm_bridge_router::BridgeState { + delivery_fee_factor: MINIMAL_DELIVERY_FEE_FACTOR, + is_congested: false + } + ); + assert!(TestLocalXcmChannelManager::is_bridge_resumed(bridge_1.bridge_id())); + assert!(TestLocalXcmChannelManager::is_bridge_resumed(bridge_2.bridge_id())); + }) + } +} diff --git a/bridges/modules/xcm-bridge/src/lib.rs b/bridges/modules/xcm-bridge/src/lib.rs new file mode 100644 index 0000000000000..6720a845cc8bd --- /dev/null +++ b/bridges/modules/xcm-bridge/src/lib.rs @@ -0,0 +1,938 @@ +// Copyright 2019-2021 Parity Technologies (UK) Ltd. +// This file is part of Parity Bridges Common. + +// Parity Bridges Common is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity Bridges Common is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity Bridges Common. If not, see . + +//! Module that adds XCM support to bridge pallets. The pallet allows to dynamically +//! open and close bridges between local (to this pallet location) and remote XCM +//! destinations. +//! +//! The `pallet_xcm_bridge_hub` pallet is used to manage (open, close) bridges between chains from +//! different consensuses. The new extrinsics `fn open_bridge` and `fn close_bridge` are introduced. +//! Other chains can manage channels with different bridged global consensuses. +//! +//! # Concept of `lane` and `LaneId` +//! +//! There is another `pallet_bridge_messages` pallet that handles inbound/outbound lanes for +//! messages. Each lane is a unique connection between two chains from different consensuses and is +//! identified by `LaneId`. `LaneId` is generated once when a new bridge is requested by `fn +//! open_bridge`. It is generated by `BridgeLocations::calculate_lane_id` based on the following +//! parameters: +//! - Source `bridge_origin_universal_location` (latest XCM) +//! - Destination `bridge_destination_universal_location` (latest XCM) +//! - XCM version (both sides of the bridge must use the same parameters to generate the same +//! `LaneId`) +//! - `bridge_origin_universal_location`, `bridge_destination_universal_location` is converted to +//! the `Versioned*` structs +//! +//! `LaneId` is expected to never change because: +//! - We need the same `LaneId` on both sides of the bridge, as `LaneId` is part of the message key +//! proofs. +//! - Runtime upgrades are entirely asynchronous. +//! - We already have a running production Polkadot/Kusama bridge that uses `LaneId([0, 0, 0, 0])`. +//! +//! `LaneId` is backward compatible, meaning it can be encoded/decoded from the older format `[u8; +//! 4]` used for static lanes, as well as the new format `H256` generated by +//! `BridgeLocations::calculate_lane_id`. +//! +//! # Concept of `bridge` and `BridgeId` +//! +//! The `pallet_xcm_bridge_hub` pallet needs to store some metadata about opened bridges. The bridge +//! (or bridge metadata) is stored under the `BridgeId` key. +//! +//! `BridgeId` is generated from `bridge_origin_relative_location` and +//! `bridge_origin_universal_location` using the `latest` XCM structs. `BridgeId` is not transferred +//! over the bridge; it is only important for local consensus. It essentially serves as an index/key +//! to bridge metadata. All the XCM infrastructure around `XcmExecutor`, `SendXcm`, `ExportXcm` use +//! the `latest` XCM, so `BridgeId` must remain compatible with the `latest` XCM. For example, we +//! have an `ExportXcm` implementation in `exporter.rs` that handles the `ExportMessage` instruction +//! with `universal_source` and `destination` (latest XCM), so we need to create `BridgeId` and the +//! corresponding `LaneId`. +//! +//! # Migrations and State +//! +//! This pallet implements `try_state`, ensuring compatibility and checking everything so we know if +//! any migration is needed. `do_try_state` checks for `BridgeId` compatibility, which is +//! recalculated on runtime upgrade. Upgrading to a new XCM version should not break anything, +//! except removing older XCM versions. In such cases, we need to add migration for `BridgeId` and +//! stored `Versioned*` structs and update `LaneToBridge` mapping, but this won't affect `LaneId` +//! over the bridge. +//! +//! # How to Open a Bridge? +//! +//! The `pallet_xcm_bridge_hub` pallet has the extrinsic `fn open_bridge` and an important +//! configuration `pallet_xcm_bridge::Config::OpenBridgeOrigin`, which translates the call's +//! origin to the XCM `Location` and converts it to the `bridge_origin_universal_location`. With the +//! current setup, this origin/location is expected to be either the relay chain or a sibling +//! parachain as one side of the bridge. Another parameter is +//! `bridge_destination_universal_location`, which is the other side of the bridge from a different +//! global consensus. +//! +//! Every bridge between two XCM locations has a dedicated lane in associated +//! messages pallet. Assuming that this pallet is deployed at the bridge hub +//! 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 `BridgeDeposit`); +//! +//! 3) the sibling parachain opens the bridge by sending XCM `Transact` instruction with the +//! `open_bridge` call. The `BridgeDeposit` 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. +//! +//! # Example +//! +//! Example of opening a bridge between some random parachains from Polkadot and Kusama: +//! +//! 0. Let's have: +//! - BridgeHubPolkadot with `UniversalLocation` = `[GlobalConsensus(Polkadot), Parachain(1002)]` +//! - BridgeHubKusama with `UniversalLocation` = `[GlobalConsensus(Kusama), Parachain(1002)]` +//! 1. The Polkadot local sibling parachain `Location::new(1, Parachain(1234))` must send some DOTs +//! to its sovereign account on BridgeHubPolkadot to cover `BridgeDeposit`, fees for `Transact`, +//! and the existential deposit. +//! 2. Send a call to the BridgeHubPolkadot from the local sibling parachain: `Location::new(1, +//! Parachain(1234))` ``` xcm::Transact( origin_kind: OriginKind::Xcm, +//! XcmOverBridgeHubKusama::open_bridge( VersionedInteriorLocation::V4([GlobalConsensus(Kusama), +//! Parachain(4567)].into()), ); ) ``` +//! 3. Check the stored bridge metadata and generated `LaneId`. +//! 4. The Kusama local sibling parachain `Location::new(1, Parachain(4567))` must send some KSMs to +//! its sovereign account +//! on BridgeHubKusama to cover `BridgeDeposit`, fees for `Transact`, and the existential deposit. +//! 5. Send a call to the BridgeHubKusama from the local sibling parachain: `Location::new(1, +//! Parachain(4567))` ``` xcm::Transact( origin_kind: OriginKind::Xcm, +//! XcmOverBridgeHubKusama::open_bridge( +//! VersionedInteriorLocation::V4([GlobalConsensus(Polkadot), Parachain(1234)].into()), ); ) ``` +//! 6. Check the stored bridge metadata and generated `LaneId`. +//! 7. Both `LaneId`s from steps 3 and 6 must be the same (see above _Concept of `lane` and +//! `LaneId`_). +//! 8. Run the bridge messages relayer for `LaneId`. +//! 9. Send messages from both sides. +//! +//! The opening bridge holds the configured `BridgeDeposit` from the origin's sovereign account, but +//! this deposit is returned when the bridge is closed with `fn close_bridge`. + +#![warn(missing_docs)] +#![cfg_attr(not(feature = "std"), no_std)] + +use bp_messages::{LaneState, MessageNonce}; +use bp_runtime::{AccountIdOf, BalanceOf, RangeInclusiveExt}; +pub use bp_xcm_bridge::{Bridge, BridgeId, BridgeLocations, BridgeState, Deposit, Receiver}; +use bp_xcm_bridge::{ + BridgeLocationsError, ChannelStatusProvider as DispatchChannelStatusProvider, DepositOf, + LocalXcmChannelManager, +}; +use frame_support::{traits::fungible::MutateHold, DefaultNoBound}; +use frame_system::Config as SystemConfig; +use pallet_bridge_messages::{Config as BridgeMessagesConfig, LanesManagerError}; +use polkadot_runtime_common::xcm_sender::PriceForMessageDelivery; +use sp_std::{boxed::Box, vec::Vec}; +use xcm::prelude::*; +use xcm_builder::DispatchBlob; +use xcm_executor::traits::ConvertLocation; + +pub use bp_xcm_bridge::XcmAsPlainPayload; +pub use dispatcher::XcmBlobMessageDispatchResult; +pub use exporter::PalletAsHaulBlobExporter; +pub use pallet::*; + +pub mod benchmarking; +pub mod congestion; +mod dispatcher; +mod exporter; +pub mod migration; +mod mock; +#[cfg(test)] +mod tests; +pub use weights::WeightInfo; +pub mod weights; + +/// 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::*, + traits::{tokens::Precision, Contains}, + }; + use frame_system::pallet_prelude::{BlockNumberFor, *}; + + /// The reason for this pallet placing a hold on funds. + #[pallet::composite_enum] + pub enum HoldReason { + /// The funds are held as a deposit for opened bridge. + #[codec(index = 0)] + BridgeDeposit, + } + + #[pallet::config] + #[pallet::disable_frame_system_supertrait_check] + pub trait Config: + BridgeMessagesConfig + { + /// Benchmarks results from runtime we're plugged into. + type WeightInfo: WeightInfo; + + /// 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 as a relative location of bridged `GlobalConsensus`. + #[pallet::constant] + type BridgedNetwork: Get; + /// Associated messages pallet instance that bridges us with the + /// `BridgedNetworkId` consensus. + type BridgeMessagesPalletInstance: 'static; + + /// Price of a single message export to the bridged consensus (`Self::BridgedNetwork`). + type MessageExportPrice: PriceForMessageDelivery; + /// Checks the XCM version for the destination. + type DestinationVersion: GetVersion; + + /// The origin that is allowed to call privileged operations on the pallet, e.g. open/close + /// bridge for locations. + type ForceOrigin: EnsureOrigin<::RuntimeOrigin>; + /// A set of XCM locations within a local consensus system that are allowed to open + /// bridges with remote destinations. + type OpenBridgeOrigin: EnsureOrigin< + ::RuntimeOrigin, + Success = Location, + >; + /// A converter between a location and a sovereign account. + type BridgeOriginAccountIdConverter: ConvertLocation>>; + + /// Amount of this chain native tokens that is reserved on the sibling parachain account + /// when bridge open request is registered. + #[pallet::constant] + type BridgeDeposit: Get>>; + /// Currency used to pay for bridge registration. + type Currency: MutateHold< + AccountIdOf>, + Balance = BalanceOf>, + Reason = Self::RuntimeHoldReason, + >; + /// The overarching runtime hold reason. + type RuntimeHoldReason: From>; + /// Do not hold `Self::BridgeDeposit` for the location of `Self::OpenBridgeOrigin`. + /// For example, it is possible to make an exception for a system parachain or relay. + type AllowWithoutBridgeDeposit: Contains; + + /// Local XCM channel manager. Dedicated to exporting capabilities when handling congestion + /// with the sending side. + type LocalXcmChannelManager: LocalXcmChannelManager; + /// XCM-level dispatcher for inbound bridge messages. + type BlobDispatcher: DispatchBlob + DispatchChannelStatusProvider; + /// Limits for handling congestion. + type CongestionLimits: Get; + } + + /// An alias for the bridge metadata. + pub type BridgeOf = Bridge, LaneIdOf>; + /// An alias for this chain. + pub type ThisChainOf = + pallet_bridge_messages::ThisChainOf>::BridgeMessagesPalletInstance>; + /// An alias for lane identifier type. + pub type LaneIdOf = + >::BridgeMessagesPalletInstance>>::LaneId; + /// An alias for the associated lanes manager. + pub type LanesManagerOf = + pallet_bridge_messages::LanesManager>::BridgeMessagesPalletInstance>; + /// Weight info of the given pallet. + pub type WeightInfoOf = >::WeightInfo; + + #[pallet::pallet] + pub struct Pallet(PhantomData<(T, I)>); + + #[pallet::hooks] + impl, I: 'static> Hooks> for Pallet { + fn integrity_test() { + assert!( + Self::bridged_network_id().is_ok(), + "Configured `T::BridgedNetwork`: {:?} does not contain `GlobalConsensus` junction with `NetworkId`!", + T::BridgedNetwork::get() + ); + assert!( + T::CongestionLimits::get().is_valid(), + "Configured `T::CongestionLimits`: {:?} are not valid!", + T::CongestionLimits::get() + ); + } + + #[cfg(feature = "try-runtime")] + fn try_state(_n: BlockNumberFor) -> Result<(), sp_runtime::TryRuntimeError> { + Self::do_try_state() + } + } + + #[pallet::call] + impl, I: 'static> Pallet { + /// Open a bridge between two locations. + /// + /// The caller must be within the `T::OpenBridgeOrigin` filter (presumably: a sibling + /// parachain or a parent relay chain). The `bridge_destination_universal_location` must be + /// a destination within the consensus of the `T::BridgedNetwork` network. + /// + /// The `BridgeDeposit` amount is reserved on the caller account. This deposit + /// is unreserved after bridge is closed. + /// + /// The states after this call: bridge is `Opened`, outbound lane is `Opened`, inbound lane + /// is `Opened`. + /// + /// Optional `maybe_notify` holds data about the `bridge_origin_relative_location` where + /// notifications can be sent to handle congestion. The notification contains an encoded + /// tuple `(BridgeId, bool)`, where `bool` is the `is_congested` flag. + #[pallet::call_index(0)] + #[pallet::weight(WeightInfoOf::::open_bridge())] + pub fn open_bridge( + origin: OriginFor, + bridge_destination_universal_location: Box, + maybe_notify: Option, + ) -> DispatchResult { + // check and compute required bridge locations and laneId + let xcm_version = bridge_destination_universal_location.identify_version(); + let locations = + Self::bridge_locations_from_origin(origin, bridge_destination_universal_location)?; + let lane_id = locations.calculate_lane_id(xcm_version).map_err(|e| { + log::trace!( + target: LOG_TARGET, + "calculate_lane_id error: {e:?}", + ); + Error::::BridgeLocations(e) + })?; + + Self::do_open_bridge(locations, lane_id, true, maybe_notify) + } + + /// 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(WeightInfoOf::::close_bridge())] + pub fn close_bridge( + origin: OriginFor, + bridge_destination_universal_location: Box, + may_prune_messages: MessageNonce, + ) -> DispatchResult { + // compute required bridge locations + let locations = + Self::bridge_locations_from_origin(origin, bridge_destination_universal_location)?; + + // TODO: https://github.com/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.bridge_id(), |bridge| match bridge { + Some(bridge) => { + bridge.state = BridgeState::Closed; + Ok(bridge.clone()) + }, + None => Err(Error::::UnknownBridge), + })?; + + // close inbound and outbound lanes + let lanes_manager = LanesManagerOf::::new(); + let mut inbound_lane = lanes_manager + .any_state_inbound_lane(bridge.lane_id) + .map_err(Error::::LanesManager)?; + let mut outbound_lane = lanes_manager + .any_state_outbound_lane(bridge.lane_id) + .map_err(Error::::LanesManager)?; + + // now prune queued messages + let mut pruned_messages = 0; + for _ in outbound_lane.queued_messages() { + if pruned_messages == may_prune_messages { + break + } + + outbound_lane.remove_oldest_unpruned_message(); + pruned_messages += 1; + } + + // if there are outbound messages in the queue, just update states and early exit + if !outbound_lane.queued_messages().is_empty() { + // update lanes state. Under normal circumstances, following calls shall never fail + inbound_lane.set_state(LaneState::Closed); + outbound_lane.set_state(LaneState::Closed); + + // write something to log + let enqueued_messages = outbound_lane.queued_messages().saturating_len(); + log::trace!( + target: LOG_TARGET, + "Bridge {:?} between {:?} and {:?} is closing lane_id: {:?}. {} messages remaining", + locations.bridge_id(), + locations.bridge_origin_universal_location(), + locations.bridge_destination_universal_location(), + bridge.lane_id, + enqueued_messages, + ); + + // deposit the `ClosingBridge` event + Self::deposit_event(Event::::ClosingBridge { + bridge_id: *locations.bridge_id(), + lane_id: bridge.lane_id.into(), + pruned_messages, + enqueued_messages, + }); + + return Ok(()) + } + + // else we have pruned all messages, so lanes and the bridge itself may gone + inbound_lane.purge(); + outbound_lane.purge(); + Bridges::::remove(locations.bridge_id()); + LaneToBridge::::remove(bridge.lane_id); + + // return deposit + let released_deposit = if let Some(deposit) = bridge.deposit { + T::Currency::release( + &HoldReason::BridgeDeposit.into(), + &deposit.account, + deposit.amount, + Precision::BestEffort, + ) + .inspect_err(|e| { + // we can't do anything here - looks like funds have been (partially) unreserved + // before by someone else. Let's not fail, though - it'll be worse for the + // caller + log::error!( + target: LOG_TARGET, + "Failed to unreserve during the bridge {:?} closure with error: {e:?}", + locations.bridge_id(), + ); + }) + .map(|released| Deposit::new(deposit.account, released)) + .ok() + } else { + None + }; + + // write something to log + log::trace!( + target: LOG_TARGET, + "Bridge {:?} between {:?} and {:?} has closed lane_id: {:?}, the bridge deposit {released_deposit:?} was returned", + locations.bridge_id(), + bridge.lane_id, + locations.bridge_origin_universal_location(), + locations.bridge_destination_universal_location(), + ); + + // deposit the `BridgePruned` event + Self::deposit_event(Event::::BridgePruned { + bridge_id: *locations.bridge_id(), + lane_id: bridge.lane_id.into(), + bridge_deposit: released_deposit, + pruned_messages, + }); + + Ok(()) + } + + /// Attempts to update the `maybe_notify` callback for receiving congestion notifications. + /// + /// This can only be called by the "owner" of this side of the bridge, which means that the + /// inbound XCM channel with the local origin chain is functional. + #[pallet::call_index(2)] + #[pallet::weight(WeightInfoOf::::update_notification_receiver())] + pub fn update_notification_receiver( + origin: OriginFor, + bridge_destination_universal_location: Box, + maybe_notify: Option, + ) -> DispatchResult { + let locations = + Self::bridge_locations_from_origin(origin, bridge_destination_universal_location)?; + Bridges::::try_mutate_exists(locations.bridge_id(), |bridge| match bridge { + Some(b) => { + b.maybe_notify = maybe_notify; + + Self::deposit_event(Event::::NotificationReceiverUpdated { + bridge_id: *locations.bridge_id(), + maybe_notify: b.maybe_notify.clone(), + }); + Ok(()) + }, + None => Err(Error::::UnknownBridge.into()), + }) + } + } + + impl, I: 'static> Pallet { + /// Open bridge for lane. + pub fn do_open_bridge( + locations: Box, + lane_id: T::LaneId, + create_lanes: bool, + maybe_notify: Option, + ) -> Result<(), DispatchError> { + // reserve balance (if needed) + let deposit = if T::AllowWithoutBridgeDeposit::contains( + locations.bridge_origin_relative_location(), + ) { + None + } else { + // get origin's sovereign account + let bridge_owner_account = T::BridgeOriginAccountIdConverter::convert_location( + locations.bridge_origin_relative_location(), + ) + .ok_or(Error::::InvalidBridgeOriginAccount)?; + + let deposit = T::BridgeDeposit::get(); + T::Currency::hold( + &HoldReason::BridgeDeposit.into(), + &bridge_owner_account, + deposit, + ) + .map_err(|e| { + log::error!( + target: LOG_TARGET, + "Failed to hold bridge deposit: {deposit:?} \ + from bridge_owner_account: {bridge_owner_account:?} derived from \ + bridge_origin_relative_location: {:?} with error: {e:?}", + locations.bridge_origin_relative_location(), + ); + Error::::FailedToReserveBridgeDeposit + })?; + Some(Deposit::new(bridge_owner_account, deposit)) + }; + + // save bridge metadata + Bridges::::try_mutate(locations.bridge_id(), |bridge| match bridge { + Some(_) => Err(Error::::BridgeAlreadyExists), + None => { + *bridge = Some(BridgeOf:: { + bridge_origin_relative_location: Box::new( + locations.bridge_origin_relative_location().clone().into(), + ), + bridge_origin_universal_location: Box::new( + locations.bridge_origin_universal_location().clone().into(), + ), + bridge_destination_universal_location: Box::new( + locations.bridge_destination_universal_location().clone().into(), + ), + state: BridgeState::Opened, + deposit: deposit.clone(), + lane_id, + maybe_notify, + }); + Ok(()) + }, + })?; + // save lane to bridge mapping + LaneToBridge::::try_mutate(lane_id, |bridge| match bridge { + Some(_) => Err(Error::::BridgeAlreadyExists), + None => { + *bridge = Some(*locations.bridge_id()); + Ok(()) + }, + })?; + + if create_lanes { + // create new lanes. Under normal circumstances, following calls shall never fail + let lanes_manager = LanesManagerOf::::new(); + lanes_manager + .create_inbound_lane(lane_id) + .map_err(Error::::LanesManager)?; + lanes_manager + .create_outbound_lane(lane_id) + .map_err(Error::::LanesManager)?; + } + + // write something to log + log::trace!( + target: LOG_TARGET, + "Bridge {:?} between {:?} and {:?} has been opened using lane_id: {lane_id:?}", + locations.bridge_id(), + locations.bridge_origin_universal_location(), + locations.bridge_destination_universal_location(), + ); + + // deposit `BridgeOpened` event + Self::deposit_event(Event::::BridgeOpened { + bridge_id: *locations.bridge_id(), + bridge_deposit: deposit, + local_endpoint: Box::new(locations.bridge_origin_universal_location().clone()), + remote_endpoint: Box::new( + locations.bridge_destination_universal_location().clone(), + ), + lane_id: lane_id.into(), + }); + + Ok(()) + } + } + + impl, I: 'static> Pallet { + /// Return bridge endpoint locations and dedicated lane identifier. This method converts + /// runtime `origin` argument to relative `Location` using the `T::OpenBridgeOrigin` + /// converter. + pub fn bridge_locations_from_origin( + origin: OriginFor, + bridge_destination_universal_location: Box, + ) -> Result, sp_runtime::DispatchError> { + Self::bridge_locations( + T::OpenBridgeOrigin::ensure_origin(origin)?, + (*bridge_destination_universal_location) + .try_into() + .map_err(|_| Error::::UnsupportedXcmVersion)?, + ) + } + + /// Return bridge endpoint locations and dedicated **bridge** identifier (`BridgeId`). + pub fn bridge_locations( + bridge_origin_relative_location: Location, + bridge_destination_universal_location: InteriorLocation, + ) -> Result, sp_runtime::DispatchError> { + BridgeLocations::bridge_locations( + T::UniversalLocation::get(), + bridge_origin_relative_location, + bridge_destination_universal_location, + Self::bridged_network_id()?, + ) + .map_err(|e| { + log::trace!( + target: LOG_TARGET, + "bridge_locations error: {e:?}", + ); + Error::::BridgeLocations(e).into() + }) + } + + /// Return bridge metadata by bridge_id + pub fn bridge(bridge_id: &BridgeId) -> Option> { + Bridges::::get(bridge_id) + } + + /// Return bridge metadata by lane_id + pub fn bridge_by_lane_id(lane_id: &T::LaneId) -> Option<(BridgeId, BridgeOf)> { + LaneToBridge::::get(lane_id) + .and_then(|bridge_id| Self::bridge(&bridge_id).map(|bridge| (bridge_id, bridge))) + } + } + + impl, I: 'static> Pallet { + /// Returns some `NetworkId` if contains `GlobalConsensus` junction. + pub fn bridged_network_id() -> Result { + match T::BridgedNetwork::get().take_first_interior() { + Some(GlobalConsensus(network)) => Ok(network), + _ => Err(Error::::BridgeLocations( + BridgeLocationsError::InvalidBridgeDestination, + ) + .into()), + } + } + } + + #[cfg(feature = "runtime-benchmarks")] + impl, I: 'static> Pallet { + /// Open bridge for lane. + pub fn open_bridge_for_benchmarks( + lane_id: LaneIdOf, + bridge_origin_relative_location: Location, + bridge_destination_universal_location: InteriorLocation, + create_lanes: bool, + maybe_notify: Option, + initial_balance: impl Fn() -> BalanceOf>, + ) -> Result, sp_runtime::DispatchError> { + let locations = Pallet::::bridge_locations( + bridge_origin_relative_location.into(), + bridge_destination_universal_location.into(), + )?; + + if !T::AllowWithoutBridgeDeposit::contains(locations.bridge_origin_relative_location()) + { + let account_id = T::BridgeOriginAccountIdConverter::convert_location( + locations.bridge_origin_relative_location(), + ) + .ok_or(Error::::InvalidBridgeOriginAccount)?; + + use frame_support::traits::fungible::Unbalanced; + use sp_runtime::Saturating; + T::Currency::increase_balance( + &account_id, + initial_balance().saturating_add(T::BridgeDeposit::get()), + Precision::BestEffort, + )?; + } + + Pallet::::do_open_bridge(locations.clone(), lane_id, create_lanes, maybe_notify)?; + + Ok(locations) + } + } + + #[cfg(any(test, feature = "try-runtime", feature = "std"))] + impl, I: 'static> Pallet { + /// Ensure the correctness of the state of this pallet. + pub fn do_try_state() -> Result<(), sp_runtime::TryRuntimeError> { + use sp_std::collections::btree_set::BTreeSet; + + let mut lanes = BTreeSet::new(); + + // check all known bridge configurations + for (bridge_id, bridge) in Bridges::::iter() { + lanes.insert(Self::do_try_state_for_bridge(bridge_id, bridge)?); + } + ensure!( + lanes.len() == Bridges::::iter().count(), + "Invalid `Bridges` configuration, probably two bridges handle the same laneId!" + ); + ensure!( + lanes.len() == LaneToBridge::::iter().count(), + "Invalid `LaneToBridge` configuration, probably missing or not removed laneId!" + ); + + // check connected `pallet_bridge_messages` state. + Self::do_try_state_for_messages() + } + + /// Ensure the correctness of the state of the bridge. + pub fn do_try_state_for_bridge( + bridge_id: BridgeId, + bridge: BridgeOf, + ) -> Result { + log::info!(target: LOG_TARGET, "Checking `do_try_state_for_bridge` for bridge_id: {bridge_id:?} and bridge: {bridge:?}"); + + // check `BridgeId` points to the same `LaneId` and vice versa. + ensure!( + Some(bridge_id) == LaneToBridge::::get(bridge.lane_id), + "Found `LaneToBridge` inconsistency for bridge_id - missing mapping!" + ); + + // check `pallet_bridge_messages` state for that `LaneId`. + let lanes_manager = LanesManagerOf::::new(); + ensure!( + lanes_manager.any_state_inbound_lane(bridge.lane_id).is_ok(), + "Inbound lane not found!", + ); + ensure!( + lanes_manager.any_state_outbound_lane(bridge.lane_id).is_ok(), + "Outbound lane not found!", + ); + + // check that `locations` are convertible to the `latest` XCM. + let bridge_origin_relative_location_as_latest: &Location = + &(*bridge.bridge_origin_relative_location).try_into().map_err(|_| { + "`bridge.bridge_origin_relative_location` cannot be converted to the `latest` XCM, needs migration!" + })?; + let bridge_origin_universal_location_as_latest: &InteriorLocation = + &(*bridge.bridge_origin_universal_location).try_into().map_err(|_| { + "`bridge.bridge_origin_universal_location` cannot be converted to the `latest` XCM, needs migration!" + })?; + let bridge_destination_universal_location_as_latest: &InteriorLocation = + &(*bridge.bridge_destination_universal_location).try_into().map_err(|_| { + "`bridge.bridge_destination_universal_location` cannot be converted to the `latest` XCM, needs migration!" + })?; + + // check `BridgeId` does not change + ensure!( + bridge_id == BridgeId::new(bridge_origin_universal_location_as_latest, bridge_destination_universal_location_as_latest), + "`bridge_id` is different than calculated from `bridge_origin_universal_location_as_latest` and `bridge_destination_universal_location_as_latest`, needs migration!" + ); + + // check bridge account owner + if let Some(deposit) = bridge.deposit { + ensure!( + T::BridgeOriginAccountIdConverter::convert_location(bridge_origin_relative_location_as_latest) == Some(deposit.account), + "`bridge.deposit.account` is different than calculated from `bridge.bridge_origin_relative_location`, needs migration!" + ); + } + + Ok(bridge.lane_id) + } + + /// Ensure the correctness of the state of the connected `pallet_bridge_messages` instance. + pub fn do_try_state_for_messages() -> Result<(), sp_runtime::TryRuntimeError> { + // check that all `InboundLanes` laneIds have mapping to some bridge. + for lane_id in pallet_bridge_messages::InboundLanes::::iter_keys() { + log::info!(target: LOG_TARGET, "Checking `do_try_state_for_messages` for `InboundLanes`'s lane_id: {lane_id:?}..."); + ensure!( + LaneToBridge::::get(lane_id).is_some(), + "Found `LaneToBridge` inconsistency for `InboundLanes`'s lane_id - missing mapping!" + ); + } + + // check that all `OutboundLanes` laneIds have mapping to some bridge. + for lane_id in pallet_bridge_messages::OutboundLanes::::iter_keys() { + log::info!(target: LOG_TARGET, "Checking `do_try_state_for_messages` for `OutboundLanes`'s lane_id: {lane_id:?}..."); + ensure!( + LaneToBridge::::get(lane_id).is_some(), + "Found `LaneToBridge` inconsistency for `OutboundLanes`'s lane_id - missing mapping!" + ); + } + + Ok(()) + } + } + + /// All registered bridges. + #[pallet::storage] + pub type Bridges, I: 'static = ()> = + StorageMap<_, Identity, BridgeId, BridgeOf>; + /// All registered `lane_id` and `bridge_id` mappings. + #[pallet::storage] + pub type LaneToBridge, I: 'static = ()> = + StorageMap<_, Identity, T::LaneId, BridgeId>; + + #[pallet::genesis_config] + #[derive(DefaultNoBound)] + pub struct GenesisConfig, I: 'static = ()> { + /// Opened bridges. + /// + /// Keep in mind that we are **NOT** reserving any amount for the bridges opened at + /// genesis. We are **NOT** opening lanes, used by this bridge. It all must be done using + /// other pallets genesis configuration or some other means. + pub opened_bridges: Vec<(Location, InteriorLocation, Option, Option)>, + /// Dummy marker. + #[serde(skip)] + pub _phantom: sp_std::marker::PhantomData<(T, I)>, + } + + #[pallet::genesis_build] + impl, I: 'static> BuildGenesisConfig for GenesisConfig + where + T: frame_system::Config>>, + { + fn build(&self) { + for ( + bridge_origin_relative_location, + bridge_destination_universal_location, + maybe_lane_id, + maybe_notify, + ) in &self.opened_bridges + { + let locations = Pallet::::bridge_locations( + bridge_origin_relative_location.clone(), + bridge_destination_universal_location.clone().into(), + ) + .expect("Invalid genesis configuration"); + + let lane_id = match maybe_lane_id { + Some(lane_id) => *lane_id, + None => + locations.calculate_lane_id(xcm::latest::VERSION).expect("Valid locations"), + }; + + Pallet::::do_open_bridge(locations, lane_id, true, maybe_notify.clone()) + .expect("Valid opened bridge!"); + } + } + } + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event, I: 'static = ()> { + /// The bridge between two locations has been opened. + BridgeOpened { + /// Bridge identifier. + bridge_id: BridgeId, + /// Bridge deposit held. + bridge_deposit: Option>>, + + /// Universal location of local bridge endpoint. + local_endpoint: Box, + /// Universal location of remote bridge endpoint. + remote_endpoint: Box, + /// Lane identifier. + lane_id: T::LaneId, + }, + /// Bridge is going to be closed, but not yet fully pruned from the runtime storage. + ClosingBridge { + /// Bridge identifier. + bridge_id: BridgeId, + /// Lane identifier. + lane_id: T::LaneId, + /// Number of pruned messages during the close call. + pruned_messages: MessageNonce, + /// Number of enqueued messages that need to be pruned in follow up calls. + enqueued_messages: MessageNonce, + }, + /// Bridge has been closed and pruned from the runtime storage. It now may be reopened + /// again by any participant. + BridgePruned { + /// Bridge identifier. + bridge_id: BridgeId, + /// Lane identifier. + lane_id: T::LaneId, + /// Amount of deposit released. + bridge_deposit: Option>>, + /// Number of pruned messages during the close call. + pruned_messages: MessageNonce, + }, + /// The bridge has been updated with a new notification receiver. + NotificationReceiverUpdated { + /// Bridge identifier. + bridge_id: BridgeId, + /// Updated notification receiver. + maybe_notify: Option, + }, + } + + #[pallet::error] + pub enum Error { + /// Bridge locations error. + BridgeLocations(BridgeLocationsError), + /// Invalid local bridge origin account. + InvalidBridgeOriginAccount, + /// The bridge is already registered in this pallet. + BridgeAlreadyExists, + /// The local origin already owns a maximal number of bridges. + TooManyBridgesForLocalOrigin, + /// Trying to close already closed bridge. + BridgeAlreadyClosed, + /// Lanes manager error. + LanesManager(LanesManagerError), + /// Trying to access unknown bridge. + UnknownBridge, + /// The bridge origin can't pay the required amount for opening the bridge. + FailedToReserveBridgeDeposit, + /// The version of XCM location argument is unsupported. + UnsupportedXcmVersion, + } +} diff --git a/bridges/modules/xcm-bridge/src/migration.rs b/bridges/modules/xcm-bridge/src/migration.rs new file mode 100644 index 0000000000000..a486befd6f820 --- /dev/null +++ b/bridges/modules/xcm-bridge/src/migration.rs @@ -0,0 +1,157 @@ +// Copyright (C) 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 . + +//! A module that is responsible for migration of storage. + +use crate::{Config, Pallet, Receiver, LOG_TARGET}; +use frame_support::{ + traits::{Get, OnRuntimeUpgrade}, + weights::Weight, +}; +use xcm::prelude::{InteriorLocation, Location}; + +/// This migration does not modify storage but can be used to open a bridge and link it to the +/// specified LaneId. This is useful when we want to open a bridge and use a custom LaneId instead +/// of the pre-calculated one provided by the `fn open_bridge extrinsic`. +/// Or perhaps if you want to ensure that your runtime (e.g., for testing) always has an open +/// bridge. +pub struct OpenBridgeForLane< + T, + I, + Lane, + CreateLane, + SourceRelativeLocation, + BridgedUniversalLocation, + MaybeNotifyRelativeLocation, +>( + core::marker::PhantomData<( + T, + I, + Lane, + CreateLane, + SourceRelativeLocation, + BridgedUniversalLocation, + MaybeNotifyRelativeLocation, + )>, +); +impl< + T: Config, + I: 'static, + Lane: Get, + CreateLane: Get, + SourceRelativeLocation: Get, + BridgedUniversalLocation: Get, + MaybeNotifyRelativeLocation: Get>, + > OnRuntimeUpgrade + for OpenBridgeForLane< + T, + I, + Lane, + CreateLane, + SourceRelativeLocation, + BridgedUniversalLocation, + MaybeNotifyRelativeLocation, + > +{ + fn on_runtime_upgrade() -> Weight { + let bridge_origin_relative_location = SourceRelativeLocation::get(); + let bridge_destination_universal_location = BridgedUniversalLocation::get(); + let lane_id = Lane::get(); + let create_lane = CreateLane::get(); + let maybe_notify = MaybeNotifyRelativeLocation::get(); + log::info!( + target: LOG_TARGET, + "OpenBridgeForLane - going to open bridge with lane_id: {lane_id:?} (create_lane: {create_lane:?}) \ + between bridge_origin_relative_location: {bridge_origin_relative_location:?} and \ + bridge_destination_universal_location: {bridge_destination_universal_location:?} \ + maybe_notify: {maybe_notify:?}", + ); + + let locations = match Pallet::::bridge_locations( + bridge_origin_relative_location.clone(), + bridge_destination_universal_location.clone(), + ) { + Ok(locations) => locations, + Err(e) => { + log::error!( + target: LOG_TARGET, + "OpenBridgeForLane - on_runtime_upgrade failed to construct bridge_locations with error: {e:?}" + ); + return T::DbWeight::get().reads(0) + }, + }; + + // check if already exists + if let Some((bridge_id, bridge)) = Pallet::::bridge_by_lane_id(&lane_id) { + log::info!( + target: LOG_TARGET, + "OpenBridgeForLane - bridge: {bridge:?} with bridge_id: {bridge_id:?} already exist for lane_id: {lane_id:?}!" + ); + if &bridge_id != locations.bridge_id() { + log::warn!( + target: LOG_TARGET, + "OpenBridgeForLane - check you parameters, because a different bridge: {bridge:?} \ + with bridge_id: {bridge_id:?} exist for lane_id: {lane_id:?} for requested \ + bridge_origin_relative_location: {bridge_origin_relative_location:?} and \ + bridge_destination_universal_location: {bridge_destination_universal_location:?} !", + ); + } + + return T::DbWeight::get().reads(2) + } + + if let Err(e) = + Pallet::::do_open_bridge(locations, lane_id, create_lane, maybe_notify) + { + log::error!(target: LOG_TARGET, "OpenBridgeForLane - do_open_bridge failed with error: {e:?}"); + T::DbWeight::get().reads(6) + } else { + log::info!(target: LOG_TARGET, "OpenBridgeForLane - do_open_bridge passed!"); + T::DbWeight::get().reads_writes(6, 4) + } + } + + #[cfg(feature = "try-runtime")] + fn post_upgrade(_state: sp_std::vec::Vec) -> Result<(), sp_runtime::DispatchError> { + let bridge_origin_relative_location = SourceRelativeLocation::get(); + let bridge_destination_universal_location = BridgedUniversalLocation::get(); + let lane_id = Lane::get(); + + // check that requested bridge is stored + let Ok(locations) = Pallet::::bridge_locations( + bridge_origin_relative_location.clone(), + bridge_destination_universal_location.clone(), + ) else { + return Err(sp_runtime::DispatchError::Other("Invalid locations!")) + }; + let Some((bridge_id, _)) = Pallet::::bridge_by_lane_id(&lane_id) else { + return Err(sp_runtime::DispatchError::Other("Missing bridge!")) + }; + frame_support::ensure!( + locations.bridge_id() == &bridge_id, + "Bridge is not stored correctly!" + ); + + log::info!( + target: LOG_TARGET, + "OpenBridgeForLane - post_upgrade found opened bridge with lane_id: {lane_id:?} \ + between bridge_origin_relative_location: {bridge_origin_relative_location:?} and \ + bridge_destination_universal_location: {bridge_destination_universal_location:?}", + ); + + Ok(()) + } +} diff --git a/bridges/modules/xcm-bridge/src/mock.rs b/bridges/modules/xcm-bridge/src/mock.rs new file mode 100644 index 0000000000000..1534f9ad1fad5 --- /dev/null +++ b/bridges/modules/xcm-bridge/src/mock.rs @@ -0,0 +1,734 @@ +// 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; +use bp_messages::{ + target_chain::{DispatchMessage, MessageDispatch}, + ChainWithMessages, HashedLaneId, MessageNonce, +}; +use bp_runtime::{messages::MessageDispatchResult, Chain, ChainId, HashOf}; +use bp_xcm_bridge::{BridgeId, BridgeLocations, LocalXcmChannelManager}; +use codec::Encode; +use frame_support::{ + assert_ok, derive_impl, parameter_types, + traits::{fungible::Mutate, EitherOf, EnsureOrigin, Equals, Everything, Get, OriginTrait}, + weights::RuntimeDbWeight, +}; +use frame_system::{EnsureNever, EnsureRoot, EnsureRootWithSuccess}; +use pallet_xcm_bridge::congestion::{ + BlobDispatcherWithChannelStatus, CongestionLimits, HereOrLocalConsensusXcmChannelManager, + UpdateBridgeStatusXcmChannelManager, +}; +use polkadot_parachain_primitives::primitives::Sibling; +use polkadot_runtime_common::xcm_sender::NoPriceForMessageDelivery; +use sp_core::H256; +use sp_runtime::{ + testing::Header as SubstrateHeader, + traits::{BlakeTwo256, ConstU32, Convert, IdentityLookup}, + AccountId32, BuildStorage, StateVersion, +}; +use sp_std::{cell::RefCell, marker::PhantomData}; +use xcm::{latest::ROCOCO_GENESIS_HASH, prelude::*}; +use xcm_builder::{ + AllowUnpaidExecutionFrom, DispatchBlob, DispatchBlobError, FixedWeightBounds, + InspectMessageQueues, LocalExporter, NetworkExportTable, NetworkExportTableItem, + ParentIsPreset, SiblingParachainConvertsVia, SovereignPaidRemoteExporter, +}; +use xcm_executor::{ + traits::{ConvertLocation, ConvertOrigin}, + XcmExecutor, +}; + +pub type AccountId = AccountId32; +pub type Balance = u64; +type Block = frame_system::mocking::MockBlock; + +/// Lane identifier type used for tests. +pub type TestLaneIdType = HashedLaneId; + +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 { + System: frame_system, + Balances: pallet_balances, + Messages: pallet_bridge_messages, + XcmOverBridge: pallet_xcm_bridge, + XcmOverBridgeWrappedWithExportMessageRouter: pallet_xcm_bridge_router = 57, + XcmOverBridgeByExportXcmRouter: pallet_xcm_bridge_router:: = 69, + } +} + +parameter_types! { + pub const DbWeight: RuntimeDbWeight = RuntimeDbWeight { read: 1, write: 2 }; + pub const ExistentialDeposit: Balance = 1; +} + +#[derive_impl(frame_system::config_preludes::TestDefaultConfig)] +impl frame_system::Config for TestRuntime { + type AccountId = AccountId; + type AccountData = pallet_balances::AccountData; + type Block = Block; + type Lookup = IdentityLookup; +} + +#[derive_impl(pallet_balances::config_preludes::TestDefaultConfig)] +impl pallet_balances::Config for TestRuntime { + type AccountStore = System; +} + +impl pallet_bridge_messages::Config for TestRuntime { + type RuntimeEvent = RuntimeEvent; + type WeightInfo = TestMessagesWeights; + + type ThisChain = ThisUnderlyingChain; + type BridgedChain = BridgedUnderlyingChain; + type BridgedHeaderChain = BridgedHeaderChain; + + type OutboundPayload = Vec; + type InboundPayload = Vec; + type LaneId = TestLaneIdType; + + type DeliveryPayments = (); + type DeliveryConfirmationPayments = (); + type OnMessagesDelivered = (); + + type MessageDispatch = TestMessageDispatch; +} + +pub struct TestMessagesWeights; + +impl pallet_bridge_messages::WeightInfo for TestMessagesWeights { + fn receive_single_message_proof() -> Weight { + Weight::zero() + } + fn receive_n_messages_proof(_: u32) -> Weight { + Weight::zero() + } + fn receive_single_message_proof_with_outbound_lane_state() -> Weight { + Weight::zero() + } + fn receive_single_n_bytes_message_proof(_: u32) -> Weight { + Weight::zero() + } + fn receive_delivery_proof_for_single_message() -> Weight { + Weight::zero() + } + fn receive_delivery_proof_for_two_messages_by_single_relayer() -> Weight { + Weight::zero() + } + fn receive_delivery_proof_for_two_messages_by_two_relayers() -> Weight { + Weight::zero() + } + fn receive_single_n_bytes_message_proof_with_dispatch(_n: u32) -> Weight { + Weight::from_parts(1, 0) + } +} + +impl pallet_bridge_messages::WeightInfoExt for TestMessagesWeights { + fn expected_extra_storage_proof_size() -> u32 { + 0 + } + + fn receive_messages_proof_overhead_from_runtime() -> Weight { + Weight::zero() + } + + fn receive_messages_delivery_proof_overhead_from_runtime() -> Weight { + Weight::zero() + } +} + +parameter_types! { + pub const HereLocation: Location = Location::here(); + pub const RelayNetwork: NetworkId = NetworkId::Kusama; + pub UniversalLocation: InteriorLocation = [ + GlobalConsensus(RelayNetwork::get()), + Parachain(THIS_BRIDGE_HUB_ID), + ].into(); + pub SiblingLocation: Location = Location::new(1, [Parachain(SIBLING_ASSET_HUB_ID)]); + pub SiblingUniversalLocation: InteriorLocation = [GlobalConsensus(RelayNetwork::get()), Parachain(SIBLING_ASSET_HUB_ID)].into(); + + pub const BridgedRelayNetwork: NetworkId = NetworkId::ByGenesis([1; 32]); + pub BridgedRelayNetworkLocation: Location = (Parent, GlobalConsensus(BridgedRelayNetwork::get())).into(); + pub BridgedRelativeDestination: InteriorLocation = [Parachain(BRIDGED_ASSET_HUB_ID)].into(); + pub BridgedUniversalDestination: InteriorLocation = [GlobalConsensus(BridgedRelayNetwork::get()), Parachain(BRIDGED_ASSET_HUB_ID)].into(); + pub const NonBridgedRelayNetwork: NetworkId = NetworkId::ByGenesis(ROCOCO_GENESIS_HASH); + + pub const BridgeDeposit: Balance = 100_000; + + // configuration for pallet_xcm_bridge_hub_router + pub BridgeHubLocation: Location = Here.into(); + pub BridgeTable: Vec + = vec![ + NetworkExportTableItem::new( + BridgedRelayNetwork::get(), + None, + BridgeHubLocation::get(), + None + ) + ]; + pub UnitWeightCost: Weight = Weight::from_parts(10, 10); + pub storage TestCongestionLimits: CongestionLimits = CongestionLimits::default(); +} + +/// **Universal** `InteriorLocation` of bridged asset hub. +pub fn bridged_asset_hub_universal_location() -> InteriorLocation { + BridgedUniversalDestination::get() +} + +pub(crate) type TestLocalXcmChannelManager = TestingLocalXcmChannelManager< + BridgeId, + HereOrLocalConsensusXcmChannelManager< + BridgeId, + // handles congestion for `XcmOverBridgeByExportXcmRouter` + XcmOverBridgeByExportXcmRouter, + // handles congestion for `XcmOverBridgeWrappedWithExportMessageRouter` + UpdateBridgeStatusXcmChannelManager< + TestRuntime, + (), + UpdateBridgeStatusXcmProvider, + FromBridgeHubLocationXcmSender, + >, + >, +>; + +impl pallet_xcm_bridge::Config for TestRuntime { + type WeightInfo = (); + + type UniversalLocation = UniversalLocation; + type BridgedNetwork = BridgedRelayNetworkLocation; + type BridgeMessagesPalletInstance = (); + + type MessageExportPrice = NoPriceForMessageDelivery; + type DestinationVersion = AlwaysLatest; + + type ForceOrigin = EnsureNever<()>; + type OpenBridgeOrigin = EitherOf< + // We want to translate `RuntimeOrigin::root()` to the `Location::here()` + EnsureRootWithSuccess, + OpenBridgeOrigin, + >; + type BridgeOriginAccountIdConverter = LocationToAccountId; + + type BridgeDeposit = BridgeDeposit; + type Currency = Balances; + type RuntimeHoldReason = RuntimeHoldReason; + type AllowWithoutBridgeDeposit = (Equals, Equals); + + type LocalXcmChannelManager = TestLocalXcmChannelManager; + type BlobDispatcher = BlobDispatcherWithChannelStatus; + type CongestionLimits = TestCongestionLimits; +} + +#[cfg(feature = "runtime-benchmarks")] +impl crate::benchmarking::Config<()> for TestRuntime { + fn open_bridge_origin() -> Option<(RuntimeOrigin, Balance)> { + Some((OpenBridgeOrigin::sibling_parachain_origin(), ExistentialDeposit::get())) + } +} + +/// A router instance simulates a scenario where the router is deployed on a different chain than +/// the `MessageExporter`. This means that the router sends an `ExportMessage`. +pub type XcmOverBridgeWrappedWithExportMessageRouterInstance = (); +#[derive_impl(pallet_xcm_bridge_router::config_preludes::TestDefaultConfig)] +impl pallet_xcm_bridge_router::Config + for TestRuntime +{ + // We use `SovereignPaidRemoteExporter` here to test and ensure that the `ExportMessage` + // produced by `pallet_xcm_bridge_hub_router` is compatible with the `ExportXcm` implementation + // of `pallet_xcm_bridge_hub`. + type MessageExporter = SovereignPaidRemoteExporter< + pallet_xcm_bridge_router::impls::ViaRemoteBridgeExporter< + TestRuntime, + // () - means `pallet_xcm_bridge_router::Config<()>` + (), + NetworkExportTable, + BridgedRelayNetwork, + BridgeHubLocation, + >, + // **Note**: The crucial part is that `ExportMessage` is processed by `XcmExecutor`, which + // calls the `ExportXcm` implementation of `pallet_xcm_bridge_hub` as the + // `MessageExporter`. + ExecuteXcmOverSendXcm, + ExportMessageOriginUniversalLocation, + >; + + type BridgeIdResolver = pallet_xcm_bridge_router::impls::EnsureIsRemoteBridgeIdResolver< + ExportMessageOriginUniversalLocation, + >; + // We convert to root here `BridgeHubLocationXcmOriginAsRoot` + type UpdateBridgeStatusOrigin = EnsureRoot; +} + +/// A router instance simulates a scenario where the router is deployed on the same chain as the +/// `MessageExporter`. This means that the router triggers `ExportXcm` trait directly. +pub type XcmOverBridgeByExportXcmRouterInstance = pallet_xcm_bridge_router::Instance2; +#[derive_impl(pallet_xcm_bridge_router::config_preludes::TestDefaultConfig)] +impl pallet_xcm_bridge_router::Config for TestRuntime { + // We use `LocalExporter` with `ViaLocalBridgeExporter` here to test and ensure that + // `pallet_xcm_bridge_hub_router` can trigger directly `pallet_xcm_bridge_hub` as exporter. + type MessageExporter = pallet_xcm_bridge_router::impls::ViaLocalBridgeExporter< + TestRuntime, + XcmOverBridgeByExportXcmRouterInstance, + LocalExporter, + >; + + type BridgeIdResolver = + pallet_xcm_bridge_router::impls::EnsureIsRemoteBridgeIdResolver; + // We don't need to support here `update_bridge_status`. + type UpdateBridgeStatusOrigin = EnsureNever<()>; +} + +/// A dynamic way to set different universal location for the origin which sends `ExportMessage`. +pub struct ExportMessageOriginUniversalLocation; +impl ExportMessageOriginUniversalLocation { + pub(crate) fn set(universal_location: Option) { + EXPORT_MESSAGE_ORIGIN_UNIVERSAL_LOCATION.with(|o| *o.borrow_mut() = universal_location); + } +} +impl Get for ExportMessageOriginUniversalLocation { + fn get() -> InteriorLocation { + EXPORT_MESSAGE_ORIGIN_UNIVERSAL_LOCATION.with(|o| { + o.borrow() + .clone() + .expect("`EXPORT_MESSAGE_ORIGIN_UNIVERSAL_LOCATION` is not set!") + }) + } +} +thread_local! { + pub static EXPORT_MESSAGE_ORIGIN_UNIVERSAL_LOCATION: RefCell> = RefCell::new(None); +} + +pub struct BridgeHubLocationXcmOriginAsRoot(PhantomData); +impl ConvertOrigin + for BridgeHubLocationXcmOriginAsRoot +{ + fn convert_origin( + origin: impl Into, + kind: OriginKind, + ) -> Result { + let origin = origin.into(); + if kind == OriginKind::Xcm && origin.eq(&BridgeHubLocation::get()) { + Ok(RuntimeOrigin::root()) + } else { + Err(origin) + } + } +} + +pub struct XcmConfig; +impl xcm_executor::Config for XcmConfig { + type RuntimeCall = RuntimeCall; + type XcmSender = (); + type XcmEventEmitter = (); + type AssetTransactor = (); + type OriginConverter = BridgeHubLocationXcmOriginAsRoot; + type IsReserve = (); + type IsTeleporter = (); + type UniversalLocation = UniversalLocation; + type Barrier = AllowUnpaidExecutionFrom; + type Weigher = FixedWeightBounds>; + type Trader = (); + type ResponseHandler = (); + type AssetTrap = (); + type AssetClaims = (); + type SubscriptionService = (); + type PalletInstancesInfo = (); + type MaxAssetsIntoHolding = (); + type AssetLocker = (); + type AssetExchanger = (); + type FeeManager = (); + // We just set `MessageExporter` as our `pallet_xcm_bridge_hub` instance. + type MessageExporter = (XcmOverBridge,); + type UniversalAliases = (); + type CallDispatcher = RuntimeCall; + type SafeCallFilter = Everything; + type Aliasers = (); + type TransactionalProcessor = (); + type HrmpNewChannelOpenRequestHandler = (); + type HrmpChannelAcceptedHandler = (); + type HrmpChannelClosingHandler = (); + type XcmRecorder = (); +} + +thread_local! { + pub static EXECUTE_XCM_ORIGIN: RefCell> = RefCell::new(None); +} + +/// The `SendXcm` implementation directly executes XCM using `XcmExecutor`. +pub struct ExecuteXcmOverSendXcm; +impl SendXcm for ExecuteXcmOverSendXcm { + type Ticket = Xcm<()>; + + fn validate( + _: &mut Option, + message: &mut Option>, + ) -> SendResult { + Ok((message.take().unwrap(), Assets::new())) + } + + fn deliver(ticket: Self::Ticket) -> Result { + let xcm: Xcm = ticket.into(); + + let origin = EXECUTE_XCM_ORIGIN.with(|o| o.borrow().clone().unwrap()); + let mut hash = xcm.using_encoded(sp_io::hashing::blake2_256); + let outcome = XcmExecutor::::prepare_and_execute( + origin, + xcm, + &mut hash, + Weight::MAX, + Weight::zero(), + ); + assert_ok!(outcome.ensure_complete()); + + Ok(hash) + } +} +impl InspectMessageQueues for ExecuteXcmOverSendXcm { + fn clear_messages() { + todo!() + } + + fn get_messages() -> Vec<(VersionedLocation, Vec>)> { + todo!() + } +} +impl ExecuteXcmOverSendXcm { + pub fn set_origin_for_execute(origin: Location) { + EXECUTE_XCM_ORIGIN.with(|o| *o.borrow_mut() = Some(origin)); + } +} + +/// 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, +); + +parameter_types! { + pub ParentRelayChainLocation: Location = Location { parents: 1, interior: Here }; +} +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(ParentRelayChainLocation::get()) + } 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(), + }); + } else if signer == Self::sibling_parachain_origin().into_signer() { + return Ok(SiblingLocation::get()); + } + + Err(o) + } + + #[cfg(feature = "runtime-benchmarks")] + fn try_successful_origin() -> Result { + Ok(Self::parent_relay_chain_origin()) + } +} + +pub(crate) type OpenBridgeOriginOf = >::OpenBridgeOrigin; + +pub(crate) fn fund_origin_sovereign_account( + locations: &BridgeLocations, + balance: Balance, +) -> AccountId { + let bridge_owner_account = + LocationToAccountId::convert_location(locations.bridge_origin_relative_location()).unwrap(); + assert_ok!(Balances::mint_into(&bridge_owner_account, balance)); + bridge_owner_account +} + +/// Testing wrapper implementation of `LocalXcmChannelManager`, that supports storing flags in +/// storage to facilitate testing of `LocalXcmChannelManager` implementation. +pub struct TestingLocalXcmChannelManager(PhantomData<(Bridge, Tested)>); + +impl TestingLocalXcmChannelManager { + fn suspended_key(bridge: &Bridge) -> Vec { + [b"TestingLocalXcmChannelManager.Suspended", bridge.encode().as_slice()].concat() + } + fn resumed_key(bridge: &Bridge) -> Vec { + [b"TestingLocalXcmChannelManager.Resumed", bridge.encode().as_slice()].concat() + } + + pub fn is_bridge_suspened(bridge: &Bridge) -> bool { + frame_support::storage::unhashed::get_or_default(&Self::suspended_key(bridge)) + } + + pub fn is_bridge_resumed(bridge: &Bridge) -> bool { + frame_support::storage::unhashed::get_or_default(&Self::resumed_key(bridge)) + } +} + +impl> + LocalXcmChannelManager for TestingLocalXcmChannelManager +{ + type Error = Tested::Error; + + fn suspend_bridge(local_origin: &Location, bridge: Bridge) -> Result<(), Self::Error> { + let result = Tested::suspend_bridge(local_origin, bridge); + + if result.is_ok() { + frame_support::storage::unhashed::put(&Self::suspended_key(&bridge), &true); + } + + result + } + + fn resume_bridge(local_origin: &Location, bridge: Bridge) -> Result<(), Self::Error> { + let result = Tested::resume_bridge(local_origin, bridge); + + if result.is_ok() { + frame_support::storage::unhashed::put(&Self::resumed_key(&bridge), &true); + } + + result + } +} + +/// Converts encoded call to the unpaid XCM `Transact`. +pub struct UpdateBridgeStatusXcmProvider; +impl Convert, Xcm<()>> for UpdateBridgeStatusXcmProvider { + fn convert(encoded_call: Vec) -> Xcm<()> { + Xcm(vec![ + UnpaidExecution { weight_limit: Unlimited, check_origin: None }, + Transact { + origin_kind: OriginKind::Xcm, + fallback_max_weight: Some(Weight::from_parts(200_000_000, 6144)), + call: encoded_call.into(), + }, + ExpectTransactStatus(MaybeErrorCode::Success), + ]) + } +} + +/// `SendXcm` implementation which sets `BridgeHubLocation` as origin for `ExecuteXcmOverSendXcm`. +pub struct FromBridgeHubLocationXcmSender(PhantomData); +impl SendXcm for FromBridgeHubLocationXcmSender { + type Ticket = Inner::Ticket; + + fn validate( + destination: &mut Option, + message: &mut Option>, + ) -> SendResult { + Inner::validate(destination, message) + } + + fn deliver(ticket: Self::Ticket) -> Result { + ExecuteXcmOverSendXcm::set_origin_for_execute(BridgeHubLocation::get()); + Inner::deliver(ticket) + } +} + +pub struct TestBlobDispatcher; + +impl TestBlobDispatcher { + pub fn is_dispatched() -> bool { + frame_support::storage::unhashed::get_or_default(b"TestBlobDispatcher.Dispatched") + } + + fn congestion_key(with: &Location) -> Vec { + [b"TestBlobDispatcher.Congested.", with.encode().as_slice()].concat() + } + + pub fn make_congested(with: &Location) { + frame_support::storage::unhashed::put(&Self::congestion_key(with), &true); + } +} + +impl pallet_xcm_bridge::DispatchChannelStatusProvider for TestBlobDispatcher { + fn is_congested(with: &Location) -> bool { + frame_support::storage::unhashed::get_or_default(&Self::congestion_key(with)) + } +} + +impl DispatchBlob for TestBlobDispatcher { + fn dispatch_blob(_blob: Vec) -> Result<(), DispatchBlobError> { + frame_support::storage::unhashed::put(b"TestBlobDispatcher.Dispatched", &true); + Ok(()) + } +} + +pub struct ThisUnderlyingChain; + +impl Chain for ThisUnderlyingChain { + const ID: ChainId = *b"tuch"; + + type BlockNumber = u64; + type Hash = H256; + type Hasher = BlakeTwo256; + type Header = SubstrateHeader; + type AccountId = AccountId; + type Balance = Balance; + type Nonce = u64; + type Signature = sp_runtime::MultiSignature; + + const STATE_VERSION: StateVersion = StateVersion::V1; + + fn max_extrinsic_size() -> u32 { + u32::MAX + } + + fn max_extrinsic_weight() -> Weight { + Weight::MAX + } +} + +impl ChainWithMessages for ThisUnderlyingChain { + 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 type BridgedHeaderHash = H256; +pub type BridgedChainHeader = SubstrateHeader; + +pub struct BridgedUnderlyingChain; +impl Chain for BridgedUnderlyingChain { + const ID: ChainId = *b"bgdc"; + type BlockNumber = u64; + type Hash = BridgedHeaderHash; + type Hasher = BlakeTwo256; + type Header = BridgedChainHeader; + type AccountId = AccountId; + type Balance = Balance; + type Nonce = u64; + type Signature = sp_runtime::MultiSignature; + + const STATE_VERSION: StateVersion = StateVersion::V1; + + fn max_extrinsic_size() -> u32 { + 4096 + } + + fn max_extrinsic_weight() -> Weight { + Weight::MAX + } +} + +impl ChainWithMessages for BridgedUnderlyingChain { + 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; +} + +pub struct BridgedHeaderChain; +impl bp_header_chain::HeaderChain for BridgedHeaderChain { + fn finalized_header_state_root( + _hash: HashOf, + ) -> Option> { + unreachable!() + } +} + +/// Test message dispatcher. +pub struct TestMessageDispatch; + +impl TestMessageDispatch { + pub fn deactivate(lane: TestLaneIdType) { + frame_support::storage::unhashed::put(&(b"inactive", lane).encode()[..], &false); + } +} + +impl MessageDispatch for TestMessageDispatch { + type DispatchPayload = Vec; + type DispatchLevelResult = (); + type LaneId = TestLaneIdType; + + fn is_active(lane: Self::LaneId) -> bool { + frame_support::storage::unhashed::take::(&(b"inactive", lane).encode()[..]) != + Some(false) + } + + fn dispatch_weight( + _message: &mut DispatchMessage, + ) -> Weight { + Weight::zero() + } + + fn dispatch( + _: DispatchMessage, + ) -> MessageDispatchResult { + MessageDispatchResult { unspent_weight: Weight::zero(), dispatch_level_result: () } + } +} + +/// Return test externalities to use in tests. +pub fn new_test_ext() -> sp_io::TestExternalities { + let t = frame_system::GenesisConfig::::default().build_storage().unwrap(); + sp_io::TestExternalities::new(t) +} + +/// Run pallet test. +pub fn run_test(test: impl FnOnce() -> T) -> T { + new_test_ext().execute_with(test) +} diff --git a/bridges/modules/xcm-bridge/src/tests.rs b/bridges/modules/xcm-bridge/src/tests.rs new file mode 100644 index 0000000000000..6db9fec32b71f --- /dev/null +++ b/bridges/modules/xcm-bridge/src/tests.rs @@ -0,0 +1,957 @@ +// 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 super::*; +use bp_messages::LaneIdType; +use mock::*; + +use frame_support::{assert_err, assert_noop, assert_ok, BoundedVec}; +use frame_system::{EventRecord, Phase}; +use sp_runtime::{traits::Zero, TryRuntimeError}; + +fn mock_open_bridge_from_with( + origin: RuntimeOrigin, + deposit: Option, + with: InteriorLocation, +) -> (BridgeOf, BridgeLocations) { + let locations = + XcmOverBridge::bridge_locations_from_origin(origin, Box::new(with.into())).unwrap(); + let lane_id = locations.calculate_lane_id(xcm::latest::VERSION).unwrap(); + + let deposit = deposit.map(|deposit| { + let bridge_owner_account = + fund_origin_sovereign_account(&locations, deposit + ExistentialDeposit::get()); + Balances::hold(&HoldReason::BridgeDeposit.into(), &bridge_owner_account, deposit).unwrap(); + Deposit::new(bridge_owner_account, deposit) + }); + + let bridge = Bridge { + bridge_origin_relative_location: Box::new( + locations.bridge_origin_relative_location().clone().into(), + ), + bridge_origin_universal_location: Box::new( + locations.bridge_origin_universal_location().clone().into(), + ), + bridge_destination_universal_location: Box::new( + locations.bridge_destination_universal_location().clone().into(), + ), + state: BridgeState::Opened, + deposit, + lane_id, + maybe_notify: None, + }; + Bridges::::insert(locations.bridge_id(), bridge.clone()); + LaneToBridge::::insert(bridge.lane_id, locations.bridge_id()); + + let lanes_manager = LanesManagerOf::::new(); + lanes_manager.create_inbound_lane(bridge.lane_id).unwrap(); + lanes_manager.create_outbound_lane(bridge.lane_id).unwrap(); + + assert_ok!(XcmOverBridge::do_try_state()); + + (bridge, *locations) +} + +fn mock_open_bridge_from( + origin: RuntimeOrigin, + deposit: Option, +) -> (BridgeOf, BridgeLocations) { + mock_open_bridge_from_with(origin, deposit, bridged_asset_hub_universal_location()) +} + +fn enqueue_message(lane: TestLaneIdType) { + let lanes_manager = LanesManagerOf::::new(); + lanes_manager + .active_outbound_lane(lane) + .unwrap() + .send_message(BoundedVec::try_from(vec![42]).expect("We craft valid messages")); +} + +#[test] +fn open_bridge_fails_if_origin_is_not_allowed() { + run_test(|| { + assert_noop!( + XcmOverBridge::open_bridge( + OpenBridgeOrigin::disallowed_origin(), + Box::new(bridged_asset_hub_universal_location().into()), + None, + ), + 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_universal_location().into()), + None, + ), + Error::::BridgeLocations(BridgeLocationsError::InvalidBridgeOrigin), + ); + + assert_noop!( + XcmOverBridge::open_bridge( + OpenBridgeOrigin::sibling_parachain_universal_origin(), + Box::new(bridged_asset_hub_universal_location().into()), + None, + ), + 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() + ), + None, + ), + 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() + ), + None, + ), + Error::::BridgeLocations(BridgeLocationsError::UnreachableDestination), + ); + }); +} + +#[test] +fn open_bridge_fails_if_origin_has_no_sovereign_account() { + run_test(|| { + assert_noop!( + XcmOverBridge::open_bridge( + OpenBridgeOrigin::origin_without_sovereign_account(), + Box::new(bridged_asset_hub_universal_location().into()), + None, + ), + Error::::InvalidBridgeOriginAccount, + ); + }); +} + +#[test] +fn open_bridge_fails_if_origin_sovereign_account_has_no_enough_funds() { + run_test(|| { + assert_noop!( + XcmOverBridge::open_bridge( + OpenBridgeOrigin::sibling_parachain_origin(), + Box::new(bridged_asset_hub_universal_location().into()), + None, + ), + Error::::FailedToReserveBridgeDeposit, + ); + }); +} + +#[test] +fn open_bridge_fails_if_it_already_exists() { + run_test(|| { + let origin = OpenBridgeOrigin::parent_relay_chain_origin(); + let locations = XcmOverBridge::bridge_locations_from_origin( + origin.clone(), + Box::new(bridged_asset_hub_universal_location().into()), + ) + .unwrap(); + let lane_id = locations.calculate_lane_id(xcm::latest::VERSION).unwrap(); + + Bridges::::insert( + locations.bridge_id(), + Bridge { + bridge_origin_relative_location: Box::new( + locations.bridge_origin_relative_location().clone().into(), + ), + bridge_origin_universal_location: Box::new( + locations.bridge_origin_universal_location().clone().into(), + ), + bridge_destination_universal_location: Box::new( + locations.bridge_destination_universal_location().clone().into(), + ), + state: BridgeState::Opened, + deposit: None, + lane_id, + maybe_notify: None, + }, + ); + + assert_noop!( + XcmOverBridge::open_bridge( + origin, + Box::new(bridged_asset_hub_universal_location().into()), + None, + ), + Error::::BridgeAlreadyExists, + ); + }) +} + +#[test] +fn open_bridge_fails_if_its_lanes_already_exists() { + run_test(|| { + let origin = OpenBridgeOrigin::parent_relay_chain_origin(); + let locations = XcmOverBridge::bridge_locations_from_origin( + origin.clone(), + Box::new(bridged_asset_hub_universal_location().into()), + ) + .unwrap(); + let lane_id = locations.calculate_lane_id(xcm::latest::VERSION).unwrap(); + fund_origin_sovereign_account(&locations, BridgeDeposit::get() + ExistentialDeposit::get()); + + let lanes_manager = LanesManagerOf::::new(); + + lanes_manager.create_inbound_lane(lane_id).unwrap(); + assert_noop!( + XcmOverBridge::open_bridge( + origin.clone(), + Box::new(bridged_asset_hub_universal_location().into()), + None, + ), + Error::::LanesManager(LanesManagerError::InboundLaneAlreadyExists), + ); + + lanes_manager.active_inbound_lane(lane_id).unwrap().purge(); + lanes_manager.create_outbound_lane(lane_id).unwrap(); + assert_noop!( + XcmOverBridge::open_bridge( + origin, + Box::new(bridged_asset_hub_universal_location().into()), + None, + ), + 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, + // any sibling parachain or local root + let origins = [ + (OpenBridgeOrigin::parent_relay_chain_origin(), None), + (OpenBridgeOrigin::sibling_parachain_origin(), Some(BridgeDeposit::get())), + (RuntimeOrigin::root(), None), + ]; + + // check that every origin may open the bridge + let lanes_manager = LanesManagerOf::::new(); + let existential_deposit = ExistentialDeposit::get(); + for (origin, expected_deposit_amount) in origins { + // reset events + System::set_block_number(1); + System::reset_events(); + + // compute all other locations + let xcm_version = xcm::latest::VERSION; + let locations = XcmOverBridge::bridge_locations_from_origin( + origin.clone(), + Box::new( + VersionedInteriorLocation::from(bridged_asset_hub_universal_location()) + .into_version(xcm_version) + .expect("valid conversion"), + ), + ) + .unwrap(); + let lane_id = locations.calculate_lane_id(xcm_version).unwrap(); + + // ensure that there's no bridge and lanes in the storage + assert_eq!(Bridges::::get(locations.bridge_id()), None); + assert_eq!( + lanes_manager.active_inbound_lane(lane_id).map(drop), + Err(LanesManagerError::UnknownInboundLane) + ); + assert_eq!( + lanes_manager.active_outbound_lane(lane_id).map(drop), + Err(LanesManagerError::UnknownOutboundLane) + ); + assert_eq!(LaneToBridge::::get(lane_id), None); + + // give enough funds to the sovereign account of the bridge origin + let expected_deposit = expected_deposit_amount.map(|deposit_amount| { + let bridge_owner_account = + fund_origin_sovereign_account(&locations, deposit_amount + existential_deposit); + assert_eq!( + Balances::free_balance(&bridge_owner_account), + deposit_amount + existential_deposit + ); + assert_eq!(Balances::reserved_balance(&bridge_owner_account), 0); + Deposit::new(bridge_owner_account, deposit_amount) + }); + + let maybe_notify = Some(Receiver::new(13, 15)); + + // now open the bridge + assert_ok!(XcmOverBridge::open_bridge( + origin, + Box::new(locations.bridge_destination_universal_location().clone().into()), + maybe_notify.clone(), + )); + + // ensure that everything has been set up in the runtime storage + assert_eq!( + Bridges::::get(locations.bridge_id()), + Some(Bridge { + bridge_origin_relative_location: Box::new( + locations.bridge_origin_relative_location().clone().into() + ), + bridge_origin_universal_location: Box::new( + locations.bridge_origin_universal_location().clone().into(), + ), + bridge_destination_universal_location: Box::new( + locations.bridge_destination_universal_location().clone().into(), + ), + state: BridgeState::Opened, + deposit: expected_deposit.clone(), + lane_id, + maybe_notify, + }), + ); + assert_eq!( + lanes_manager.active_inbound_lane(lane_id).map(|l| l.state()), + Ok(LaneState::Opened) + ); + assert_eq!( + lanes_manager.active_outbound_lane(lane_id).map(|l| l.state()), + Ok(LaneState::Opened) + ); + assert_eq!(LaneToBridge::::get(lane_id), Some(*locations.bridge_id())); + if let Some(expected_deposit) = expected_deposit.as_ref() { + assert_eq!(Balances::free_balance(&expected_deposit.account), existential_deposit); + assert_eq!( + Balances::reserved_balance(&expected_deposit.account), + expected_deposit.amount + ); + } + + // ensure that the proper event is deposited + assert_eq!( + System::events().last(), + Some(&EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::XcmOverBridge(Event::BridgeOpened { + bridge_id: *locations.bridge_id(), + bridge_deposit: expected_deposit, + local_endpoint: Box::new( + locations.bridge_origin_universal_location().clone() + ), + remote_endpoint: Box::new( + locations.bridge_destination_universal_location().clone() + ), + lane_id: lane_id.into() + }), + topics: vec![], + }), + ); + + // check state + assert_ok!(XcmOverBridge::do_try_state()); + } + }); +} + +#[test] +fn close_bridge_fails_if_origin_is_not_allowed() { + run_test(|| { + assert_noop!( + XcmOverBridge::close_bridge( + OpenBridgeOrigin::disallowed_origin(), + Box::new(bridged_asset_hub_universal_location().into()), + 0, + ), + 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_universal_location().into()), + 0, + ), + Error::::BridgeLocations(BridgeLocationsError::InvalidBridgeOrigin), + ); + + assert_noop!( + XcmOverBridge::close_bridge( + OpenBridgeOrigin::sibling_parachain_universal_origin(), + Box::new(bridged_asset_hub_universal_location().into()), + 0, + ), + Error::::BridgeLocations(BridgeLocationsError::InvalidBridgeOrigin), + ); + }) +} + +#[test] +fn close_bridge_fails_if_its_lanes_are_unknown() { + run_test(|| { + let origin = OpenBridgeOrigin::parent_relay_chain_origin(); + let (bridge, locations) = mock_open_bridge_from(origin.clone(), None); + + let lanes_manager = LanesManagerOf::::new(); + lanes_manager.any_state_inbound_lane(bridge.lane_id).unwrap().purge(); + assert_noop!( + XcmOverBridge::close_bridge( + origin.clone(), + Box::new(locations.bridge_destination_universal_location().clone().into()), + 0, + ), + Error::::LanesManager(LanesManagerError::UnknownInboundLane), + ); + lanes_manager.any_state_outbound_lane(bridge.lane_id).unwrap().purge(); + + let (_, locations) = mock_open_bridge_from(origin.clone(), None); + lanes_manager.any_state_outbound_lane(bridge.lane_id).unwrap().purge(); + assert_noop!( + XcmOverBridge::close_bridge( + origin, + Box::new(locations.bridge_destination_universal_location().clone().into()), + 0, + ), + Error::::LanesManager(LanesManagerError::UnknownOutboundLane), + ); + }); +} + +#[test] +fn close_bridge_works() { + run_test(|| { + let origin = OpenBridgeOrigin::parent_relay_chain_origin(); + let expected_deposit = BridgeDeposit::get(); + let (bridge, locations) = mock_open_bridge_from(origin.clone(), Some(expected_deposit)); + System::set_block_number(1); + let bridge_owner_account = bridge.deposit.unwrap().account; + + // remember owner balances + let free_balance = Balances::free_balance(&bridge_owner_account); + let reserved_balance = Balances::reserved_balance(&bridge_owner_account); + + // enqueue some messages + for _ in 0..32 { + enqueue_message(bridge.lane_id); + } + + // now call the `close_bridge`, which will only partially prune messages + assert_ok!(XcmOverBridge::close_bridge( + origin.clone(), + Box::new(locations.bridge_destination_universal_location().clone().into()), + 16, + ),); + + // as a result, the bridge and lanes are switched to the `Closed` state, some messages + // are pruned, but funds are not unreserved + let lanes_manager = LanesManagerOf::::new(); + assert_eq!( + Bridges::::get(locations.bridge_id()).map(|b| b.state), + Some(BridgeState::Closed) + ); + assert_eq!( + lanes_manager.any_state_inbound_lane(bridge.lane_id).unwrap().state(), + LaneState::Closed + ); + assert_eq!( + lanes_manager.any_state_outbound_lane(bridge.lane_id).unwrap().state(), + LaneState::Closed + ); + assert_eq!( + lanes_manager + .any_state_outbound_lane(bridge.lane_id) + .unwrap() + .queued_messages() + .checked_len(), + Some(16) + ); + assert_eq!( + LaneToBridge::::get(bridge.lane_id), + Some(*locations.bridge_id()) + ); + assert_eq!(Balances::free_balance(&bridge_owner_account), free_balance); + assert_eq!(Balances::reserved_balance(&bridge_owner_account), reserved_balance); + assert_eq!( + System::events().last(), + Some(&EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::XcmOverBridge(Event::ClosingBridge { + bridge_id: *locations.bridge_id(), + lane_id: bridge.lane_id.into(), + pruned_messages: 16, + enqueued_messages: 16, + }), + topics: vec![], + }), + ); + + // now call the `close_bridge` again, which will only partially prune messages + assert_ok!(XcmOverBridge::close_bridge( + origin.clone(), + Box::new(locations.bridge_destination_universal_location().clone().into()), + 8, + ),); + + // nothing is changed (apart from the pruned messages) + assert_eq!( + Bridges::::get(locations.bridge_id()).map(|b| b.state), + Some(BridgeState::Closed) + ); + assert_eq!( + lanes_manager.any_state_inbound_lane(bridge.lane_id).unwrap().state(), + LaneState::Closed + ); + assert_eq!( + lanes_manager.any_state_outbound_lane(bridge.lane_id).unwrap().state(), + LaneState::Closed + ); + assert_eq!( + lanes_manager + .any_state_outbound_lane(bridge.lane_id) + .unwrap() + .queued_messages() + .checked_len(), + Some(8) + ); + assert_eq!( + LaneToBridge::::get(bridge.lane_id), + Some(*locations.bridge_id()) + ); + assert_eq!(Balances::free_balance(&bridge_owner_account), free_balance); + assert_eq!(Balances::reserved_balance(&bridge_owner_account), reserved_balance); + assert_eq!( + System::events().last(), + Some(&EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::XcmOverBridge(Event::ClosingBridge { + bridge_id: *locations.bridge_id(), + lane_id: bridge.lane_id.into(), + pruned_messages: 8, + enqueued_messages: 8, + }), + topics: vec![], + }), + ); + + // now call the `close_bridge` again that will prune all remaining messages and the + // bridge + assert_ok!(XcmOverBridge::close_bridge( + origin, + Box::new(locations.bridge_destination_universal_location().clone().into()), + 9, + ),); + + // there's no traces of bridge in the runtime storage and funds are unreserved + assert_eq!(Bridges::::get(locations.bridge_id()).map(|b| b.state), None); + assert_eq!( + lanes_manager.any_state_inbound_lane(bridge.lane_id).map(drop), + Err(LanesManagerError::UnknownInboundLane) + ); + assert_eq!( + lanes_manager.any_state_outbound_lane(bridge.lane_id).map(drop), + Err(LanesManagerError::UnknownOutboundLane) + ); + assert_eq!(LaneToBridge::::get(bridge.lane_id), None); + assert_eq!(Balances::free_balance(&bridge_owner_account), free_balance + reserved_balance); + assert_eq!(Balances::reserved_balance(&bridge_owner_account), 0); + assert_eq!( + System::events().last(), + Some(&EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::XcmOverBridge(Event::BridgePruned { + bridge_id: *locations.bridge_id(), + lane_id: bridge.lane_id.into(), + bridge_deposit: Some(Deposit::new(bridge_owner_account, expected_deposit)), + pruned_messages: 8, + }), + topics: vec![], + }), + ); + }); +} + +#[test] +fn update_notification_receiver_works() { + run_test(|| { + let origin = OpenBridgeOrigin::parent_relay_chain_origin(); + let locations = XcmOverBridge::bridge_locations_from_origin( + origin.clone(), + Box::new(VersionedInteriorLocation::from(bridged_asset_hub_universal_location())), + ) + .unwrap(); + + // open the bridge + assert_ok!(XcmOverBridge::open_bridge( + origin.clone(), + Box::new(locations.bridge_destination_universal_location().clone().into()), + Some(Receiver::new(13, 15)), + )); + assert_eq!( + Bridges::::get(locations.bridge_id()) + .map(|b| b.maybe_notify) + .unwrap(), + Some(Receiver::new(13, 15)) + ); + + // update the notification receiver to `None` + assert_ok!(XcmOverBridge::update_notification_receiver( + origin.clone(), + Box::new(locations.bridge_destination_universal_location().clone().into()), + None, + )); + assert_eq!( + Bridges::::get(locations.bridge_id()) + .map(|b| b.maybe_notify) + .unwrap(), + None, + ); + + // update the notification receiver to `Some(..)` + assert_ok!(XcmOverBridge::update_notification_receiver( + origin.clone(), + Box::new(locations.bridge_destination_universal_location().clone().into()), + Some(Receiver::new(29, 43)), + )); + assert_eq!( + Bridges::::get(locations.bridge_id()) + .map(|b| b.maybe_notify) + .unwrap(), + Some(Receiver::new(29, 43)) + ); + // update the notification receiver to `Some(..)` + assert_ok!(XcmOverBridge::update_notification_receiver( + origin.clone(), + Box::new(locations.bridge_destination_universal_location().clone().into()), + Some(Receiver::new(29, 79)), + )); + assert_eq!( + Bridges::::get(locations.bridge_id()) + .map(|b| b.maybe_notify) + .unwrap(), + Some(Receiver::new(29, 79)) + ); + }); +} + +#[test] +fn do_try_state_works() { + let bridge_origin_relative_location = SiblingLocation::get(); + let bridge_origin_universal_location = SiblingUniversalLocation::get(); + let bridge_destination_universal_location = BridgedUniversalDestination::get(); + let bridge_owner_account = + LocationToAccountId::convert_location(&bridge_origin_relative_location) + .expect("valid accountId"); + let bridge_owner_account_mismatch = + LocationToAccountId::convert_location(&Location::parent()).expect("valid accountId"); + let bridge_id = + BridgeId::new(&bridge_origin_universal_location, &bridge_destination_universal_location); + let bridge_id_mismatch = BridgeId::new(&InteriorLocation::Here, &InteriorLocation::Here); + let lane_id = TestLaneIdType::try_new(1, 2).unwrap(); + let lane_id_mismatch = TestLaneIdType::try_new(3, 4).unwrap(); + + let test_bridge_state = |id, + bridge, + (lane_id, bridge_id), + (inbound_lane_id, outbound_lane_id), + expected_error: Option| { + Bridges::::insert(id, bridge); + LaneToBridge::::insert(lane_id, bridge_id); + + let lanes_manager = LanesManagerOf::::new(); + lanes_manager.create_inbound_lane(inbound_lane_id).unwrap(); + lanes_manager.create_outbound_lane(outbound_lane_id).unwrap(); + + let result = XcmOverBridge::do_try_state(); + if let Some(e) = expected_error { + assert_err!(result, e); + } else { + assert_ok!(result); + } + }; + let cleanup = |bridge_id, lane_ids| { + Bridges::::remove(bridge_id); + for lane_id in lane_ids { + LaneToBridge::::remove(lane_id); + let lanes_manager = LanesManagerOf::::new(); + if let Ok(lane) = lanes_manager.any_state_inbound_lane(lane_id) { + lane.purge(); + } + if let Ok(lane) = lanes_manager.any_state_outbound_lane(lane_id) { + lane.purge(); + } + } + assert_ok!(XcmOverBridge::do_try_state()); + }; + + run_test(|| { + // ok state + test_bridge_state( + bridge_id, + Bridge { + bridge_origin_relative_location: Box::new(VersionedLocation::from( + bridge_origin_relative_location.clone(), + )), + bridge_origin_universal_location: Box::new(VersionedInteriorLocation::from( + bridge_origin_universal_location.clone(), + )), + bridge_destination_universal_location: Box::new(VersionedInteriorLocation::from( + bridge_destination_universal_location.clone(), + )), + state: BridgeState::Opened, + deposit: Some(Deposit::new(bridge_owner_account.clone(), Zero::zero())), + lane_id, + maybe_notify: None, + }, + (lane_id, bridge_id), + (lane_id, lane_id), + None, + ); + cleanup(bridge_id, vec![lane_id]); + + // error - missing `LaneToBridge` mapping + test_bridge_state( + bridge_id, + Bridge { + bridge_origin_relative_location: Box::new(VersionedLocation::from( + bridge_origin_relative_location.clone(), + )), + bridge_origin_universal_location: Box::new(VersionedInteriorLocation::from( + bridge_origin_universal_location.clone(), + )), + bridge_destination_universal_location: Box::new(VersionedInteriorLocation::from( + bridge_destination_universal_location.clone(), + )), + state: BridgeState::Opened, + deposit: Some(Deposit::new(bridge_owner_account.clone(), Zero::zero())), + lane_id, + maybe_notify: None, + }, + (lane_id, bridge_id_mismatch), + (lane_id, lane_id), + Some(TryRuntimeError::Other( + "Found `LaneToBridge` inconsistency for bridge_id - missing mapping!", + )), + ); + cleanup(bridge_id, vec![lane_id]); + + // error bridge owner account cannot be calculated + test_bridge_state( + bridge_id, + Bridge { + bridge_origin_relative_location: Box::new(VersionedLocation::from( + bridge_origin_relative_location.clone(), + )), + bridge_origin_universal_location: Box::new(VersionedInteriorLocation::from( + bridge_origin_universal_location.clone(), + )), + bridge_destination_universal_location: Box::new(VersionedInteriorLocation::from( + bridge_destination_universal_location.clone(), + )), + state: BridgeState::Opened, + deposit: Some(Deposit::new(bridge_owner_account_mismatch.clone(), Zero::zero())), + lane_id, + maybe_notify: None, + }, + (lane_id, bridge_id), + (lane_id, lane_id), + Some(TryRuntimeError::Other("`bridge.deposit.account` is different than calculated from `bridge.bridge_origin_relative_location`, needs migration!")), + ); + cleanup(bridge_id, vec![lane_id]); + + // error when (bridge_origin_universal_location + bridge_destination_universal_location) + // produces different `BridgeId` + test_bridge_state( + bridge_id_mismatch, + Bridge { + bridge_origin_relative_location: Box::new(VersionedLocation::from( + bridge_origin_relative_location.clone(), + )), + bridge_origin_universal_location: Box::new(VersionedInteriorLocation::from( + bridge_origin_universal_location.clone(), + )), + bridge_destination_universal_location: Box::new(VersionedInteriorLocation::from( + bridge_destination_universal_location.clone(), + )), + state: BridgeState::Opened, + deposit: Some(Deposit::new(bridge_owner_account_mismatch.clone(), Zero::zero())), + lane_id, + maybe_notify: None, + }, + (lane_id, bridge_id_mismatch), + (lane_id, lane_id), + Some(TryRuntimeError::Other("`bridge_id` is different than calculated from `bridge_origin_universal_location_as_latest` and `bridge_destination_universal_location_as_latest`, needs migration!")), + ); + cleanup(bridge_id_mismatch, vec![lane_id]); + + // missing inbound lane for a bridge + test_bridge_state( + bridge_id, + Bridge { + bridge_origin_relative_location: Box::new(VersionedLocation::from( + bridge_origin_relative_location.clone(), + )), + bridge_origin_universal_location: Box::new(VersionedInteriorLocation::from( + bridge_origin_universal_location.clone(), + )), + bridge_destination_universal_location: Box::new(VersionedInteriorLocation::from( + bridge_destination_universal_location.clone(), + )), + state: BridgeState::Opened, + deposit: Some(Deposit::new(bridge_owner_account.clone(), Zero::zero())), + lane_id, + maybe_notify: None, + }, + (lane_id, bridge_id), + (lane_id_mismatch, lane_id), + Some(TryRuntimeError::Other("Inbound lane not found!")), + ); + cleanup(bridge_id, vec![lane_id, lane_id_mismatch]); + + // missing outbound lane for a bridge + test_bridge_state( + bridge_id, + Bridge { + bridge_origin_relative_location: Box::new(VersionedLocation::from( + bridge_origin_relative_location.clone(), + )), + bridge_origin_universal_location: Box::new(VersionedInteriorLocation::from( + bridge_origin_universal_location.clone(), + )), + bridge_destination_universal_location: Box::new(VersionedInteriorLocation::from( + bridge_destination_universal_location.clone(), + )), + state: BridgeState::Opened, + deposit: Some(Deposit::new(bridge_owner_account.clone(), Zero::zero())), + lane_id, + maybe_notify: None, + }, + (lane_id, bridge_id), + (lane_id, lane_id_mismatch), + Some(TryRuntimeError::Other("Outbound lane not found!")), + ); + cleanup(bridge_id, vec![lane_id, lane_id_mismatch]); + + // ok state with old XCM version + test_bridge_state( + bridge_id, + Bridge { + bridge_origin_relative_location: Box::new( + VersionedLocation::from(bridge_origin_relative_location.clone()) + .into_version(XCM_VERSION - 1) + .unwrap(), + ), + bridge_origin_universal_location: Box::new( + VersionedInteriorLocation::from(bridge_origin_universal_location.clone()) + .into_version(XCM_VERSION - 1) + .unwrap(), + ), + bridge_destination_universal_location: Box::new( + VersionedInteriorLocation::from(bridge_destination_universal_location.clone()) + .into_version(XCM_VERSION - 1) + .unwrap(), + ), + state: BridgeState::Opened, + deposit: Some(Deposit::new(bridge_owner_account, Zero::zero())), + lane_id, + maybe_notify: None, + }, + (lane_id, bridge_id), + (lane_id, lane_id), + None, + ); + cleanup(bridge_id, vec![lane_id]); + + // missing bridge for inbound lane + let lanes_manager = LanesManagerOf::::new(); + assert!(lanes_manager.create_inbound_lane(lane_id).is_ok()); + assert_err!(XcmOverBridge::do_try_state(), TryRuntimeError::Other("Found `LaneToBridge` inconsistency for `InboundLanes`'s lane_id - missing mapping!")); + cleanup(bridge_id, vec![lane_id]); + + // missing bridge for outbound lane + let lanes_manager = LanesManagerOf::::new(); + assert!(lanes_manager.create_outbound_lane(lane_id).is_ok()); + assert_err!(XcmOverBridge::do_try_state(), TryRuntimeError::Other("Found `LaneToBridge` inconsistency for `OutboundLanes`'s lane_id - missing mapping!")); + cleanup(bridge_id, vec![lane_id]); + }); +} + +#[test] +fn ensure_encoding_compatibility() { + use codec::Encode; + + let bridge_destination_universal_location = BridgedUniversalDestination::get(); + let may_prune_messages = 13; + let receiver = Receiver::new(13, 15); + + assert_eq!( + bp_xcm_bridge::XcmBridgeCall::open_bridge { + bridge_destination_universal_location: Box::new( + bridge_destination_universal_location.clone().into() + ), + maybe_notify: Some(receiver.clone()), + } + .encode(), + Call::::open_bridge { + bridge_destination_universal_location: Box::new( + bridge_destination_universal_location.clone().into() + ), + maybe_notify: Some(receiver), + } + .encode() + ); + assert_eq!( + bp_xcm_bridge::XcmBridgeCall::close_bridge { + bridge_destination_universal_location: Box::new( + bridge_destination_universal_location.clone().into() + ), + may_prune_messages, + } + .encode(), + Call::::close_bridge { + bridge_destination_universal_location: Box::new( + bridge_destination_universal_location.clone().into() + ), + may_prune_messages, + } + .encode() + ); +} diff --git a/bridges/modules/xcm-bridge/src/weights.rs b/bridges/modules/xcm-bridge/src/weights.rs new file mode 100644 index 0000000000000..c0bab87ef7272 --- /dev/null +++ b/bridges/modules/xcm-bridge/src/weights.rs @@ -0,0 +1,90 @@ +// Copyright (C) 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 . + +//! Autogenerated weights for pallet_xcm_bridge_hub +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2023-08-03, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `covid`, CPU: `11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz` +//! EXECUTION: , WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024 + +// Executed Command: +// target/release/rip-bridge-node +// benchmark +// pallet +// --chain=dev +// --steps=50 +// --repeat=20 +// --pallet=pallet_xcm_bridge_hub_router +// --extrinsic=* +// --execution=wasm +// --wasm-execution=Compiled +// --heap-pages=4096 +// --output=./modules/xcm-bridge-hub-router/src/weights.rs +// --template=./.maintain/bridge-weight-template.hbs + +#![allow(clippy::all)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{ + traits::Get, + weights::{constants::RocksDbWeight, Weight}, +}; +use sp_std::marker::PhantomData; + +/// Weight functions needed for pallet_xcm_bridge_hub_router. +pub trait WeightInfo { + fn open_bridge() -> Weight; + fn close_bridge() -> Weight; + fn update_notification_receiver() -> Weight; +} + +// For backwards compatibility and tests +impl WeightInfo for () { + fn open_bridge() -> Weight { + // Proof Size summary in bytes: + // Measured: `204` + // Estimated: `6070` + // Minimum execution time: 19_370_000 picoseconds. + Weight::from_parts(19_928_000, 0) + .saturating_add(Weight::from_parts(0, 6070)) + .saturating_add(RocksDbWeight::get().reads(2)) + .saturating_add(RocksDbWeight::get().writes(1)) + } + fn close_bridge() -> Weight { + // Proof Size summary in bytes: + // Measured: `204` + // Estimated: `6070` + // Minimum execution time: 20_045_000 picoseconds. + Weight::from_parts(20_861_000, 0) + .saturating_add(Weight::from_parts(0, 6070)) + .saturating_add(RocksDbWeight::get().reads(2)) + .saturating_add(RocksDbWeight::get().writes(1)) + } + fn update_notification_receiver() -> Weight { + // Proof Size summary in bytes: + // Measured: `109` + // Estimated: `3530` + // Minimum execution time: 12_179_000 picoseconds. + Weight::from_parts(12_679_000, 0) + .saturating_add(Weight::from_parts(0, 3530)) + .saturating_add(RocksDbWeight::get().reads(1)) + .saturating_add(RocksDbWeight::get().writes(1)) + } +} diff --git a/bridges/primitives/xcm-bridge-router/Cargo.toml b/bridges/primitives/xcm-bridge-router/Cargo.toml new file mode 100644 index 0000000000000..655f9fb073bdd --- /dev/null +++ b/bridges/primitives/xcm-bridge-router/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "bp-xcm-bridge-router" +description = "Primitives of the xcm-bridge-hub fee pallet." +version = "0.1.0" +authors.workspace = true +edition.workspace = true +license = "GPL-3.0-or-later WITH Classpath-exception-2.0" +repository.workspace = true + +[lints] +workspace = true + +[dependencies] +codec = { features = ["bit-vec", "derive"], workspace = true } +scale-info = { features = ["bit-vec", "derive"], workspace = true } + +# Substrate Dependencies +sp-core = { workspace = true } +sp-runtime = { workspace = true } + +# Polkadot Dependencies +xcm = { workspace = true } + +[features] +default = ["std"] +std = [ + "codec/std", + "scale-info/std", + "sp-core/std", + "sp-runtime/std", + "xcm/std", +] diff --git a/bridges/primitives/xcm-bridge-router/src/lib.rs b/bridges/primitives/xcm-bridge-router/src/lib.rs new file mode 100644 index 0000000000000..d1af5fa4f25e0 --- /dev/null +++ b/bridges/primitives/xcm-bridge-router/src/lib.rs @@ -0,0 +1,83 @@ +// Copyright (C) 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 . + +//! Primitives of the `xcm-bridge-hub-router` pallet. + +#![cfg_attr(not(feature = "std"), no_std)] + +use codec::{Decode, Encode, FullCodec, MaxEncodedLen}; +use scale_info::TypeInfo; +use sp_core::sp_std::fmt::Debug; +use sp_runtime::{FixedU128, RuntimeDebug}; +use xcm::latest::prelude::{InteriorLocation, Location, NetworkId}; + +/// Minimal delivery fee factor. +pub const MINIMAL_DELIVERY_FEE_FACTOR: FixedU128 = FixedU128::from_u32(1); + +/// Current status of the bridge. +#[derive(Clone, Decode, Encode, Eq, PartialEq, TypeInfo, MaxEncodedLen, RuntimeDebug)] +pub struct BridgeState { + /// Current delivery fee factor. + pub delivery_fee_factor: FixedU128, + /// Bridge congestion flag. + pub is_congested: bool, +} + +impl Default for BridgeState { + fn default() -> Self { + BridgeState { delivery_fee_factor: MINIMAL_DELIVERY_FEE_FACTOR, is_congested: false } + } +} + +/// Trait that resolves a specific `BridgeId` for `dest`. +pub trait ResolveBridgeId { + /// Bridge identifier. + type BridgeId: FullCodec + MaxEncodedLen + TypeInfo + Debug + Clone + PartialEq + Eq; + /// Resolves `Self::BridgeId` for `dest`. If `None`, it means there is no supported bridge ID. + fn resolve_for_dest(bridged_dest: &Location) -> Option; + + /// Resolves `Self::BridgeId` for `bridged_network` and `bridged_dest`. If `None`, it means + /// there is no supported bridge ID. + fn resolve_for( + bridged_network: &NetworkId, + bridged_dest: &InteriorLocation, + ) -> Option; +} + +/// The default implementation of `ResolveBridgeId` for `()` returns `None`. +impl ResolveBridgeId for () { + type BridgeId = (); + + fn resolve_for_dest(_dest: &Location) -> Option { + None + } + + fn resolve_for( + _bridged_network: &NetworkId, + _bridged_dest: &InteriorLocation, + ) -> Option { + None + } +} + +/// A minimized version of `pallet-xcm-bridge-router::Call` that can be used without a runtime. +#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone, TypeInfo)] +#[allow(non_camel_case_types)] +pub enum XcmBridgeHubCall { + /// `pallet-xcm-bridge-router::Call::update_bridge_status` + #[codec(index = 0)] + update_bridge_status { bridge_id: BridgeId, is_congested: bool }, +} diff --git a/bridges/primitives/xcm-bridge/Cargo.toml b/bridges/primitives/xcm-bridge/Cargo.toml new file mode 100644 index 0000000000000..a324729b91541 --- /dev/null +++ b/bridges/primitives/xcm-bridge/Cargo.toml @@ -0,0 +1,45 @@ +[package] +name = "bp-xcm-bridge" +description = "Primitives of the xcm-bridge-hub pallet." +version = "0.2.0" +authors.workspace = true +edition.workspace = true +license = "GPL-3.0-or-later WITH Classpath-exception-2.0" +repository.workspace = true + +[lints] +workspace = true + +[dependencies] +codec = { features = ["derive"], workspace = true } +impl-trait-for-tuples = { workspace = true } +scale-info = { features = ["derive"], workspace = true } +serde = { features = ["alloc", "derive"], workspace = true } + +# Bridge Dependencies +bp-messages = { workspace = true } +bp-runtime = { workspace = true } + +# Substrate Dependencies +frame-support = { workspace = true } +sp-core = { workspace = true } +sp-io = { workspace = true } +sp-std = { workspace = true } + +# Polkadot Dependencies +xcm = { workspace = true } + +[features] +default = ["std"] +std = [ + "bp-messages/std", + "bp-runtime/std", + "codec/std", + "frame-support/std", + "scale-info/std", + "serde/std", + "sp-core/std", + "sp-io/std", + "sp-std/std", + "xcm/std", +] diff --git a/bridges/primitives/xcm-bridge/src/call_info.rs b/bridges/primitives/xcm-bridge/src/call_info.rs new file mode 100644 index 0000000000000..d040746fae9b4 --- /dev/null +++ b/bridges/primitives/xcm-bridge/src/call_info.rs @@ -0,0 +1,47 @@ +// Copyright (C) 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 . + +//! Defines structures related to calls of the `pallet-xcm-bridge` pallet. + +use crate::Receiver; +use bp_messages::MessageNonce; +use codec::{Decode, Encode}; +use scale_info::TypeInfo; +use sp_std::boxed::Box; +use xcm::prelude::VersionedInteriorLocation; + +/// A minimized version of `pallet_xcm_bridge::Call` that can be used without a runtime. +#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone, TypeInfo)] +#[allow(non_camel_case_types)] +pub enum XcmBridgeCall { + /// `pallet_xcm_bridge::Call::open_bridge` + #[codec(index = 0)] + open_bridge { + /// Universal `InteriorLocation` from the bridged consensus. + bridge_destination_universal_location: Box, + /// Optional `maybe_notify` holds data about the `bridge_origin_relative_location` where + /// notifications can be sent to handle congestion. + maybe_notify: Option, + }, + /// `pallet_xcm_bridge::Call::close_bridge` + #[codec(index = 1)] + close_bridge { + /// Universal `InteriorLocation` from the bridged consensus. + bridge_destination_universal_location: Box, + /// The number of messages that we may prune in a single call. + may_prune_messages: MessageNonce, + }, +} diff --git a/bridges/primitives/xcm-bridge/src/lib.rs b/bridges/primitives/xcm-bridge/src/lib.rs new file mode 100644 index 0000000000000..2db3acb4fc1d8 --- /dev/null +++ b/bridges/primitives/xcm-bridge/src/lib.rs @@ -0,0 +1,789 @@ +// 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 . + +//! Primitives of the xcm-bridge-hub pallet. + +#![warn(missing_docs)] +#![cfg_attr(not(feature = "std"), no_std)] + +use bp_messages::LaneIdType; +use bp_runtime::{AccountIdOf, BalanceOf, Chain}; +pub use call_info::XcmBridgeCall; +use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen}; +use frame_support::{ + ensure, sp_runtime::RuntimeDebug, CloneNoBound, PalletError, PartialEqNoBound, + RuntimeDebugNoBound, +}; +use scale_info::TypeInfo; +use serde::{Deserialize, Serialize}; +use sp_core::H256; +use sp_io::hashing::blake2_256; +use sp_std::boxed::Box; +use xcm::{ + latest::prelude::*, prelude::XcmVersion, IntoVersion, VersionedInteriorLocation, + VersionedLocation, +}; + +mod call_info; + +/// Encoded XCM blob. We expect the bridge messages pallet to use this blob type for both inbound +/// and outbound payloads. +pub type XcmAsPlainPayload = sp_std::vec::Vec; + +/// Bridge identifier - used **only** for communicating with sibling/parent chains in the same +/// consensus. +/// +/// For example, `SendXcm` implementations (which use the `latest` XCM) can use it to identify a +/// bridge and the corresponding `LaneId` that is used for over-consensus communication between +/// bridge hubs. +/// +/// This identifier is constructed from the `latest` XCM, so it is expected to ensure migration to +/// the `latest` XCM version. This could change the `BridgeId`, but it will not affect the `LaneId`. +/// In other words, `LaneId` will never change, while `BridgeId` could change with (every) XCM +/// upgrade. +#[derive( + Clone, + Copy, + Decode, + DecodeWithMemTracking, + Encode, + Eq, + Ord, + PartialOrd, + PartialEq, + TypeInfo, + MaxEncodedLen, + Serialize, + Deserialize, +)] +pub struct BridgeId(H256); + +impl BridgeId { + /// Create bridge identifier from two universal locations. + /// + /// Note: The `BridgeId` is constructed from `latest` XCM, so if stored, you need to ensure + /// compatibility with newer XCM versions. + pub fn new( + universal_source: &InteriorLocation, + universal_destination: &InteriorLocation, + ) -> Self { + const VALUES_SEPARATOR: [u8; 33] = *b"bridges-bridge-id-value-separator"; + + BridgeId( + (universal_source, VALUES_SEPARATOR, universal_destination) + .using_encoded(blake2_256) + .into(), + ) + } + + /// Access the inner representation. + pub fn inner(&self) -> H256 { + self.0 + } +} + +impl core::fmt::Debug for BridgeId { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + core::fmt::Debug::fmt(&self.0, f) + } +} + +/// Local XCM channel manager. +pub trait LocalXcmChannelManager { + /// Error that may be returned when suspending/resuming the bridge. + type Error: sp_std::fmt::Debug; + + /// Suspend the bridge, opened by given origin. + /// + /// The `local_origin` is guaranteed to be in the same consensus. However, it may point to + /// something below the chain level - like the contract or pallet instance, for example. + fn suspend_bridge(local_origin: &Location, bridge: Bridge) -> Result<(), Self::Error>; + + /// Resume the previously suspended bridge, opened by given origin. + /// + /// The `local_origin` is guaranteed to be in the same consensus. However, it may point to + /// something below the chain level - like the contract or pallet instance, for example. + fn resume_bridge(local_origin: &Location, bridge: Bridge) -> Result<(), Self::Error>; +} + +impl LocalXcmChannelManager for () { + type Error = (); + + fn suspend_bridge(_local_origin: &Location, _bridge: Bridge) -> Result<(), Self::Error> { + Ok(()) + } + + fn resume_bridge(_local_origin: &Location, _bridge: Bridge) -> Result<(), Self::Error> { + Ok(()) + } +} + +/// Channel status provider that may report whether it is congested or not. +pub trait ChannelStatusProvider { + /// Returns true if the channel is currently active and can be used. + fn is_congested(with: &Location) -> bool; +} + +/// Tuple implementation of `ChannelStatusProvider`, by default indicating no congestion. +#[impl_trait_for_tuples::impl_for_tuples(30)] +impl ChannelStatusProvider for Tuple { + fn is_congested(with: &Location) -> bool { + for_tuples!( #( + if Tuple::is_congested(with) { + return true; + } + )* ); + + false + } +} + +/// 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 suspended. Associated lanes are opened. + /// *suspended* means that we have sent the "Suspended" message/signal to the local bridge + /// origin. + /// + /// We keep accepting messages to the bridge to allow any inflight messages to be processed. + SoftSuspended, + /// Bridge is suspended and new messages are now being actively dropped. + HardSuspended, + /// 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, LaneId))] +pub struct Bridge { + /// Relative location of the bridge origin chain. This is expected to be **convertible** to the + /// `latest` XCM, so the check and migration needs to be ensured. + pub bridge_origin_relative_location: Box, + + /// See [`BridgeLocations::bridge_origin_universal_location`]. + /// Stored for `BridgeId` sanity check. + pub bridge_origin_universal_location: Box, + /// See [`BridgeLocations::bridge_destination_universal_location`]. + /// Stored for `BridgeId` sanity check. + pub bridge_destination_universal_location: Box, + + /// Current bridge state. + pub state: BridgeState, + + /// Reserved amount on the sovereign account of the sibling bridge origin. + /// The account is derived from `self.bridge_origin_relative_location`. + pub deposit: Option>, + + /// Mapping to the unique `LaneId`. + pub lane_id: LaneId, + + /// Holds data about the `bridge_origin_relative_location` where notifications can be sent for + /// handling congestion. + pub maybe_notify: Option, +} + +/// Receiver metadata. +#[derive( + CloneNoBound, + Decode, + DecodeWithMemTracking, + Encode, + Eq, + PartialEqNoBound, + TypeInfo, + MaxEncodedLen, + RuntimeDebugNoBound, + Serialize, + Deserialize, +)] +pub struct Receiver { + /// Pallet index. + pub pallet_index: u8, + /// Call/extrinsic index. + pub call_index: u8, +} + +impl Receiver { + /// Create a new receiver. + pub fn new(pallet_index: u8, call_index: u8) -> Self { + Self { pallet_index, call_index } + } +} + +/// An alias for the bridge deposit of `ThisChain`. +pub type DepositOf = Deposit, BalanceOf>; + +/// A structure containing information about from whom the deposit is reserved. +#[derive( + Clone, + Decode, + DecodeWithMemTracking, + Encode, + Eq, + PartialEq, + TypeInfo, + MaxEncodedLen, + RuntimeDebug, +)] +pub struct Deposit { + /// Account with the reserved funds. + pub account: AccountId, + /// Reserved amount. + pub amount: Balance, +} + +impl Deposit { + /// Create new deposit. + pub fn new(account: AccountId, amount: Balance) -> Self { + Self { account, amount } + } +} + +/// 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. + bridge_origin_relative_location: Location, + /// Universal (unique) location of this side of the bridge. + bridge_origin_universal_location: InteriorLocation, + /// Universal (unique) location of other side of the bridge. + bridge_destination_universal_location: InteriorLocation, + /// An identifier of the dedicated bridge message lane. + bridge_id: BridgeId, +} + +/// Errors that may happen when we check bridge locations. +#[derive( + Encode, Decode, DecodeWithMemTracking, 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, + /// The version of XCM location argument is unsupported. + UnsupportedXcmVersion, + /// The `LaneIdType` generator is not supported. + UnsupportedLaneIdType, +} + +impl BridgeLocations { + /// 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: InteriorLocation, + bridge_origin_relative_location: Location, + bridge_destination_universal_location: InteriorLocation, + 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 bridge_id = BridgeId::new( + &bridge_origin_universal_location, + &bridge_destination_universal_location, + ); + + Ok(Box::new(BridgeLocations { + bridge_origin_relative_location, + bridge_origin_universal_location, + bridge_destination_universal_location, + bridge_id, + })) + } + + /// Getter for `bridge_origin_relative_location` + pub fn bridge_origin_relative_location(&self) -> &Location { + &self.bridge_origin_relative_location + } + + /// Getter for `bridge_origin_universal_location` + pub fn bridge_origin_universal_location(&self) -> &InteriorLocation { + &self.bridge_origin_universal_location + } + + /// Getter for `bridge_destination_universal_location` + pub fn bridge_destination_universal_location(&self) -> &InteriorLocation { + &self.bridge_destination_universal_location + } + + /// Getter for `bridge_id` + pub fn bridge_id(&self) -> &BridgeId { + &self.bridge_id + } + + /// Generates the exact same `LaneId` on the both bridge hubs. + /// + /// Note: Use this **only** when opening a new bridge. + pub fn calculate_lane_id( + &self, + xcm_version: XcmVersion, + ) -> Result { + // a tricky helper struct that adds required `Ord` support for + // `VersionedInteriorLocation` + #[derive(Eq, PartialEq, Ord, PartialOrd)] + struct EncodedVersionedInteriorLocation(sp_std::vec::Vec); + impl Encode for EncodedVersionedInteriorLocation { + fn encode(&self) -> sp_std::vec::Vec { + self.0.clone() + } + } + + let universal_location1 = + VersionedInteriorLocation::from(self.bridge_origin_universal_location.clone()) + .into_version(xcm_version) + .map_err(|_| BridgeLocationsError::UnsupportedXcmVersion); + let universal_location2 = + VersionedInteriorLocation::from(self.bridge_destination_universal_location.clone()) + .into_version(xcm_version) + .map_err(|_| BridgeLocationsError::UnsupportedXcmVersion); + + LaneId::try_new( + EncodedVersionedInteriorLocation(universal_location1.encode()), + EncodedVersionedInteriorLocation(universal_location2.encode()), + ) + .map_err(|_| BridgeLocationsError::UnsupportedLaneIdType) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use xcm::latest::ROCOCO_GENESIS_HASH; + + const LOCAL_NETWORK: NetworkId = Kusama; + const REMOTE_NETWORK: NetworkId = Polkadot; + const UNREACHABLE_NETWORK: NetworkId = NetworkId::ByGenesis(ROCOCO_GENESIS_HASH); + 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, + + expected_remote_network: NetworkId, + } + + fn run_successful_test(test: SuccessfulTest) -> BridgeLocations { + let locations = BridgeLocations::bridge_locations( + test.here_universal_location, + test.bridge_origin_relative_location.clone(), + test.bridge_destination_universal_location.clone(), + test.expected_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.clone(), + bridge_destination_universal_location: test + .bridge_destination_universal_location + .clone(), + bridge_id: BridgeId::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(), + + expected_remote_network: REMOTE_NETWORK, + }); + } + + #[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(), + + expected_remote_network: REMOTE_NETWORK, + }); + } + + #[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(), + + expected_remote_network: REMOTE_NETWORK, + }); + } + + #[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(), + + expected_remote_network: REMOTE_NETWORK, + }); + } + + #[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(), + + expected_remote_network: REMOTE_NETWORK, + }); + } + + #[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(), + + expected_remote_network: REMOTE_NETWORK, + }); + } + + #[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(), + + expected_remote_network: REMOTE_NETWORK, + }); + } + + #[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(), + + expected_remote_network: REMOTE_NETWORK, + }); + } + + // 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(), + + expected_remote_network: REMOTE_NETWORK, + }); + 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(), + + expected_remote_network: REMOTE_NETWORK, + }); + + assert_eq!(locations1.bridge_id, locations2.bridge_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(), + + expected_remote_network: REMOTE_NETWORK, + }); + 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(), + + expected_remote_network: REMOTE_NETWORK, + }); + + assert_eq!(locations1.bridge_id, locations2.bridge_id); + } + + #[test] + fn calculate_lane_id_works() { + type TestLaneId = bp_messages::HashedLaneId; + + let from_local_to_remote = 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(), + + expected_remote_network: REMOTE_NETWORK, + }); + + let from_remote_to_local = run_successful_test(SuccessfulTest { + here_universal_location: [GlobalConsensus(REMOTE_NETWORK), Parachain(LOCAL_BRIDGE_HUB)] + .into(), + bridge_origin_relative_location: ParentThen([Parachain(REMOTE_PARACHAIN)].into()) + .into(), + + bridge_origin_universal_location: [ + GlobalConsensus(REMOTE_NETWORK), + Parachain(REMOTE_PARACHAIN), + ] + .into(), + bridge_destination_universal_location: [ + GlobalConsensus(LOCAL_NETWORK), + Parachain(SIBLING_PARACHAIN), + ] + .into(), + + expected_remote_network: LOCAL_NETWORK, + }); + + assert_ne!( + from_local_to_remote.calculate_lane_id::(xcm::latest::VERSION), + from_remote_to_local.calculate_lane_id::(xcm::latest::VERSION - 1), + ); + assert_eq!( + from_local_to_remote.calculate_lane_id::(xcm::latest::VERSION), + from_remote_to_local.calculate_lane_id::(xcm::latest::VERSION), + ); + } + + // negative tests + + #[test] + fn bridge_locations_fails_when_here_is_not_universal_location() { + assert_eq!( + BridgeLocations::bridge_locations( + [Parachain(1000)].into(), + Here.into(), + [GlobalConsensus(REMOTE_NETWORK)].into(), + REMOTE_NETWORK, + ), + Err(BridgeLocationsError::NonUniversalLocation), + ); + } + + #[test] + fn bridge_locations_fails_when_computed_destination_is_not_universal_location() { + assert_eq!( + BridgeLocations::bridge_locations( + [GlobalConsensus(LOCAL_NETWORK)].into(), + Here.into(), + [OnlyChild].into(), + REMOTE_NETWORK, + ), + Err(BridgeLocationsError::NonUniversalLocation), + ); + } + + #[test] + fn bridge_locations_fails_when_computed_destination_is_local() { + assert_eq!( + BridgeLocations::bridge_locations( + [GlobalConsensus(LOCAL_NETWORK)].into(), + Here.into(), + [GlobalConsensus(LOCAL_NETWORK), OnlyChild].into(), + REMOTE_NETWORK, + ), + Err(BridgeLocationsError::DestinationIsLocal), + ); + } + + #[test] + fn bridge_locations_fails_when_computed_destination_is_unreachable() { + assert_eq!( + BridgeLocations::bridge_locations( + [GlobalConsensus(LOCAL_NETWORK)].into(), + Here.into(), + [GlobalConsensus(UNREACHABLE_NETWORK)].into(), + REMOTE_NETWORK, + ), + Err(BridgeLocationsError::UnreachableDestination), + ); + } +} diff --git a/cumulus/pallets/xcmp-queue/Cargo.toml b/cumulus/pallets/xcmp-queue/Cargo.toml index b71730577a6ff..8fc3d0e6f311c 100644 --- a/cumulus/pallets/xcmp-queue/Cargo.toml +++ b/cumulus/pallets/xcmp-queue/Cargo.toml @@ -41,6 +41,7 @@ bounded-collections = { workspace = true } frame-benchmarking = { optional = true, workspace = true } # Bridges +bp-xcm-bridge = { optional = true, workspace = true } bp-xcm-bridge-hub-router = { optional = true, workspace = true } [dev-dependencies] @@ -58,6 +59,7 @@ std = [ "approx/std", "bounded-collections/std", "bp-xcm-bridge-hub-router?/std", + "bp-xcm-bridge?/std", "codec/std", "cumulus-primitives-core/std", "frame-benchmarking?/std", @@ -100,4 +102,4 @@ try-runtime = [ "polkadot-runtime-parachains/try-runtime", "sp-runtime/try-runtime", ] -bridging = ["bp-xcm-bridge-hub-router"] +bridging = ["bp-xcm-bridge", "bp-xcm-bridge-hub-router"] diff --git a/cumulus/pallets/xcmp-queue/src/bridging.rs b/cumulus/pallets/xcmp-queue/src/bridging.rs index 355691a41659d..d3e43fdb6328c 100644 --- a/cumulus/pallets/xcmp-queue/src/bridging.rs +++ b/cumulus/pallets/xcmp-queue/src/bridging.rs @@ -95,6 +95,14 @@ impl bp_xcm_bridge_hub_router::XcmChannelStatusProvider } } +impl bp_xcm_bridge::ChannelStatusProvider + for OutXcmpChannelStatusProvider +{ + fn is_congested(with: &Location) -> bool { + Self::is_congested(with) + } +} + #[cfg(feature = "runtime-benchmarks")] pub fn suspend_channel_for_benchmarks(target: ParaId) { pallet::Pallet::::suspend_channel(target) diff --git a/cumulus/parachains/integration-tests/emulated/chains/parachains/assets/asset-hub-rococo/src/lib.rs b/cumulus/parachains/integration-tests/emulated/chains/parachains/assets/asset-hub-rococo/src/lib.rs index 052d7eb074475..d8564d7676d37 100644 --- a/cumulus/parachains/integration-tests/emulated/chains/parachains/assets/asset-hub-rococo/src/lib.rs +++ b/cumulus/parachains/integration-tests/emulated/chains/parachains/assets/asset-hub-rococo/src/lib.rs @@ -24,8 +24,8 @@ use frame_support::traits::OnInitialize; use emulated_integration_tests_common::{ impl_accounts_helpers_for_parachain, impl_assert_events_helpers_for_parachain, impl_assets_helpers_for_parachain, impl_assets_helpers_for_system_parachain, - impl_bridge_helpers_for_chain, impl_foreign_assets_helpers_for_parachain, - impl_xcm_helpers_for_parachain, impls::Parachain, xcm_emulator::decl_test_parachains, + impl_foreign_assets_helpers_for_parachain, impl_xcm_helpers_for_parachain, impls::Parachain, + xcm_emulator::decl_test_parachains, }; use rococo_emulated_chain::Rococo; @@ -63,9 +63,3 @@ impl_assets_helpers_for_system_parachain!(AssetHubRococo, Rococo); impl_assets_helpers_for_parachain!(AssetHubRococo); impl_foreign_assets_helpers_for_parachain!(AssetHubRococo, xcm::v5::Location); impl_xcm_helpers_for_parachain!(AssetHubRococo); -impl_bridge_helpers_for_chain!( - AssetHubRococo, - ParaPallet, - PolkadotXcm, - bp_bridge_hub_rococo::RuntimeCall::XcmOverBridgeHubWestend -); diff --git a/cumulus/parachains/integration-tests/emulated/chains/parachains/assets/asset-hub-westend/src/lib.rs b/cumulus/parachains/integration-tests/emulated/chains/parachains/assets/asset-hub-westend/src/lib.rs index 6465e4fece3fd..40da6f2f9fc6b 100644 --- a/cumulus/parachains/integration-tests/emulated/chains/parachains/assets/asset-hub-westend/src/lib.rs +++ b/cumulus/parachains/integration-tests/emulated/chains/parachains/assets/asset-hub-westend/src/lib.rs @@ -24,8 +24,8 @@ use frame_support::traits::OnInitialize; use emulated_integration_tests_common::{ impl_accounts_helpers_for_parachain, impl_assert_events_helpers_for_parachain, impl_assets_helpers_for_parachain, impl_assets_helpers_for_system_parachain, - impl_bridge_helpers_for_chain, impl_foreign_assets_helpers_for_parachain, - impl_xcm_helpers_for_parachain, impls::Parachain, xcm_emulator::decl_test_parachains, + impl_foreign_assets_helpers_for_parachain, impl_xcm_helpers_for_parachain, impls::Parachain, + xcm_emulator::decl_test_parachains, }; use westend_emulated_chain::Westend; @@ -65,9 +65,3 @@ impl_assets_helpers_for_system_parachain!(AssetHubWestend, Westend); impl_assets_helpers_for_parachain!(AssetHubWestend); impl_foreign_assets_helpers_for_parachain!(AssetHubWestend, xcm::v5::Location); impl_xcm_helpers_for_parachain!(AssetHubWestend); -impl_bridge_helpers_for_chain!( - AssetHubWestend, - ParaPallet, - PolkadotXcm, - bp_bridge_hub_westend::RuntimeCall::XcmOverBridgeHubRococo -); diff --git a/cumulus/parachains/integration-tests/emulated/common/Cargo.toml b/cumulus/parachains/integration-tests/emulated/common/Cargo.toml index c31196a9ef50c..5beb279dff6b1 100644 --- a/cumulus/parachains/integration-tests/emulated/common/Cargo.toml +++ b/cumulus/parachains/integration-tests/emulated/common/Cargo.toml @@ -53,6 +53,7 @@ xcm-emulator = { workspace = true, default-features = true } # Bridges bp-messages = { workspace = true, default-features = true } -bp-xcm-bridge-hub = { workspace = true, default-features = true } +bp-xcm-bridge = { workspace = true, default-features = true } pallet-bridge-messages = { workspace = true, default-features = true } +pallet-xcm-bridge = { workspace = true, default-features = true } pallet-xcm-bridge-hub = { workspace = true, default-features = true } diff --git a/cumulus/parachains/integration-tests/emulated/common/src/impls.rs b/cumulus/parachains/integration-tests/emulated/common/src/impls.rs index 00b96d39c75d0..168ac7cf4ea24 100644 --- a/cumulus/parachains/integration-tests/emulated/common/src/impls.rs +++ b/cumulus/parachains/integration-tests/emulated/common/src/impls.rs @@ -64,17 +64,12 @@ use bp_messages::{ target_chain::{DispatchMessage, DispatchMessageData, MessageDispatch}, MessageKey, OutboundLaneData, }; -pub use bp_xcm_bridge_hub::XcmBridgeHubCall; +pub use bp_xcm_bridge::{Receiver, XcmBridgeCall}; use pallet_bridge_messages::{Config as BridgeMessagesConfig, LaneIdOf, OutboundLanes, Pallet}; pub use pallet_bridge_messages::{ Instance1 as BridgeMessagesInstance1, Instance2 as BridgeMessagesInstance2, Instance3 as BridgeMessagesInstance3, }; -use pallet_xcm_bridge_hub::XcmBlobMessageDispatchResult; - -pub struct BridgeHubMessageHandler { - _marker: std::marker::PhantomData<(S, SI, T, TI)>, -} struct LaneIdWrapper(LaneId); impl From> for BridgeLaneId { @@ -88,6 +83,11 @@ impl From for LaneIdWrapper { } } +/// Implementation of `BridgeMessageHandler` for `pallet_bridge_messages`, +/// configured with `pallet_xcm_bridge_hub`. +pub struct BridgeHubMessageHandler { + _marker: std::marker::PhantomData<(S, SI, T, TI)>, +} impl BridgeMessageHandler for BridgeHubMessageHandler where S: BridgeMessagesConfig, @@ -96,7 +96,7 @@ where TI: 'static, >::InboundPayload: From>, >::MessageDispatch: - MessageDispatch, + MessageDispatch, { fn get_source_outbound_messages() -> Vec { // get the source active outbound lanes @@ -142,6 +142,94 @@ where data: DispatchMessageData::> { payload }, }); + use pallet_xcm_bridge_hub::XcmBlobMessageDispatchResult; + + let result = match dispatch_result.dispatch_level_result { + XcmBlobMessageDispatchResult::Dispatched => Ok(()), + XcmBlobMessageDispatchResult::InvalidPayload => Err(BridgeMessageDispatchError( + Box::new(XcmBlobMessageDispatchResult::InvalidPayload), + )), + XcmBlobMessageDispatchResult::NotDispatched(e) => Err(BridgeMessageDispatchError( + Box::new(XcmBlobMessageDispatchResult::NotDispatched(e)), + )), + }; + result + } + + fn notify_source_message_delivery(lane_id: BridgeLaneId) { + let lane_id: LaneIdOf = LaneIdWrapper::from(lane_id).0; + let data = OutboundLanes::::get(lane_id).unwrap(); + let new_data = OutboundLaneData { + oldest_unpruned_nonce: data.oldest_unpruned_nonce + 1, + latest_received_nonce: data.latest_received_nonce + 1, + ..data + }; + + OutboundLanes::::insert(lane_id, new_data); + } +} + +/// Implementation of `BridgeMessageHandler` for `pallet_bridge_messages`, +/// configured with `pallet_xcm_bridge`. +pub struct BridgeMessagesHandler { + _marker: std::marker::PhantomData<(S, SI, T, TI)>, +} +impl BridgeMessageHandler for BridgeMessagesHandler +where + S: BridgeMessagesConfig, + SI: 'static, + T: BridgeMessagesConfig, + TI: 'static, + >::InboundPayload: From>, + >::MessageDispatch: + MessageDispatch, +{ + fn get_source_outbound_messages() -> Vec { + // get the source active outbound lanes + let active_outbound_lanes = OutboundLanes::::iter_keys(); + + let mut messages: Vec = Default::default(); + + // collect messages from `OutboundMessages` for each active outbound lane in the source + for lane in active_outbound_lanes { + let latest_generated_nonce = + OutboundLanes::::get(lane).unwrap().latest_generated_nonce; + let latest_received_nonce = + OutboundLanes::::get(lane).unwrap().latest_received_nonce; + + (latest_received_nonce + 1..=latest_generated_nonce).for_each(|nonce| { + let encoded_payload: Vec = Pallet::::outbound_message_data(lane, nonce) + .expect("Bridge message does not exist") + .into(); + let payload = Vec::::decode(&mut &encoded_payload[..]) + .expect("Decoding XCM message failed"); + let message = BridgeMessage { lane_id: LaneIdWrapper(lane).into(), nonce, payload }; + + messages.push(message); + }); + } + messages + } + + fn dispatch_target_inbound_message( + message: BridgeMessage, + ) -> Result<(), BridgeMessageDispatchError> { + type TargetMessageDispatch = >::MessageDispatch; + type InboundPayload = >::InboundPayload; + + let lane_id = LaneIdWrapper::from(message.lane_id).0; + let nonce = message.nonce; + let payload = Ok(From::from(message.payload)); + + // Directly dispatch outbound messages assuming everything is correct + // and bypassing the `Relayers` and `InboundLane` logic + let dispatch_result = TargetMessageDispatch::::dispatch(DispatchMessage { + key: MessageKey { lane_id, nonce }, + data: DispatchMessageData::> { payload }, + }); + + use pallet_xcm_bridge::XcmBlobMessageDispatchResult; + let result = match dispatch_result.dispatch_level_result { XcmBlobMessageDispatchResult::Dispatched => Ok(()), XcmBlobMessageDispatchResult::InvalidPayload => Err(BridgeMessageDispatchError( @@ -937,25 +1025,27 @@ macro_rules! impl_bridge_helpers_for_chain { ( $chain:ident, $pallet:ident, $pallet_xcm:ident, $runtime_call_wrapper:path ) => { $crate::impls::paste::paste! { impl $chain { - /// Open bridge with `dest`. + /// Open bridge with `dest` (works with `pallet-xcm-bridge`). pub fn open_bridge( bridge_location: $crate::impls::Location, bridge_destination_universal_location: $crate::impls::InteriorLocation, - maybe_paid: Option<($crate::impls::Asset, $crate::impls::AccountId)> + maybe_paid: Option<($crate::impls::Asset, $crate::impls::AccountId)>, + maybe_notify: Option<$crate::impls::Receiver>, ) { ::execute_with(|| { use $crate::impls::{bx, Chain}; - use $crate::impls::XcmBridgeHubCall; + use $crate::impls::XcmBridgeCall; use $crate::impls::Encode; // important to use `root` and `OriginKind::Xcm` let root_origin = ::RuntimeOrigin::root(); // construct call - let call: $crate::impls::DoubleEncoded<()> = $runtime_call_wrapper(XcmBridgeHubCall::open_bridge { + let call: $crate::impls::DoubleEncoded<()> = $runtime_call_wrapper(XcmBridgeCall::open_bridge { bridge_destination_universal_location: bx!( bridge_destination_universal_location.clone().into() - ) + ), + maybe_notify, }).encode().into(); let xcm = if let Some((fee_asset, beneficiary)) = maybe_paid { diff --git a/cumulus/parachains/runtimes/assets/asset-hub-rococo/Cargo.toml b/cumulus/parachains/runtimes/assets/asset-hub-rococo/Cargo.toml index 48e8b651ebc46..f1b1425457c4a 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-rococo/Cargo.toml +++ b/cumulus/parachains/runtimes/assets/asset-hub-rococo/Cargo.toml @@ -97,10 +97,11 @@ bp-asset-hub-rococo = { workspace = true } bp-asset-hub-westend = { workspace = true } bp-bridge-hub-rococo = { workspace = true } bp-bridge-hub-westend = { workspace = true } -pallet-xcm-bridge-hub-router = { workspace = true } +pallet-xcm-bridge-router = { workspace = true } [dev-dependencies] asset-test-utils = { workspace = true, default-features = true } +bp-xcm-bridge = { workspace = true } parachains-runtimes-test-utils = { workspace = true, default-features = true } [build-dependencies] @@ -138,7 +139,7 @@ runtime-benchmarks = [ "pallet-uniques/runtime-benchmarks", "pallet-utility/runtime-benchmarks", "pallet-xcm-benchmarks/runtime-benchmarks", - "pallet-xcm-bridge-hub-router/runtime-benchmarks", + "pallet-xcm-bridge-router/runtime-benchmarks", "pallet-xcm/runtime-benchmarks", "parachains-common/runtime-benchmarks", "polkadot-parachain-primitives/runtime-benchmarks", @@ -180,7 +181,7 @@ try-runtime = [ "pallet-transaction-payment/try-runtime", "pallet-uniques/try-runtime", "pallet-utility/try-runtime", - "pallet-xcm-bridge-hub-router/try-runtime", + "pallet-xcm-bridge-router/try-runtime", "pallet-xcm/try-runtime", "parachain-info/try-runtime", "parachains-common/try-runtime", @@ -193,6 +194,7 @@ std = [ "bp-asset-hub-westend/std", "bp-bridge-hub-rococo/std", "bp-bridge-hub-westend/std", + "bp-xcm-bridge/std", "codec/std", "cumulus-pallet-aura-ext/std", "cumulus-pallet-parachain-system/std", @@ -233,7 +235,7 @@ std = [ "pallet-uniques/std", "pallet-utility/std", "pallet-xcm-benchmarks?/std", - "pallet-xcm-bridge-hub-router/std", + "pallet-xcm-bridge-router/std", "pallet-xcm/std", "parachain-info/std", "parachains-common/std", diff --git a/cumulus/parachains/runtimes/assets/asset-hub-rococo/bridge-primitives/Cargo.toml b/cumulus/parachains/runtimes/assets/asset-hub-rococo/bridge-primitives/Cargo.toml index f7159f334a8b5..fd7c3e97b56f4 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-rococo/bridge-primitives/Cargo.toml +++ b/cumulus/parachains/runtimes/assets/asset-hub-rococo/bridge-primitives/Cargo.toml @@ -26,7 +26,7 @@ sp-core = { workspace = true } bp-bridge-hub-cumulus = { workspace = true } bp-messages = { workspace = true } bp-runtime = { workspace = true } -bp-xcm-bridge-hub-router = { workspace = true } +bp-xcm-bridge-router = { workspace = true } testnet-parachains-constants = { features = ["rococo"], workspace = true } # Polkadot dependencies @@ -38,7 +38,7 @@ std = [ "bp-bridge-hub-cumulus/std", "bp-messages/std", "bp-runtime/std", - "bp-xcm-bridge-hub-router/std", + "bp-xcm-bridge-router/std", "codec/std", "frame-support/std", "scale-info/std", diff --git a/cumulus/parachains/runtimes/assets/asset-hub-rococo/bridge-primitives/src/lib.rs b/cumulus/parachains/runtimes/assets/asset-hub-rococo/bridge-primitives/src/lib.rs index 759972b62fd79..52ccc8bb1881c 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-rococo/bridge-primitives/src/lib.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-rococo/bridge-primitives/src/lib.rs @@ -27,7 +27,7 @@ use bp_messages::*; use bp_runtime::{ decl_bridge_finality_runtime_apis, decl_bridge_messages_runtime_apis, Chain, ChainId, Parachain, }; -pub use bp_xcm_bridge_hub_router::XcmBridgeHubRouterCall; +pub use bp_xcm_bridge_router::XcmBridgeHubCall; use frame_support::{ dispatch::DispatchClass, sp_runtime::{MultiAddress, MultiSigner, RuntimeDebug, StateVersion}, @@ -48,7 +48,7 @@ use xcm::latest::prelude::*; pub enum Call { /// `ToWestendXcmRouter` bridge pallet. #[codec(index = 45)] - ToWestendXcmRouter(XcmBridgeHubRouterCall), + ToWestendXcmRouter(XcmBridgeHubCall), } frame_support::parameter_types! { @@ -69,7 +69,7 @@ pub fn build_congestion_message( Transact { origin_kind: OriginKind::Xcm, fallback_max_weight: Some(XcmBridgeHubRouterTransactCallMaxWeight::get()), - call: Call::ToWestendXcmRouter(XcmBridgeHubRouterCall::report_bridge_status { + call: Call::ToWestendXcmRouter(XcmBridgeHubCall::update_bridge_status { bridge_id, is_congested, }) diff --git a/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/lib.rs b/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/lib.rs index 994a45634d92c..da93f839b322c 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/lib.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/lib.rs @@ -101,7 +101,7 @@ use polkadot_runtime_common::{BlockHashCount, SlowAdjustingFeeUpdate}; #[cfg(feature = "runtime-benchmarks")] use xcm::latest::prelude::{ Asset, Assets as XcmAssets, Fungible, Here, InteriorLocation, Junction, Junction::*, Location, - NetworkId, NonFungible, Parent, ParentThen, Response, WeightLimit, XCM_VERSION, + NetworkId, NonFungible, Parent, ParentThen, Response, WeightLimit, }; use xcm::{ latest::prelude::{AssetId, BodyId}, @@ -117,6 +117,7 @@ use xcm_runtime_apis::{ use frame_support::traits::PalletInfoAccess; use weights::{BlockExecutionWeight, ExtrinsicBaseWeight, RocksDbWeight}; +use xcm_builder::{NetworkExportTable, SovereignPaidRemoteExporter}; impl_opaque_keys! { pub struct SessionKeys { @@ -994,26 +995,39 @@ impl pallet_nfts::Config for Runtime { /// XCM router instance to BridgeHub with bridging capabilities for `Westend` global /// consensus with dynamic fees and back-pressure. -pub type ToWestendXcmRouterInstance = pallet_xcm_bridge_hub_router::Instance3; -impl pallet_xcm_bridge_hub_router::Config for Runtime { - type RuntimeEvent = RuntimeEvent; - type WeightInfo = weights::pallet_xcm_bridge_hub_router::WeightInfo; +pub type ToWestendXcmRouterInstance = pallet_xcm_bridge_router::Instance3; +impl pallet_xcm_bridge_router::Config for Runtime { + type WeightInfo = weights::pallet_xcm_bridge_router::WeightInfo; - type UniversalLocation = xcm_config::UniversalLocation; - type SiblingBridgeHubLocation = xcm_config::bridging::SiblingBridgeHub; - type BridgedNetworkId = xcm_config::bridging::to_westend::WestendNetwork; - type Bridges = xcm_config::bridging::NetworkExportTable; type DestinationVersion = PolkadotXcm; - type BridgeHubOrigin = frame_support::traits::EitherOfDiverse< - EnsureRoot, - EnsureXcm>, + // Let's use `SovereignPaidRemoteExporter`, which sends `ExportMessage` over HRMP to the sibling + // BridgeHub. + type MessageExporter = SovereignPaidRemoteExporter< + // `ExporterFor` wrapper handling dynamic fees for congestion. + pallet_xcm_bridge_router::impls::ViaRemoteBridgeExporter< + Runtime, + ToWestendXcmRouterInstance, + NetworkExportTable, + xcm_config::bridging::to_westend::WestendNetwork, + xcm_config::bridging::SiblingBridgeHub, + >, + XcmpQueue, + xcm_config::UniversalLocation, + >; + + // For congestion - resolves `BridgeId` using the same algorithm as `pallet_xcm_bridge_hub` on + // the BH. + type BridgeIdResolver = pallet_xcm_bridge_router::impls::EnsureIsRemoteBridgeIdResolver< + xcm_config::UniversalLocation, >; - type ToBridgeHubSender = XcmpQueue; - type LocalXcmChannelManager = - cumulus_pallet_xcmp_queue::bridging::InAndOutXcmpChannelStatusProvider; + // For congestion - allow only calls from BH. + type UpdateBridgeStatusOrigin = + AsEnsureOriginWithArg>>; + // For adding message size fees type ByteFee = xcm_config::bridging::XcmBridgeHubRouterByteFee; + // For adding message size fees type FeeAsset = xcm_config::bridging::XcmBridgeHubRouterFeeAssetId; } @@ -1101,7 +1115,7 @@ construct_runtime!( Proxy: pallet_proxy = 42, // Bridge utilities. - ToWestendXcmRouter: pallet_xcm_bridge_hub_router:: = 45, + ToWestendXcmRouter: pallet_xcm_bridge_router:: = 45, // The main stage. Assets: pallet_assets:: = 50, @@ -1330,7 +1344,7 @@ mod benches { [pallet_collator_selection, CollatorSelection] [cumulus_pallet_parachain_system, ParachainSystem] [cumulus_pallet_xcmp_queue, XcmpQueue] - [pallet_xcm_bridge_hub_router, ToWestend] + [pallet_xcm_bridge_router, ToWestend] [pallet_asset_conversion_ops, AssetConversionMigration] // XCM [pallet_xcm, PalletXcmExtrinsicsBenchmark::] @@ -1652,7 +1666,7 @@ impl_runtime_apis! { use frame_system_benchmarking::extensions::Pallet as SystemExtensionsBench; use cumulus_pallet_session_benchmarking::Pallet as SessionBench; use pallet_xcm::benchmarking::Pallet as PalletXcmExtrinsicsBenchmark; - use pallet_xcm_bridge_hub_router::benchmarking::Pallet as XcmBridgeHubRouterBench; + use pallet_xcm_bridge_router::benchmarking::Pallet as XcmBridgeHubRouterBench; // This is defined once again in dispatch_benchmark, because list_benchmarks! // and add_benchmarks! are macros exported by define_benchmarks! macros and those types @@ -1701,7 +1715,7 @@ impl_runtime_apis! { use cumulus_pallet_session_benchmarking::Pallet as SessionBench; impl cumulus_pallet_session_benchmarking::Config for Runtime {} - use pallet_xcm_bridge_hub_router::benchmarking::{ + use pallet_xcm_bridge_router::benchmarking::{ Pallet as XcmBridgeHubRouterBench, Config as XcmBridgeHubRouterConfig, }; @@ -1833,32 +1847,11 @@ impl_runtime_apis! { } impl XcmBridgeHubRouterConfig for Runtime { - fn make_congested() { - cumulus_pallet_xcmp_queue::bridging::suspend_channel_for_benchmarks::( - xcm_config::bridging::SiblingBridgeHubParaId::get().into() - ); - } fn ensure_bridged_target_destination() -> Result { - ParachainSystem::open_outbound_hrmp_channel_for_benchmarks_or_tests( - xcm_config::bridging::SiblingBridgeHubParaId::get().into() - ); - let bridged_asset_hub = xcm_config::bridging::to_westend::AssetHubWestend::get(); - let _ = PolkadotXcm::force_xcm_version( - RuntimeOrigin::root(), - alloc::boxed::Box::new(bridged_asset_hub.clone()), - XCM_VERSION, - ).map_err(|e| { - tracing::error!( - target: "bridges::benchmark", - error=?e, - origin=?RuntimeOrigin::root(), - location=?bridged_asset_hub, - version=?XCM_VERSION, - "Failed to dispatch `force_xcm_version`" - ); - BenchmarkError::Stop("XcmVersion was not stored!") - })?; - Ok(bridged_asset_hub) + Ok(xcm_config::bridging::to_westend::AssetHubWestend::get()) + } + fn update_bridge_status_origin() -> Option { + Some(pallet_xcm::Origin::Xcm(xcm_config::bridging::SiblingBridgeHub::get()).into()) } } diff --git a/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/weights/mod.rs b/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/weights/mod.rs index 0f02c503a6fc6..f7ca4c9e0df95 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/weights/mod.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/weights/mod.rs @@ -41,7 +41,7 @@ pub mod pallet_transaction_payment; pub mod pallet_uniques; pub mod pallet_utility; pub mod pallet_xcm; -pub mod pallet_xcm_bridge_hub_router; +pub mod pallet_xcm_bridge_router; pub mod paritydb_weights; pub mod rocksdb_weights; pub mod xcm; diff --git a/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/weights/pallet_xcm_bridge_hub_router.rs b/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/weights/pallet_xcm_bridge_hub_router.rs deleted file mode 100644 index b18bd6bfcbc75..0000000000000 --- a/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/weights/pallet_xcm_bridge_hub_router.rs +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright (C) Parity Technologies (UK) Ltd. -// SPDX-License-Identifier: Apache-2.0 - -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Autogenerated weights for `pallet_xcm_bridge_hub_router` -//! -//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 32.0.0 -//! DATE: 2025-02-21, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` -//! WORST CASE MAP SIZE: `1000000` -//! HOSTNAME: `ef4134d66388`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz` -//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 - -// Executed Command: -// frame-omni-bencher -// v1 -// benchmark -// pallet -// --extrinsic=* -// --runtime=target/production/wbuild/asset-hub-rococo-runtime/asset_hub_rococo_runtime.wasm -// --pallet=pallet_xcm_bridge_hub_router -// --header=/__w/polkadot-sdk/polkadot-sdk/cumulus/file_header.txt -// --output=./cumulus/parachains/runtimes/assets/asset-hub-rococo/src/weights -// --wasm-execution=compiled -// --steps=50 -// --repeat=20 -// --heap-pages=4096 -// --no-storage-info -// --no-min-squares -// --no-median-slopes - -#![cfg_attr(rustfmt, rustfmt_skip)] -#![allow(unused_parens)] -#![allow(unused_imports)] -#![allow(missing_docs)] - -use frame_support::{traits::Get, weights::Weight}; -use core::marker::PhantomData; - -/// Weight functions for `pallet_xcm_bridge_hub_router`. -pub struct WeightInfo(PhantomData); -impl pallet_xcm_bridge_hub_router::WeightInfo for WeightInfo { - /// Storage: `XcmpQueue::InboundXcmpSuspended` (r:1 w:0) - /// Proof: `XcmpQueue::InboundXcmpSuspended` (`max_values`: Some(1), `max_size`: Some(4002), added: 4497, mode: `MaxEncodedLen`) - /// Storage: `XcmpQueue::OutboundXcmpStatus` (r:1 w:0) - /// Proof: `XcmpQueue::OutboundXcmpStatus` (`max_values`: Some(1), `max_size`: Some(1282), added: 1777, mode: `MaxEncodedLen`) - /// Storage: `ToWestendXcmRouter::Bridge` (r:1 w:1) - /// Proof: `ToWestendXcmRouter::Bridge` (`max_values`: Some(1), `max_size`: Some(17), added: 512, mode: `MaxEncodedLen`) - fn on_initialize_when_non_congested() -> Weight { - // Proof Size summary in bytes: - // Measured: `154` - // Estimated: `5487` - // Minimum execution time: 10_896_000 picoseconds. - Weight::from_parts(11_372_000, 0) - .saturating_add(Weight::from_parts(0, 5487)) - .saturating_add(T::DbWeight::get().reads(3)) - .saturating_add(T::DbWeight::get().writes(1)) - } - /// Storage: `XcmpQueue::InboundXcmpSuspended` (r:1 w:0) - /// Proof: `XcmpQueue::InboundXcmpSuspended` (`max_values`: Some(1), `max_size`: Some(4002), added: 4497, mode: `MaxEncodedLen`) - /// Storage: `XcmpQueue::OutboundXcmpStatus` (r:1 w:0) - /// Proof: `XcmpQueue::OutboundXcmpStatus` (`max_values`: Some(1), `max_size`: Some(1282), added: 1777, mode: `MaxEncodedLen`) - fn on_initialize_when_congested() -> Weight { - // Proof Size summary in bytes: - // Measured: `144` - // Estimated: `5487` - // Minimum execution time: 5_121_000 picoseconds. - Weight::from_parts(5_348_000, 0) - .saturating_add(Weight::from_parts(0, 5487)) - .saturating_add(T::DbWeight::get().reads(2)) - } - /// Storage: `ToWestendXcmRouter::Bridge` (r:1 w:1) - /// Proof: `ToWestendXcmRouter::Bridge` (`max_values`: Some(1), `max_size`: Some(17), added: 512, mode: `MaxEncodedLen`) - fn report_bridge_status() -> Weight { - // Proof Size summary in bytes: - // Measured: `150` - // Estimated: `1502` - // Minimum execution time: 9_451_000 picoseconds. - Weight::from_parts(9_808_000, 0) - .saturating_add(Weight::from_parts(0, 1502)) - .saturating_add(T::DbWeight::get().reads(1)) - .saturating_add(T::DbWeight::get().writes(1)) - } -} diff --git a/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/weights/pallet_xcm_bridge_router.rs b/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/weights/pallet_xcm_bridge_router.rs new file mode 100644 index 0000000000000..e025b3139323e --- /dev/null +++ b/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/weights/pallet_xcm_bridge_router.rs @@ -0,0 +1,62 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Autogenerated weights for `pallet_xcm_bridge_hub_router` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 32.0.0 +//! DATE: 2024-12-07, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `runner-acd6uxux-project-674-concurrent-0`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz` +//! WASM-EXECUTION: `Compiled`, CHAIN: `Some("asset-hub-rococo-dev")`, DB CACHE: 1024 + +// Executed Command: +// target/production/polkadot-parachain +// benchmark +// pallet +// --steps=50 +// --repeat=20 +// --extrinsic=* +// --wasm-execution=compiled +// --heap-pages=4096 +// --json-file=/builds/parity/mirrors/polkadot-sdk/.git/.artifacts/bench.json +// --pallet=pallet_xcm_bridge_hub_router +// --chain=asset-hub-rococo-dev +// --header=./cumulus/file_header.txt +// --output=./cumulus/parachains/runtimes/assets/asset-hub-rococo/src/weights/ + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::Weight}; +use core::marker::PhantomData; + +/// Weight functions for `pallet_xcm_bridge_hub_router`. +pub struct WeightInfo(PhantomData); +impl pallet_xcm_bridge_router::WeightInfo for WeightInfo { + /// Storage: `ToWestendXcmRouter::Bridges` (r:1 w:1) + /// Proof: `ToWestendXcmRouter::Bridges` (`max_values`: None, `max_size`: Some(65), added: 2540, mode: `MaxEncodedLen`) + fn update_bridge_status() -> Weight { + // Proof Size summary in bytes: + // Measured: `109` + // Estimated: `3530` + // Minimum execution time: 12_193_000 picoseconds. + Weight::from_parts(12_658_000, 0) + .saturating_add(Weight::from_parts(0, 3530)) + .saturating_add(T::DbWeight::get().reads(1)) + .saturating_add(T::DbWeight::get().writes(1)) + } +} diff --git a/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/xcm_config.rs b/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/xcm_config.rs index 839e58ca9d5fa..6f4b834e3529e 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/xcm_config.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/xcm_config.rs @@ -532,19 +532,12 @@ pub mod bridging { /// (`AssetId` has to be aligned with `BridgeTable`) pub XcmBridgeHubRouterFeeAssetId: AssetId = TokenLocation::get().into(); - pub BridgeTable: alloc::vec::Vec = - alloc::vec::Vec::new().into_iter() - .chain(to_westend::BridgeTable::get()) - .collect(); - pub EthereumBridgeTable: alloc::vec::Vec = alloc::vec::Vec::new().into_iter() .chain(to_ethereum::BridgeTable::get()) .collect(); } - pub type NetworkExportTable = xcm_builder::NetworkExportTable; - pub type EthereumNetworkExportTable = xcm_builder::NetworkExportTable; pub mod to_westend { diff --git a/cumulus/parachains/runtimes/assets/asset-hub-rococo/tests/tests.rs b/cumulus/parachains/runtimes/assets/asset-hub-rococo/tests/tests.rs index 4a3f629f551bc..9598fbe6ddb2e 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-rococo/tests/tests.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-rococo/tests/tests.rs @@ -907,35 +907,35 @@ mod asset_hub_rococo_tests { } #[test] - fn report_bridge_status_from_xcm_bridge_router_for_westend_works() { - asset_test_utils::test_cases_over_bridge::report_bridge_status_from_xcm_bridge_router_works::< + fn update_bridge_status_from_xcm_bridge_router_for_westend_works() { + asset_test_utils::test_cases_over_bridge::update_bridge_status_from_xcm_bridge_router_works::< Runtime, AllPalletsWithoutSystem, XcmConfig, LocationToAccountId, ToWestendXcmRouterInstance, - >( - collator_session_keys(), - bridging_to_asset_hub_westend, - || bp_asset_hub_rococo::build_congestion_message(Default::default(), true).into(), - || bp_asset_hub_rococo::build_congestion_message(Default::default(), false).into(), - ) + >(collator_session_keys(), bridging_to_asset_hub_westend, |bridge_id, is_congested| { + bp_asset_hub_rococo::build_congestion_message(bridge_id.inner(), is_congested).into() + }) } #[test] fn test_report_bridge_status_call_compatibility() { + let bridge_id = bp_xcm_bridge::BridgeId::new( + &InteriorLocation::from([GlobalConsensus(ByGenesis([0; 32]))]), + &InteriorLocation::from([GlobalConsensus(ByGenesis([1; 32]))]), + ); + // if this test fails, make sure `bp_asset_hub_rococo` has valid encoding assert_eq!( - RuntimeCall::ToWestendXcmRouter( - pallet_xcm_bridge_hub_router::Call::report_bridge_status { - bridge_id: Default::default(), - is_congested: true, - } - ) + RuntimeCall::ToWestendXcmRouter(pallet_xcm_bridge_router::Call::update_bridge_status { + bridge_id, + is_congested: true, + }) .encode(), bp_asset_hub_rococo::Call::ToWestendXcmRouter( - bp_asset_hub_rococo::XcmBridgeHubRouterCall::report_bridge_status { - bridge_id: Default::default(), + bp_asset_hub_rococo::XcmBridgeHubCall::update_bridge_status { + bridge_id: bridge_id.inner(), is_congested: true, } ) @@ -945,10 +945,10 @@ mod asset_hub_rococo_tests { #[test] fn check_sane_weight_report_bridge_status_for_westend() { - use pallet_xcm_bridge_hub_router::WeightInfo; - let actual = >::WeightInfo::report_bridge_status(); + >>::WeightInfo::update_bridge_status(); let max_weight = bp_asset_hub_rococo::XcmBridgeHubRouterTransactCallMaxWeight::get(); assert!( actual.all_lte(max_weight), diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/Cargo.toml b/cumulus/parachains/runtimes/assets/asset-hub-westend/Cargo.toml index 26bf9731690bb..8cc0fb46f371c 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/Cargo.toml +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/Cargo.toml @@ -127,7 +127,7 @@ bp-asset-hub-rococo = { workspace = true } bp-asset-hub-westend = { workspace = true } bp-bridge-hub-rococo = { workspace = true } bp-bridge-hub-westend = { workspace = true } -pallet-xcm-bridge-hub-router = { workspace = true } +pallet-xcm-bridge-router = { workspace = true } snowbridge-outbound-queue-primitives = { workspace = true } snowbridge-pallet-system-frontend = { workspace = true } snowbridge-runtime-common = { workspace = true } @@ -135,6 +135,7 @@ snowbridge-runtime-common = { workspace = true } [dev-dependencies] alloy-core = { workspace = true, features = ["sol-types"] } asset-test-utils = { workspace = true, default-features = true } +bp-xcm-bridge = { workspace = true } pallet-revive-fixtures = { workspace = true, default-features = true } parachains-runtimes-test-utils = { workspace = true, default-features = true } @@ -196,7 +197,7 @@ runtime-benchmarks = [ "pallet-vesting/runtime-benchmarks", "pallet-whitelist/runtime-benchmarks", "pallet-xcm-benchmarks/runtime-benchmarks", - "pallet-xcm-bridge-hub-router/runtime-benchmarks", + "pallet-xcm-bridge-router/runtime-benchmarks", "pallet-xcm/runtime-benchmarks", "parachains-common/runtime-benchmarks", "polkadot-parachain-primitives/runtime-benchmarks", @@ -265,7 +266,7 @@ try-runtime = [ "pallet-utility/try-runtime", "pallet-vesting/try-runtime", "pallet-whitelist/try-runtime", - "pallet-xcm-bridge-hub-router/try-runtime", + "pallet-xcm-bridge-router/try-runtime", "pallet-xcm/try-runtime", "parachain-info/try-runtime", "parachains-common/try-runtime", @@ -281,6 +282,7 @@ std = [ "bp-asset-hub-westend/std", "bp-bridge-hub-rococo/std", "bp-bridge-hub-westend/std", + "bp-xcm-bridge/std", "codec/std", "cumulus-pallet-aura-ext/std", "cumulus-pallet-parachain-system/std", @@ -347,7 +349,7 @@ std = [ "pallet-vesting/std", "pallet-whitelist/std", "pallet-xcm-benchmarks?/std", - "pallet-xcm-bridge-hub-router/std", + "pallet-xcm-bridge-router/std", "pallet-xcm/std", "parachain-info/std", "parachains-common/std", diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/bridge-primitives/Cargo.toml b/cumulus/parachains/runtimes/assets/asset-hub-westend/bridge-primitives/Cargo.toml index f078f8edcb9c8..5c654f52b471b 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/bridge-primitives/Cargo.toml +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/bridge-primitives/Cargo.toml @@ -26,7 +26,7 @@ sp-core = { workspace = true } bp-bridge-hub-cumulus = { workspace = true } bp-messages = { workspace = true } bp-runtime = { workspace = true } -bp-xcm-bridge-hub-router = { workspace = true } +bp-xcm-bridge-router = { workspace = true } testnet-parachains-constants = { features = ["westend"], workspace = true } # Polkadot dependencies @@ -38,7 +38,7 @@ std = [ "bp-bridge-hub-cumulus/std", "bp-messages/std", "bp-runtime/std", - "bp-xcm-bridge-hub-router/std", + "bp-xcm-bridge-router/std", "codec/std", "frame-support/std", "scale-info/std", diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/bridge-primitives/src/lib.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/bridge-primitives/src/lib.rs index 77133c09b530c..c96f1c95905f2 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/bridge-primitives/src/lib.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/bridge-primitives/src/lib.rs @@ -24,7 +24,7 @@ use bp_messages::*; use bp_runtime::{ decl_bridge_finality_runtime_apis, decl_bridge_messages_runtime_apis, Chain, ChainId, Parachain, }; -pub use bp_xcm_bridge_hub_router::XcmBridgeHubRouterCall; +pub use bp_xcm_bridge_router::XcmBridgeHubCall; use codec::{Decode, Encode}; use frame_support::{ dispatch::DispatchClass, @@ -47,7 +47,7 @@ use xcm::latest::prelude::*; pub enum Call { /// `ToRococoXcmRouter` bridge pallet. #[codec(index = 34)] - ToRococoXcmRouter(XcmBridgeHubRouterCall), + ToRococoXcmRouter(XcmBridgeHubCall), } frame_support::parameter_types! { @@ -69,7 +69,7 @@ pub fn build_congestion_message( Transact { origin_kind: OriginKind::Xcm, fallback_max_weight: Some(XcmBridgeHubRouterTransactCallMaxWeight::get()), - call: Call::ToRococoXcmRouter(XcmBridgeHubRouterCall::report_bridge_status { + call: Call::ToRococoXcmRouter(XcmBridgeHubCall::update_bridge_status { bridge_id, is_congested, }) diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs index 2bb5042378397..f4408a8d7084e 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs @@ -116,6 +116,7 @@ use xcm::{ XcmVersion, }, }; +use xcm_builder::{NetworkExportTable, SovereignPaidRemoteExporter}; #[cfg(feature = "runtime-benchmarks")] use frame_support::traits::PalletInfoAccess; @@ -123,7 +124,7 @@ use frame_support::traits::PalletInfoAccess; #[cfg(feature = "runtime-benchmarks")] use xcm::latest::prelude::{ Asset, Assets as XcmAssets, Fungible, Here, InteriorLocation, Junction, Junction::*, Location, - NetworkId, NonFungible, Parent, ParentThen, Response, WeightLimit, XCM_VERSION, + NetworkId, NonFungible, Parent, ParentThen, Response, WeightLimit, }; use xcm_runtime_apis::{ @@ -1135,26 +1136,39 @@ impl pallet_nfts::Config for Runtime { /// XCM router instance to BridgeHub with bridging capabilities for `Rococo` global /// consensus with dynamic fees and back-pressure. -pub type ToRococoXcmRouterInstance = pallet_xcm_bridge_hub_router::Instance1; -impl pallet_xcm_bridge_hub_router::Config for Runtime { - type RuntimeEvent = RuntimeEvent; - type WeightInfo = weights::pallet_xcm_bridge_hub_router::WeightInfo; +pub type ToRococoXcmRouterInstance = pallet_xcm_bridge_router::Instance1; +impl pallet_xcm_bridge_router::Config for Runtime { + type WeightInfo = weights::pallet_xcm_bridge_router::WeightInfo; - type UniversalLocation = xcm_config::UniversalLocation; - type SiblingBridgeHubLocation = xcm_config::bridging::SiblingBridgeHub; - type BridgedNetworkId = xcm_config::bridging::to_rococo::RococoNetwork; - type Bridges = xcm_config::bridging::NetworkExportTable; type DestinationVersion = PolkadotXcm; - type BridgeHubOrigin = frame_support::traits::EitherOfDiverse< - EnsureRoot, - EnsureXcm>, + // Let's use `SovereignPaidRemoteExporter`, which sends `ExportMessage` over HRMP to the sibling + // BridgeHub. + type MessageExporter = SovereignPaidRemoteExporter< + // `ExporterFor` wrapper handling dynamic fees for congestion. + pallet_xcm_bridge_router::impls::ViaRemoteBridgeExporter< + Runtime, + ToRococoXcmRouterInstance, + NetworkExportTable, + xcm_config::bridging::to_rococo::RococoNetwork, + xcm_config::bridging::SiblingBridgeHub, + >, + XcmpQueue, + xcm_config::UniversalLocation, + >; + + // For congestion - resolves `BridgeId` using the same algorithm as `pallet_xcm_bridge_hub` on + // the BH. + type BridgeIdResolver = pallet_xcm_bridge_router::impls::EnsureIsRemoteBridgeIdResolver< + xcm_config::UniversalLocation, >; - type ToBridgeHubSender = XcmpQueue; - type LocalXcmChannelManager = - cumulus_pallet_xcmp_queue::bridging::InAndOutXcmpChannelStatusProvider; + // For congestion - allow only calls from BH. + type UpdateBridgeStatusOrigin = + AsEnsureOriginWithArg>>; + // For adding message size fees type ByteFee = xcm_config::bridging::XcmBridgeHubRouterByteFee; + // For adding message size fees type FeeAsset = xcm_config::bridging::XcmBridgeHubRouterFeeAssetId; } @@ -1315,7 +1329,7 @@ construct_runtime!( PolkadotXcm: pallet_xcm = 31, CumulusXcm: cumulus_pallet_xcm = 32, // Bridge utilities. - ToRococoXcmRouter: pallet_xcm_bridge_hub_router:: = 34, + ToRococoXcmRouter: pallet_xcm_bridge_router:: = 34, MessageQueue: pallet_message_queue = 35, // Snowbridge SnowbridgeSystemFrontend: snowbridge_pallet_system_frontend = 36, @@ -1680,7 +1694,7 @@ mod benches { [pallet_treasury, Treasury] [pallet_vesting, Vesting] [pallet_whitelist, Whitelist] - [pallet_xcm_bridge_hub_router, ToRococo] + [pallet_xcm_bridge_router, ToRococo] [pallet_asset_conversion_ops, AssetConversionMigration] [pallet_revive, Revive] // XCM @@ -2145,7 +2159,7 @@ pallet_revive::impl_runtime_apis_plus_revive!( use frame_system_benchmarking::extensions::Pallet as SystemExtensionsBench; use cumulus_pallet_session_benchmarking::Pallet as SessionBench; use pallet_xcm::benchmarking::Pallet as PalletXcmExtrinsicsBenchmark; - use pallet_xcm_bridge_hub_router::benchmarking::Pallet as XcmBridgeHubRouterBench; + use pallet_xcm_bridge_router::benchmarking::Pallet as XcmBridgeHubRouterBench; // This is defined once again in dispatch_benchmark, because list_benchmarks! // and add_benchmarks! are macros exported by define_benchmarks! macros and those types @@ -2320,38 +2334,17 @@ pallet_revive::impl_runtime_apis_plus_revive!( } } - use pallet_xcm_bridge_hub_router::benchmarking::{ + use pallet_xcm_bridge_router::benchmarking::{ Pallet as XcmBridgeHubRouterBench, Config as XcmBridgeHubRouterConfig, }; impl XcmBridgeHubRouterConfig for Runtime { - fn make_congested() { - cumulus_pallet_xcmp_queue::bridging::suspend_channel_for_benchmarks::( - xcm_config::bridging::SiblingBridgeHubParaId::get().into() - ); - } fn ensure_bridged_target_destination() -> Result { - ParachainSystem::open_outbound_hrmp_channel_for_benchmarks_or_tests( - xcm_config::bridging::SiblingBridgeHubParaId::get().into() - ); - let bridged_asset_hub = xcm_config::bridging::to_rococo::AssetHubRococo::get(); - let _ = PolkadotXcm::force_xcm_version( - RuntimeOrigin::root(), - alloc::boxed::Box::new(bridged_asset_hub.clone()), - XCM_VERSION, - ).map_err(|e| { - tracing::error!( - target: "bridges::benchmark", - error=?e, - origin=?RuntimeOrigin::root(), - location=?bridged_asset_hub, - version=?XCM_VERSION, - "Failed to dispatch `force_xcm_version`" - ); - BenchmarkError::Stop("XcmVersion was not stored!") - })?; - Ok(bridged_asset_hub) + Ok(xcm_config::bridging::to_rococo::AssetHubRococo::get()) + } + fn update_bridge_status_origin() -> Option { + Some(pallet_xcm::Origin::Xcm(xcm_config::bridging::SiblingBridgeHub::get()).into()) } } diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/mod.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/mod.rs index 0559b559ec144..b02954b1ba6f9 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/mod.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/mod.rs @@ -57,7 +57,7 @@ pub mod pallet_utility; pub mod pallet_vesting; pub mod pallet_whitelist; pub mod pallet_xcm; -pub mod pallet_xcm_bridge_hub_router; +pub mod pallet_xcm_bridge_router; pub mod paritydb_weights; pub mod rocksdb_weights; pub mod snowbridge_pallet_system_backend; diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/pallet_xcm_bridge_hub_router.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/pallet_xcm_bridge_hub_router.rs deleted file mode 100644 index b96cb9a285f24..0000000000000 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/pallet_xcm_bridge_hub_router.rs +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright (C) Parity Technologies (UK) Ltd. -// SPDX-License-Identifier: Apache-2.0 - -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Autogenerated weights for `pallet_xcm_bridge_hub_router` -//! -//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 32.0.0 -//! DATE: 2025-02-21, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` -//! WORST CASE MAP SIZE: `1000000` -//! HOSTNAME: `73b9817d6032`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz` -//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 - -// Executed Command: -// frame-omni-bencher -// v1 -// benchmark -// pallet -// --extrinsic=* -// --runtime=target/production/wbuild/asset-hub-westend-runtime/asset_hub_westend_runtime.wasm -// --pallet=pallet_xcm_bridge_hub_router -// --header=/__w/polkadot-sdk/polkadot-sdk/cumulus/file_header.txt -// --output=./cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights -// --wasm-execution=compiled -// --steps=50 -// --repeat=20 -// --heap-pages=4096 -// --no-storage-info -// --no-min-squares -// --no-median-slopes - -#![cfg_attr(rustfmt, rustfmt_skip)] -#![allow(unused_parens)] -#![allow(unused_imports)] -#![allow(missing_docs)] - -use frame_support::{traits::Get, weights::Weight}; -use core::marker::PhantomData; - -/// Weight functions for `pallet_xcm_bridge_hub_router`. -pub struct WeightInfo(PhantomData); -impl pallet_xcm_bridge_hub_router::WeightInfo for WeightInfo { - /// Storage: `XcmpQueue::InboundXcmpSuspended` (r:1 w:0) - /// Proof: `XcmpQueue::InboundXcmpSuspended` (`max_values`: Some(1), `max_size`: Some(4002), added: 4497, mode: `MaxEncodedLen`) - /// Storage: `XcmpQueue::OutboundXcmpStatus` (r:1 w:0) - /// Proof: `XcmpQueue::OutboundXcmpStatus` (`max_values`: Some(1), `max_size`: Some(1282), added: 1777, mode: `MaxEncodedLen`) - /// Storage: `ToRococoXcmRouter::Bridge` (r:1 w:1) - /// Proof: `ToRococoXcmRouter::Bridge` (`max_values`: Some(1), `max_size`: Some(17), added: 512, mode: `MaxEncodedLen`) - fn on_initialize_when_non_congested() -> Weight { - // Proof Size summary in bytes: - // Measured: `259` - // Estimated: `5487` - // Minimum execution time: 11_878_000 picoseconds. - Weight::from_parts(12_382_000, 0) - .saturating_add(Weight::from_parts(0, 5487)) - .saturating_add(T::DbWeight::get().reads(3)) - .saturating_add(T::DbWeight::get().writes(1)) - } - /// Storage: `XcmpQueue::InboundXcmpSuspended` (r:1 w:0) - /// Proof: `XcmpQueue::InboundXcmpSuspended` (`max_values`: Some(1), `max_size`: Some(4002), added: 4497, mode: `MaxEncodedLen`) - /// Storage: `XcmpQueue::OutboundXcmpStatus` (r:1 w:0) - /// Proof: `XcmpQueue::OutboundXcmpStatus` (`max_values`: Some(1), `max_size`: Some(1282), added: 1777, mode: `MaxEncodedLen`) - fn on_initialize_when_congested() -> Weight { - // Proof Size summary in bytes: - // Measured: `144` - // Estimated: `5487` - // Minimum execution time: 5_094_000 picoseconds. - Weight::from_parts(5_316_000, 0) - .saturating_add(Weight::from_parts(0, 5487)) - .saturating_add(T::DbWeight::get().reads(2)) - } - /// Storage: `ToRococoXcmRouter::Bridge` (r:1 w:1) - /// Proof: `ToRococoXcmRouter::Bridge` (`max_values`: Some(1), `max_size`: Some(17), added: 512, mode: `MaxEncodedLen`) - fn report_bridge_status() -> Weight { - // Proof Size summary in bytes: - // Measured: `150` - // Estimated: `1502` - // Minimum execution time: 9_591_000 picoseconds. - Weight::from_parts(10_157_000, 0) - .saturating_add(Weight::from_parts(0, 1502)) - .saturating_add(T::DbWeight::get().reads(1)) - .saturating_add(T::DbWeight::get().writes(1)) - } -} diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/pallet_xcm_bridge_router.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/pallet_xcm_bridge_router.rs new file mode 100644 index 0000000000000..9e19594572850 --- /dev/null +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/pallet_xcm_bridge_router.rs @@ -0,0 +1,62 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Autogenerated weights for `pallet_xcm_bridge_hub_router` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 32.0.0 +//! DATE: 2024-12-07, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `runner-acd6uxux-project-674-concurrent-0`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz` +//! WASM-EXECUTION: `Compiled`, CHAIN: `Some("asset-hub-westend-dev")`, DB CACHE: 1024 + +// Executed Command: +// target/production/polkadot-parachain +// benchmark +// pallet +// --steps=50 +// --repeat=20 +// --extrinsic=* +// --wasm-execution=compiled +// --heap-pages=4096 +// --json-file=/builds/parity/mirrors/polkadot-sdk/.git/.artifacts/bench.json +// --pallet=pallet_xcm_bridge_hub_router +// --chain=asset-hub-westend-dev +// --header=./cumulus/file_header.txt +// --output=./cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/ + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::Weight}; +use core::marker::PhantomData; + +/// Weight functions for `pallet_xcm_bridge_hub_router`. +pub struct WeightInfo(PhantomData); +impl pallet_xcm_bridge_router::WeightInfo for WeightInfo { + /// Storage: `ToRococoXcmRouter::Bridges` (r:1 w:1) + /// Proof: `ToRococoXcmRouter::Bridges` (`max_values`: None, `max_size`: Some(65), added: 2540, mode: `MaxEncodedLen`) + fn update_bridge_status() -> Weight { + // Proof Size summary in bytes: + // Measured: `109` + // Estimated: `3530` + // Minimum execution time: 10_800_000 picoseconds. + Weight::from_parts(11_171_000, 0) + .saturating_add(Weight::from_parts(0, 3530)) + .saturating_add(T::DbWeight::get().reads(1)) + .saturating_add(T::DbWeight::get().writes(1)) + } +} diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/xcm_config.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/xcm_config.rs index 9e6ab673b2993..34412d3ad1729 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/xcm_config.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/xcm_config.rs @@ -632,15 +632,8 @@ pub mod bridging { /// Router expects payment with this `AssetId`. /// (`AssetId` has to be aligned with `BridgeTable`) pub XcmBridgeHubRouterFeeAssetId: AssetId = WestendLocation::get().into(); - - pub BridgeTable: alloc::vec::Vec = - alloc::vec::Vec::new().into_iter() - .chain(to_rococo::BridgeTable::get()) - .collect(); } - pub type NetworkExportTable = xcm_builder::NetworkExportTable; - pub mod to_rococo { use super::*; diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/tests/tests.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/tests/tests.rs index f60b8cb027a72..f22cc74de5381 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/tests/tests.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/tests/tests.rs @@ -1021,33 +1021,35 @@ fn receive_reserve_asset_deposited_roc_from_asset_hub_rococo_fees_paid_by_suffic } #[test] -fn report_bridge_status_from_xcm_bridge_router_for_rococo_works() { - asset_test_utils::test_cases_over_bridge::report_bridge_status_from_xcm_bridge_router_works::< +fn update_bridge_status_from_xcm_bridge_router_for_rococo_works() { + asset_test_utils::test_cases_over_bridge::update_bridge_status_from_xcm_bridge_router_works::< Runtime, AllPalletsWithoutSystem, XcmConfig, LocationToAccountId, ToRococoXcmRouterInstance, - >( - collator_session_keys(), - bridging_to_asset_hub_rococo, - || bp_asset_hub_westend::build_congestion_message(Default::default(), true).into(), - || bp_asset_hub_westend::build_congestion_message(Default::default(), false).into(), - ) + >(collator_session_keys(), bridging_to_asset_hub_rococo, |bridge_id, is_congested| { + bp_asset_hub_westend::build_congestion_message(bridge_id.inner(), is_congested).into() + }) } #[test] fn test_report_bridge_status_call_compatibility() { + let bridge_id = bp_xcm_bridge::BridgeId::new( + &InteriorLocation::from([GlobalConsensus(ByGenesis([0; 32]))]), + &InteriorLocation::from([GlobalConsensus(ByGenesis([1; 32]))]), + ); + // if this test fails, make sure `bp_asset_hub_rococo` has valid encoding assert_eq!( - RuntimeCall::ToRococoXcmRouter(pallet_xcm_bridge_hub_router::Call::report_bridge_status { - bridge_id: Default::default(), + RuntimeCall::ToRococoXcmRouter(pallet_xcm_bridge_router::Call::update_bridge_status { + bridge_id, is_congested: true, }) .encode(), bp_asset_hub_westend::Call::ToRococoXcmRouter( - bp_asset_hub_westend::XcmBridgeHubRouterCall::report_bridge_status { - bridge_id: Default::default(), + bp_asset_hub_westend::XcmBridgeHubCall::update_bridge_status { + bridge_id: bridge_id.inner(), is_congested: true, } ) @@ -1057,10 +1059,10 @@ fn test_report_bridge_status_call_compatibility() { #[test] fn check_sane_weight_report_bridge_status() { - use pallet_xcm_bridge_hub_router::WeightInfo; - let actual = >::WeightInfo::report_bridge_status(); + >>::WeightInfo::update_bridge_status(); let max_weight = bp_asset_hub_westend::XcmBridgeHubRouterTransactCallMaxWeight::get(); assert!( actual.all_lte(max_weight), diff --git a/cumulus/parachains/runtimes/assets/test-utils/Cargo.toml b/cumulus/parachains/runtimes/assets/test-utils/Cargo.toml index a4c83ea39890f..4d61085de60e3 100644 --- a/cumulus/parachains/runtimes/assets/test-utils/Cargo.toml +++ b/cumulus/parachains/runtimes/assets/test-utils/Cargo.toml @@ -43,6 +43,7 @@ xcm-runtime-apis = { workspace = true } # Bridges pallet-xcm-bridge-hub-router = { workspace = true } +pallet-xcm-bridge-router = { workspace = true } [features] default = ["std"] @@ -60,6 +61,7 @@ std = [ "pallet-session/std", "pallet-timestamp/std", "pallet-xcm-bridge-hub-router/std", + "pallet-xcm-bridge-router/std", "pallet-xcm/std", "parachain-info/std", "parachains-common/std", diff --git a/cumulus/parachains/runtimes/assets/test-utils/src/test_cases_over_bridge.rs b/cumulus/parachains/runtimes/assets/test-utils/src/test_cases_over_bridge.rs index 9b05f2d46dfb5..0dc64e59e3cfa 100644 --- a/cumulus/parachains/runtimes/assets/test-utils/src/test_cases_over_bridge.rs +++ b/cumulus/parachains/runtimes/assets/test-utils/src/test_cases_over_bridge.rs @@ -558,3 +558,100 @@ pub fn report_bridge_status_from_xcm_bridge_router_works< report_bridge_status(false); }) } + +pub fn update_bridge_status_from_xcm_bridge_router_works< + Runtime, + AllPalletsWithoutSystem, + XcmConfig, + LocationToAccountId, + XcmBridgeHubRouterInstance, +>( + collator_session_keys: CollatorSessionKeys, + prepare_configuration: fn() -> TestBridgingConfig, + congestion_message: fn( + pallet_xcm_bridge_router::BridgeIdOf, + bool, + ) -> Xcm, +) where + Runtime: frame_system::Config + + pallet_balances::Config + + pallet_session::Config + + pallet_xcm::Config + + parachain_info::Config + + pallet_collator_selection::Config + + cumulus_pallet_parachain_system::Config + + cumulus_pallet_xcmp_queue::Config + + pallet_xcm_bridge_router::Config + + pallet_timestamp::Config, + AllPalletsWithoutSystem: + OnInitialize> + OnFinalize>, + AccountIdOf: Into<[u8; 32]>, + ValidatorIdOf: From>, + BalanceOf: From, + ::Balance: From + Into, + XcmConfig: xcm_executor::Config, + LocationToAccountId: ConvertLocation>, + ::AccountId: + Into<<::RuntimeOrigin as OriginTrait>::AccountId>, + <::Lookup as StaticLookup>::Source: + From<::AccountId>, + ::AccountId: From, + XcmBridgeHubRouterInstance: 'static, +{ + ExtBuilder::::default() + .with_collators(collator_session_keys.collators()) + .with_session_keys(collator_session_keys.session_keys()) + .with_tracing() + .build() + .execute_with(|| { + let update_bridge_status = |is_congested: bool| { + // prepare bridge config + let TestBridgingConfig { + local_bridge_hub_location, bridged_target_location, .. + } = prepare_configuration(); + + use pallet_xcm_bridge_router::ResolveBridgeId; + let bridge_id = <>::BridgeIdResolver>::resolve_for_dest( + &bridged_target_location + ) + .expect("resolved BridgeId"); + + // check before + let bridge_state = pallet_xcm_bridge_router::Bridges::< + Runtime, + XcmBridgeHubRouterInstance, + >::get(&bridge_id); + let is_congested_before = bridge_state.is_congested; + + // Call received XCM execution + let xcm = congestion_message(bridge_id.clone(), is_congested); + let mut hash = xcm.using_encoded(sp_io::hashing::blake2_256); + + // execute xcm as XcmpQueue would do + let outcome = XcmExecutor::::prepare_and_execute( + local_bridge_hub_location.clone(), + xcm, + &mut hash, + RuntimeHelper::::xcm_max_weight( + XcmReceivedFrom::Sibling, + ), + Weight::zero(), + ); + assert_ok!(outcome.ensure_complete()); + + // check after + let bridge_state = pallet_xcm_bridge_router::Bridges::< + Runtime, + XcmBridgeHubRouterInstance, + >::get(&bridge_id); + let is_congested_after = bridge_state.is_congested; + assert_eq!(is_congested_after, is_congested); + assert_ne!(is_congested_after, is_congested_before,); + }; + + update_bridge_status(true); + update_bridge_status(false); + }) +} diff --git a/cumulus/parachains/runtimes/bridge-hubs/test-utils/Cargo.toml b/cumulus/parachains/runtimes/bridge-hubs/test-utils/Cargo.toml index 0e5084e5daec5..341fa00e7e722 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/test-utils/Cargo.toml +++ b/cumulus/parachains/runtimes/bridge-hubs/test-utils/Cargo.toml @@ -54,6 +54,8 @@ pallet-bridge-grandpa = { workspace = true } pallet-bridge-messages = { features = ["test-helpers"], workspace = true } pallet-bridge-parachains = { workspace = true } pallet-bridge-relayers = { workspace = true } + +pallet-xcm-bridge = { workspace = true } pallet-xcm-bridge-hub = { workspace = true } [features] @@ -80,6 +82,7 @@ std = [ "pallet-timestamp/std", "pallet-utility/std", "pallet-xcm-bridge-hub/std", + "pallet-xcm-bridge/std", "pallet-xcm/std", "parachains-common/std", "parachains-runtimes-test-utils/std", diff --git a/cumulus/parachains/runtimes/bridge-hubs/test-utils/src/lib.rs b/cumulus/parachains/runtimes/bridge-hubs/test-utils/src/lib.rs index 196ed18ad5e41..437f307090fd3 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/test-utils/src/lib.rs +++ b/cumulus/parachains/runtimes/bridge-hubs/test-utils/src/lib.rs @@ -24,8 +24,13 @@ extern crate alloc; pub use bp_test_utils::test_header; pub use parachains_runtimes_test_utils::*; use sp_runtime::Perbill; -pub use test_cases::helpers::for_pallet_xcm_bridge_hub::{ - ensure_opened_bridge, open_bridge_with_extrinsic, open_bridge_with_storage, +pub use test_cases::helpers::{ + for_pallet_xcm_bridge::{ + ensure_opened_xcm_bridge, open_xcm_bridge_with_extrinsic, open_xcm_bridge_with_storage, + }, + for_pallet_xcm_bridge_hub::{ + ensure_opened_bridge, open_bridge_with_extrinsic, open_bridge_with_storage, + }, }; /// A helper function for comparing the actual value of a fee constant with its estimated value. The diff --git a/cumulus/parachains/runtimes/bridge-hubs/test-utils/src/test_cases/helpers.rs b/cumulus/parachains/runtimes/bridge-hubs/test-utils/src/test_cases/helpers.rs index 5460a9286b2f6..9b7238f8fc2f6 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/test-utils/src/test_cases/helpers.rs +++ b/cumulus/parachains/runtimes/bridge-hubs/test-utils/src/test_cases/helpers.rs @@ -593,3 +593,214 @@ pub(crate) mod for_pallet_xcm_bridge_hub { ); } } + +pub(crate) mod for_pallet_xcm_bridge { + use super::{super::for_pallet_xcm_bridge::*, *}; + + /// Helper function to open the bridge/lane for `source` and `destination` while ensuring all + /// required balances are placed into the SA of the source. + pub fn ensure_opened_xcm_bridge< + Runtime, + XcmOverBridgePalletInstance, + LocationToAccountId, + TokenLocation> + (source: Location, destination: InteriorLocation, is_paid_xcm_execution: bool, bridge_opener: impl Fn(pallet_xcm_bridge::BridgeLocations, Option)) -> (pallet_xcm_bridge::BridgeLocations, pallet_xcm_bridge::LaneIdOf) + where + Runtime: BasicParachainRuntime + BridgeXcmOverBridgeConfig, + XcmOverBridgePalletInstance: 'static, + ::RuntimeCall: GetDispatchInfo + From>, + ::Balance: From<<>::BridgeMessagesPalletInstance>>::ThisChain as bp_runtime::Chain>::Balance>, + ::Balance: From, + LocationToAccountId: ConvertLocation>, + TokenLocation: Get + { + // construct expected bridge configuration + let locations = + pallet_xcm_bridge::Pallet::::bridge_locations( + source.clone().into(), + destination.clone().into(), + ) + .expect("valid bridge locations"); + assert!(pallet_xcm_bridge::Bridges::::get( + locations.bridge_id() + ) + .is_none()); + + // SA of source location needs to have some required balance + if !>::AllowWithoutBridgeDeposit::contains(&source) { + // required balance: ED + fee + BridgeDeposit + let bridge_deposit = + >::BridgeDeposit::get(); + let balance_needed = ::ExistentialDeposit::get() + bridge_deposit.into(); + + let source_account_id = LocationToAccountId::convert_location(&source).expect("valid location"); + let _ = >::mint_into(&source_account_id, balance_needed) + .expect("mint_into passes"); + }; + + let maybe_paid_execution = if is_paid_xcm_execution { + // random high enough value for `BuyExecution` fees + let buy_execution_fee_amount = 5_000_000_000_000_u128; + let buy_execution_fee = (TokenLocation::get(), buy_execution_fee_amount).into(); + + let balance_needed = ::ExistentialDeposit::get() + + buy_execution_fee_amount.into(); + let source_account_id = + LocationToAccountId::convert_location(&source).expect("valid location"); + let _ = + >::mint_into(&source_account_id, balance_needed) + .expect("mint_into passes"); + Some(buy_execution_fee) + } else { + None + }; + + // call the bridge opener + bridge_opener(*locations.clone(), maybe_paid_execution); + + // check opened bridge + let bridge = pallet_xcm_bridge::Bridges::::get( + locations.bridge_id(), + ) + .expect("opened bridge"); + + // check state + assert_ok!( + pallet_xcm_bridge::Pallet::::do_try_state() + ); + + // return locations + (*locations, bridge.lane_id) + } + + /// Utility for opening bridge with dedicated `pallet_xcm_bridge_hub`'s extrinsic. + pub fn open_xcm_bridge_with_extrinsic( + (origin, origin_kind): (Location, OriginKind), + bridge_destination_universal_location: InteriorLocation, + maybe_paid_execution: Option, + ) where + Runtime: frame_system::Config + + pallet_xcm_bridge::Config + + cumulus_pallet_parachain_system::Config + + pallet_xcm::Config, + XcmOverBridgePalletInstance: 'static, + ::RuntimeCall: + GetDispatchInfo + From>, + { + // open bridge with `Transact` call + let open_bridge_call = RuntimeCallOf::::from(BridgeXcmOverBridgeCall::< + Runtime, + XcmOverBridgePalletInstance, + >::open_bridge { + bridge_destination_universal_location: Box::new( + bridge_destination_universal_location.clone().into(), + ), + maybe_notify: None, + }); + + // execute XCM as source origin would do with `Transact -> Origin::Xcm` + assert_ok!(RuntimeHelper::::execute_as_origin( + (origin, origin_kind), + open_bridge_call, + maybe_paid_execution + ) + .ensure_complete()); + } + + /// Utility for opening bridge directly inserting data to the `pallet_xcm_bridge_hub`'s storage + /// (used only for legacy purposes). + pub fn open_xcm_bridge_with_storage( + locations: pallet_xcm_bridge::BridgeLocations, + lane_id: pallet_xcm_bridge::LaneIdOf, + maybe_notify: Option, + ) where + Runtime: pallet_xcm_bridge::Config, + XcmOverBridgePalletInstance: 'static, + { + // insert bridge data directly to the storage + assert_ok!( + pallet_xcm_bridge::Pallet::::do_open_bridge( + Box::new(locations), + lane_id, + true, + maybe_notify, + ) + ); + } + + /// Helper function to close the bridge/lane for `source` and `destination`. + pub fn close_xcm_bridge( + expected_source: Location, + bridge_destination_universal_location: InteriorLocation, + (origin, origin_kind): (Location, OriginKind), + is_paid_xcm_execution: bool + ) where + Runtime: BasicParachainRuntime + BridgeXcmOverBridgeConfig, + XcmOverBridgePalletInstance: 'static, + ::RuntimeCall: GetDispatchInfo + From>, + ::Balance: From<<>::BridgeMessagesPalletInstance>>::ThisChain as bp_runtime::Chain>::Balance>, + ::Balance: From, + LocationToAccountId: ConvertLocation>, + TokenLocation: Get + { + // construct expected bridge configuration + let locations = + pallet_xcm_bridge::Pallet::::bridge_locations( + expected_source.clone().into(), + bridge_destination_universal_location.clone().into(), + ) + .expect("valid bridge locations"); + assert!(pallet_xcm_bridge::Bridges::::get( + locations.bridge_id() + ) + .is_some()); + + // required balance: ED + fee + BridgeDeposit + let maybe_paid_execution = if is_paid_xcm_execution { + // random high enough value for `BuyExecution` fees + let buy_execution_fee_amount = 2_500_000_000_000_u128; + let buy_execution_fee = (TokenLocation::get(), buy_execution_fee_amount).into(); + + let balance_needed = ::ExistentialDeposit::get() + + buy_execution_fee_amount.into(); + let source_account_id = + LocationToAccountId::convert_location(&expected_source).expect("valid location"); + let _ = + >::mint_into(&source_account_id, balance_needed) + .expect("mint_into passes"); + Some(buy_execution_fee) + } else { + None + }; + + // close bridge with `Transact` call + let close_bridge_call = RuntimeCallOf::::from(BridgeXcmOverBridgeCall::< + Runtime, + XcmOverBridgePalletInstance, + >::close_bridge { + bridge_destination_universal_location: Box::new( + bridge_destination_universal_location.into(), + ), + may_prune_messages: 16, + }); + + // execute XCM as source origin would do with `Transact -> Origin::Xcm` + assert_ok!(RuntimeHelper::::execute_as_origin( + (origin, origin_kind), + close_bridge_call, + maybe_paid_execution + ) + .ensure_complete()); + + // bridge is closed + assert!(pallet_xcm_bridge::Bridges::::get( + locations.bridge_id() + ) + .is_none()); + + // check state + assert_ok!( + pallet_xcm_bridge::Pallet::::do_try_state() + ); + } +} diff --git a/cumulus/parachains/runtimes/bridge-hubs/test-utils/src/test_cases/mod.rs b/cumulus/parachains/runtimes/bridge-hubs/test-utils/src/test_cases/mod.rs index ce2b78990dce1..b17e853b3505b 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/test-utils/src/test_cases/mod.rs +++ b/cumulus/parachains/runtimes/bridge-hubs/test-utils/src/test_cases/mod.rs @@ -65,6 +65,7 @@ pub(crate) mod bridges_prelude { } // Re-export test-case +pub use for_pallet_xcm_bridge::open_and_close_xcm_bridge_works; pub use for_pallet_xcm_bridge_hub::open_and_close_bridge_works; // Re-export test_case from assets @@ -806,3 +807,154 @@ pub(crate) mod for_pallet_xcm_bridge_hub { }); } } + +pub(crate) mod for_pallet_xcm_bridge { + use super::*; + use crate::test_cases::helpers::for_pallet_xcm_bridge::{ + close_xcm_bridge, ensure_opened_xcm_bridge, open_xcm_bridge_with_extrinsic, + }; + pub(crate) use pallet_xcm_bridge::{ + Bridge, BridgeState, Call as BridgeXcmOverBridgeCall, Config as BridgeXcmOverBridgeConfig, + LanesManagerOf, + }; + + /// Test-case makes sure that `Runtime` can open/close bridges. + pub fn open_and_close_xcm_bridge_works( + collator_session_key: CollatorSessionKeys, + runtime_para_id: u32, + expected_source: Location, + destination: InteriorLocation, + origin_with_origin_kind: (Location, OriginKind), + is_paid_xcm_execution: bool, + ) where + Runtime: BasicParachainRuntime + BridgeXcmOverBridgeConfig, + XcmOverBridgePalletInstance: 'static, + ::RuntimeCall: GetDispatchInfo + From>, + ::Balance: From<<>::BridgeMessagesPalletInstance>>::ThisChain as bp_runtime::Chain>::Balance>, + ::Balance: From, + <>::BridgeMessagesPalletInstance>>::ThisChain as bp_runtime::Chain>::AccountId: From<::AccountId>, + LocationToAccountId: ConvertLocation>, + TokenLocation: Get, + { + run_test::(collator_session_key, runtime_para_id, vec![], || { + // construct expected bridge configuration + let locations = pallet_xcm_bridge::Pallet::::bridge_locations( + expected_source.clone().into(), + destination.clone().into(), + ).expect("valid bridge locations"); + let expected_lane_id = + locations.calculate_lane_id(xcm::latest::VERSION).expect("valid laneId"); + let lanes_manager = LanesManagerOf::::new(); + + let expected_deposit = if >::AllowWithoutBridgeDeposit::contains( + locations.bridge_origin_relative_location() + ) { + None + } else { + let bridge_owner_account = LocationToAccountId::convert_location(&expected_source) + .expect("valid location") + .into(); + let deposit = >::BridgeDeposit::get(); + + Some(pallet_xcm_bridge::Deposit::new(bridge_owner_account, deposit)) + }; + + // check bridge/lane DOES not exist + assert_eq!( + pallet_xcm_bridge::Bridges::::get( + locations.bridge_id() + ), + None + ); + assert_eq!( + lanes_manager.active_inbound_lane(expected_lane_id).map(drop), + Err(LanesManagerError::UnknownInboundLane) + ); + assert_eq!( + lanes_manager.active_outbound_lane(expected_lane_id).map(drop), + Err(LanesManagerError::UnknownOutboundLane) + ); + + // open bridge with Transact call + assert_eq!( + ensure_opened_xcm_bridge::< + Runtime, + XcmOverBridgePalletInstance, + LocationToAccountId, + TokenLocation, + >( + expected_source.clone(), + destination.clone(), + is_paid_xcm_execution, + |locations, maybe_paid_execution| open_xcm_bridge_with_extrinsic::< + Runtime, + XcmOverBridgePalletInstance, + >( + origin_with_origin_kind.clone(), + locations.bridge_destination_universal_location().clone(), + maybe_paid_execution + ) + ) + .0 + .bridge_id(), + locations.bridge_id() + ); + + // check bridge/lane DOES exist + assert_eq!( + pallet_xcm_bridge::Bridges::::get( + locations.bridge_id() + ), + Some(Bridge { + bridge_origin_relative_location: Box::new(expected_source.clone().into()), + bridge_origin_universal_location: Box::new( + locations.bridge_origin_universal_location().clone().into() + ), + bridge_destination_universal_location: Box::new( + locations.bridge_destination_universal_location().clone().into() + ), + state: BridgeState::Opened, + deposit: expected_deposit, + lane_id: expected_lane_id, + maybe_notify: None, + }) + ); + assert_eq!( + lanes_manager.active_inbound_lane(expected_lane_id).map(|lane| lane.state()), + Ok(LaneState::Opened) + ); + assert_eq!( + lanes_manager.active_outbound_lane(expected_lane_id).map(|lane| lane.state()), + Ok(LaneState::Opened) + ); + + // close bridge with Transact call + close_xcm_bridge::< + Runtime, + XcmOverBridgePalletInstance, + LocationToAccountId, + TokenLocation, + >(expected_source, destination, origin_with_origin_kind, is_paid_xcm_execution); + + // check bridge/lane DOES not exist + assert_eq!( + pallet_xcm_bridge::Bridges::::get( + locations.bridge_id() + ), + None + ); + assert_eq!( + lanes_manager.active_inbound_lane(expected_lane_id).map(drop), + Err(LanesManagerError::UnknownInboundLane) + ); + assert_eq!( + lanes_manager.active_outbound_lane(expected_lane_id).map(drop), + Err(LanesManagerError::UnknownOutboundLane) + ); + }); + } +} diff --git a/prdoc/pr_6231.prdoc b/prdoc/pr_6231.prdoc new file mode 100644 index 0000000000000..25b575687ec00 --- /dev/null +++ b/prdoc/pr_6231.prdoc @@ -0,0 +1,41 @@ +title: Bridges - Add improved congestion control mechanism +doc: +- audience: Runtime Dev + description: |- + This release introduces an enhanced bridge congestion mechanism, decoupling bridge queues from XCMP and improving suspension handling with bridge-specific channel control. + Using `BridgeId`, `pallet-xcm-bridge-hub` and `pallet-xcm-bridge-hub-router` now support selective suspension and resumption of individual bridges. + A new congestion detection feature in `pallet-xcm-bridge-hub` triggers `fn suspend_bridge` and `fn resume_bridge` callbacks: + if the router is on a sibling chain, it sends `xcm::Transact(update_bridge_status(bridge_id, is_congested))` using stored callback data, while on local chains, it directly manages state. + Additionally, a new `stop_threshold` in `pallet-xcm-bridge-hub` enables or disables `ExportXcm::validate`, acting as a safeguard if the router ignores the `suspend` signal. + Finally, `pallet-xcm-bridge-hub-router` now flexibly supports message routing to either sibling chains (`ExportMessage`) or local deployments (`ExportXcm`), + enhancing modularity and compatibility across deployment setups. + +crates: +- name: asset-hub-rococo-runtime + bump: major +- name: asset-hub-westend-runtime + bump: major +- name: asset-test-utils + bump: minor +- name: cumulus-pallet-xcmp-queue + bump: minor +- name: bridge-hub-test-utils + bump: minor +- name: emulated-integration-tests-common + bump: minor +- name: bp-asset-hub-rococo + bump: minor +- name: bp-asset-hub-westend + bump: minor +- name: polkadot-sdk + bump: minor +- name: pallet-bridge-messages + bump: patch +- name: bp-xcm-bridge + bump: patch +- name: pallet-xcm-bridge + bump: patch +- name: pallet-xcm-bridge-router + bump: patch +- name: bp-xcm-bridge-router + bump: patch \ No newline at end of file diff --git a/umbrella/Cargo.toml b/umbrella/Cargo.toml index 4b91a80784a24..fe9722e3ca28f 100644 --- a/umbrella/Cargo.toml +++ b/umbrella/Cargo.toml @@ -19,6 +19,8 @@ std = [ "bp-test-utils?/std", "bp-xcm-bridge-hub-router?/std", "bp-xcm-bridge-hub?/std", + "bp-xcm-bridge-router?/std", + "bp-xcm-bridge?/std", "bridge-hub-common?/std", "bridge-hub-test-utils?/std", "bridge-runtime-common?/std", @@ -164,6 +166,8 @@ std = [ "pallet-xcm-benchmarks?/std", "pallet-xcm-bridge-hub-router?/std", "pallet-xcm-bridge-hub?/std", + "pallet-xcm-bridge-router?/std", + "pallet-xcm-bridge?/std", "pallet-xcm?/std", "parachains-common?/std", "parachains-runtimes-test-utils?/std", @@ -339,6 +343,8 @@ runtime-benchmarks = [ "pallet-xcm-benchmarks?/runtime-benchmarks", "pallet-xcm-bridge-hub-router?/runtime-benchmarks", "pallet-xcm-bridge-hub?/runtime-benchmarks", + "pallet-xcm-bridge-router?/runtime-benchmarks", + "pallet-xcm-bridge?/runtime-benchmarks", "pallet-xcm?/runtime-benchmarks", "parachains-common?/runtime-benchmarks", "polkadot-cli?/runtime-benchmarks", @@ -476,6 +482,8 @@ try-runtime = [ "pallet-whitelist?/try-runtime", "pallet-xcm-bridge-hub-router?/try-runtime", "pallet-xcm-bridge-hub?/try-runtime", + "pallet-xcm-bridge-router?/try-runtime", + "pallet-xcm-bridge?/try-runtime", "pallet-xcm?/try-runtime", "parachains-common?/try-runtime", "polkadot-cli?/try-runtime", @@ -547,8 +555,10 @@ runtime-full = [ "bp-relayers", "bp-runtime", "bp-test-utils", + "bp-xcm-bridge", "bp-xcm-bridge-hub", "bp-xcm-bridge-hub-router", + "bp-xcm-bridge-router", "bridge-hub-common", "bridge-runtime-common", "cumulus-pallet-aura-ext", @@ -698,8 +708,10 @@ runtime-full = [ "pallet-whitelist", "pallet-xcm", "pallet-xcm-benchmarks", + "pallet-xcm-bridge", "pallet-xcm-bridge-hub", "pallet-xcm-bridge-hub-router", + "pallet-xcm-bridge-router", "parachains-common", "polkadot-core-primitives", "polkadot-parachain-primitives", @@ -1042,6 +1054,11 @@ default-features = false optional = true path = "../bridges/primitives/test-utils" +[dependencies.bp-xcm-bridge] +default-features = false +optional = true +path = "../bridges/primitives/xcm-bridge" + [dependencies.bp-xcm-bridge-hub] default-features = false optional = true @@ -1052,6 +1069,11 @@ default-features = false optional = true path = "../bridges/primitives/xcm-bridge-hub-router" +[dependencies.bp-xcm-bridge-router] +default-features = false +optional = true +path = "../bridges/primitives/xcm-bridge-router" + [dependencies.bridge-hub-common] default-features = false optional = true @@ -1797,6 +1819,11 @@ default-features = false optional = true path = "../polkadot/xcm/pallet-xcm-benchmarks" +[dependencies.pallet-xcm-bridge] +default-features = false +optional = true +path = "../bridges/modules/xcm-bridge" + [dependencies.pallet-xcm-bridge-hub] default-features = false optional = true @@ -1807,6 +1834,11 @@ default-features = false optional = true path = "../bridges/modules/xcm-bridge-hub-router" +[dependencies.pallet-xcm-bridge-router] +default-features = false +optional = true +path = "../bridges/modules/xcm-bridge-router" + [dependencies.parachains-common] default-features = false optional = true diff --git a/umbrella/src/lib.rs b/umbrella/src/lib.rs index 641e33500a70a..f14128d4962c6 100644 --- a/umbrella/src/lib.rs +++ b/umbrella/src/lib.rs @@ -51,6 +51,10 @@ pub use bp_runtime; #[cfg(feature = "bp-test-utils")] pub use bp_test_utils; +/// Primitives of the xcm-bridge-hub pallet. +#[cfg(feature = "bp-xcm-bridge")] +pub use bp_xcm_bridge; + /// Primitives of the xcm-bridge-hub pallet. #[cfg(feature = "bp-xcm-bridge-hub")] pub use bp_xcm_bridge_hub; @@ -59,6 +63,10 @@ pub use bp_xcm_bridge_hub; #[cfg(feature = "bp-xcm-bridge-hub-router")] pub use bp_xcm_bridge_hub_router; +/// Primitives of the xcm-bridge-hub fee pallet. +#[cfg(feature = "bp-xcm-bridge-router")] +pub use bp_xcm_bridge_router; + /// Bridge hub common utilities. #[cfg(feature = "bridge-hub-common")] pub use bridge_hub_common; @@ -783,6 +791,10 @@ pub use pallet_xcm; #[cfg(feature = "pallet-xcm-benchmarks")] pub use pallet_xcm_benchmarks; +/// Module that adds dynamic bridges/lanes support to XCM infrastructure at the bridge hub. +#[cfg(feature = "pallet-xcm-bridge")] +pub use pallet_xcm_bridge; + /// Module that adds dynamic bridges/lanes support to XCM infrastructure at the bridge hub. #[cfg(feature = "pallet-xcm-bridge-hub")] pub use pallet_xcm_bridge_hub; @@ -791,6 +803,10 @@ pub use pallet_xcm_bridge_hub; #[cfg(feature = "pallet-xcm-bridge-hub-router")] pub use pallet_xcm_bridge_hub_router; +/// Bridge hub interface for sibling/parent chains with dynamic fees support. +#[cfg(feature = "pallet-xcm-bridge-router")] +pub use pallet_xcm_bridge_router; + /// Logic which is common to all parachain runtimes. #[cfg(feature = "parachains-common")] pub use parachains_common;