diff --git a/roadmap/implementers-guide/src/types/overseer-protocol.md b/roadmap/implementers-guide/src/types/overseer-protocol.md index b1b139f859a9..78d536f1a21c 100644 --- a/roadmap/implementers-guide/src/types/overseer-protocol.md +++ b/roadmap/implementers-guide/src/types/overseer-protocol.md @@ -441,7 +441,7 @@ enum DisputeCoordinatorMessage { pending_confirmation: oneshot::Sender }, /// Fetch a list of all recent disputes that the co-ordinator is aware of. - /// These are disputes which have occured any time in recent sessions, which may have already concluded. + /// These are disputes which have occurred any time in recent sessions, which may have already concluded. RecentDisputes(ResponseChannel>), /// Fetch a list of all active disputes that the co-ordinator is aware of. /// These disputes are either unconcluded or recently concluded. @@ -699,7 +699,7 @@ The Runtime API subsystem is responsible for providing an interface to the state This is fueled by an auxiliary type encapsulating all request types defined in the Runtime API section of the guide. -> TODO: link to the Runtime API section. Not possible currently because of https://github.com/Michael-F-Bryan/mdbook-linkcheck/issues/25. Once v0.7.1 is released it will work. +> To do: link to the Runtime API section. Not possible currently because of https://github.com/Michael-F-Bryan/mdbook-linkcheck/issues/25. Once v0.7.1 is released it will work. ```rust enum RuntimeApiRequest { diff --git a/runtime/kusama/src/lib.rs b/runtime/kusama/src/lib.rs index 036b8ab392fb..9368bcffdcf3 100644 --- a/runtime/kusama/src/lib.rs +++ b/runtime/kusama/src/lib.rs @@ -1299,6 +1299,7 @@ impl xcm_executor::Config for XcmConfig { type ResponseHandler = XcmPallet; type AssetTrap = XcmPallet; type AssetClaims = XcmPallet; + type SubscriptionService = XcmPallet; } parameter_types! { @@ -1318,7 +1319,6 @@ pub type LocalOriginToLocation = ( // And a usual Signed origin to be used in XCM as a corresponding AccountId32 SignedToAccountId32, ); - impl pallet_xcm::Config for Runtime { type Event = Event; type SendXcmOrigin = xcm_builder::EnsureXcmOrigin; @@ -1334,6 +1334,8 @@ impl pallet_xcm::Config for Runtime { type LocationInverter = LocationInverter; type Origin = Origin; type Call = Call; + const VERSION_DISCOVERY_QUEUE_SIZE: u32 = 100; + type AdvertisedXcmVersion = pallet_xcm::CurrentXcmVersion; } parameter_types! { diff --git a/runtime/rococo/src/lib.rs b/runtime/rococo/src/lib.rs index c46bca567506..e1aa48a6d1fc 100644 --- a/runtime/rococo/src/lib.rs +++ b/runtime/rococo/src/lib.rs @@ -672,6 +672,7 @@ impl xcm_executor::Config for XcmConfig { type ResponseHandler = XcmPallet; type AssetTrap = XcmPallet; type AssetClaims = XcmPallet; + type SubscriptionService = XcmPallet; } parameter_types! { @@ -703,6 +704,8 @@ impl pallet_xcm::Config for Runtime { type LocationInverter = LocationInverter; type Origin = Origin; type Call = Call; + const VERSION_DISCOVERY_QUEUE_SIZE: u32 = 100; + type AdvertisedXcmVersion = pallet_xcm::CurrentXcmVersion; } impl parachains_session_info::Config for Runtime {} diff --git a/runtime/test-runtime/src/lib.rs b/runtime/test-runtime/src/lib.rs index 529a44f53d09..bf322f7716aa 100644 --- a/runtime/test-runtime/src/lib.rs +++ b/runtime/test-runtime/src/lib.rs @@ -515,6 +515,8 @@ impl pallet_xcm::Config for Runtime { type XcmReserveTransferFilter = Everything; type Origin = Origin; type Call = Call; + const VERSION_DISCOVERY_QUEUE_SIZE: u32 = 100; + type AdvertisedXcmVersion = pallet_xcm::CurrentXcmVersion; } impl parachains_hrmp::Config for Runtime { @@ -537,7 +539,7 @@ impl pallet_test_notifier::Config for Runtime { pub mod pallet_test_notifier { use frame_support::pallet_prelude::*; use frame_system::pallet_prelude::*; - use pallet_xcm::{ensure_response, QueryId}; + use pallet_xcm::ensure_response; use sp_runtime::DispatchResult; use xcm::latest::prelude::*; diff --git a/runtime/test-runtime/src/xcm_config.rs b/runtime/test-runtime/src/xcm_config.rs index 23c770f94a28..2b22989ea93d 100644 --- a/runtime/test-runtime/src/xcm_config.rs +++ b/runtime/test-runtime/src/xcm_config.rs @@ -88,4 +88,5 @@ impl xcm_executor::Config for XcmConfig { type ResponseHandler = super::Xcm; type AssetTrap = super::Xcm; type AssetClaims = super::Xcm; + type SubscriptionService = super::Xcm; } diff --git a/runtime/westend/src/lib.rs b/runtime/westend/src/lib.rs index 52c8a72c134e..3003ad33741b 100644 --- a/runtime/westend/src/lib.rs +++ b/runtime/westend/src/lib.rs @@ -941,6 +941,7 @@ impl xcm_executor::Config for XcmConfig { type ResponseHandler = XcmPallet; type AssetTrap = XcmPallet; type AssetClaims = XcmPallet; + type SubscriptionService = XcmPallet; } /// Type to convert an `Origin` type value into a `MultiLocation` value which represents an interior location @@ -965,6 +966,8 @@ impl pallet_xcm::Config for Runtime { type LocationInverter = LocationInverter; type Origin = Origin; type Call = Call; + const VERSION_DISCOVERY_QUEUE_SIZE: u32 = 100; + type AdvertisedXcmVersion = pallet_xcm::CurrentXcmVersion; } construct_runtime! { diff --git a/xcm/pallet-xcm/src/lib.rs b/xcm/pallet-xcm/src/lib.rs index bbb477971ec3..314a929803cd 100644 --- a/xcm/pallet-xcm/src/lib.rs +++ b/xcm/pallet-xcm/src/lib.rs @@ -23,9 +23,12 @@ mod mock; #[cfg(test)] mod tests; -use codec::{Decode, Encode}; +use codec::{Decode, Encode, EncodeLike}; use frame_support::traits::{Contains, EnsureOrigin, Get, OriginTrait}; -use sp_runtime::{traits::BadOrigin, RuntimeDebug}; +use sp_runtime::{ + traits::{BadOrigin, Saturating}, + RuntimeDebug, +}; use sp_std::{ boxed::Box, convert::{TryFrom, TryInto}, @@ -34,10 +37,7 @@ use sp_std::{ result::Result, vec, }; -use xcm::{ - latest::prelude::*, VersionedMultiAssets, VersionedMultiLocation, VersionedResponse, - VersionedXcm, -}; +use xcm::prelude::*; use xcm_executor::traits::ConvertOrigin; use frame_support::PalletId; @@ -49,15 +49,25 @@ pub mod pallet { use frame_support::{ dispatch::{Dispatchable, GetDispatchInfo, PostDispatchInfo}, pallet_prelude::*, + parameter_types, }; use frame_system::{pallet_prelude::*, Config as SysConfig}; use sp_core::H256; use sp_runtime::traits::{AccountIdConversion, BlakeTwo256, BlockNumberProvider, Hash}; use xcm_executor::{ - traits::{ClaimAssets, DropAssets, InvertLocation, OnResponse, WeightBounds}, + traits::{ + ClaimAssets, DropAssets, InvertLocation, OnResponse, VersionChangeNotifier, + WeightBounds, + }, Assets, }; + parameter_types! { + /// An implementation of `Get` which just returns the latest XCM version which we can + /// support. + pub const CurrentXcmVersion: u32 = XCM_VERSION; + } + #[pallet::pallet] #[pallet::generate_store(pub(super) trait Store)] pub struct Pallet(_); @@ -106,6 +116,12 @@ pub mod pallet { + GetDispatchInfo + IsType<::Call> + Dispatchable::Origin, PostInfo = PostDispatchInfo>; + + const VERSION_DISCOVERY_QUEUE_SIZE: u32; + + /// The latest supported version that we advertise. Generally just set it to + /// `pallet_xcm::CurrentXcmVersion`. + type AdvertisedXcmVersion: Get; } /// The maximum number of distinct assets allowed to be transferred in a single helper extrinsic. @@ -160,7 +176,7 @@ pub mod pallet { /// be received and acted upon. /// /// \[ origin location, id, expected location \] - InvalidResponder(MultiLocation, QueryId, MultiLocation), + InvalidResponder(MultiLocation, QueryId, Option), /// Expected query response has been received but the expected origin location placed in /// storate by this runtime previously cannot be decoded. The query remains registered. /// @@ -179,6 +195,25 @@ pub mod pallet { /// /// \[ hash, origin, assets \] AssetsTrapped(H256, MultiLocation, VersionedMultiAssets), + /// An XCM version change notification message has been attempted to be sent. + /// + /// \[ destination, result \] + VersionChangeNotified(MultiLocation, XcmVersion), + /// The supported version of a location has been changed. This might be through an + /// automatic notification or a manual intervention. + /// + /// \[ location, XCM version \] + SupportedVersionChanged(MultiLocation, XcmVersion), + /// A given location which had a version change subscription was dropped owing to an error + /// sending the notification to it. + /// + /// \[ location, query ID, error \] + NotifyTargetSendFail(MultiLocation, QueryId, XcmError), + /// A given location which had a version change subscription was dropped owing to an error + /// migrating the location to our new XCM format. + /// + /// \[ location, query ID \] + NotifyTargetMigrationFail(VersionedMultiLocation, QueryId), } #[pallet::origin] @@ -219,6 +254,13 @@ pub mod pallet { InvalidOrigin, /// The version of the `Versioned` value used is not able to be interpreted. BadVersion, + /// The given location could not be used (e.g. because it cannot be expressed in the + /// desired version of XCM). + BadLocation, + /// The referenced subscription could not be found. + NoSubscription, + /// The location is invalid since it already has a subscription from us. + AlreadySubscribed, } /// The status of a query. @@ -230,16 +272,41 @@ pub mod pallet { maybe_notify: Option<(u8, u8)>, timeout: BlockNumber, }, + /// The query is for an ongoing version notification subscription. + VersionNotifier { origin: VersionedMultiLocation, is_active: bool }, /// A response has been received. Ready { response: VersionedResponse, at: BlockNumber }, } - /// Value of a query, must be unique for each query. - pub type QueryId = u64; + #[derive(Copy, Clone)] + pub(crate) struct LatestVersionedMultiLocation<'a>(pub(crate) &'a MultiLocation); + impl<'a> EncodeLike for LatestVersionedMultiLocation<'a> {} + impl<'a> Encode for LatestVersionedMultiLocation<'a> { + fn encode(&self) -> Vec { + let mut r = VersionedMultiLocation::from(MultiLocation::default()).encode(); + r.truncate(1); + self.0.using_encoded(|d| r.extend_from_slice(d)); + r + } + } + + #[derive(Clone, Encode, Decode, Eq, PartialEq, Ord, PartialOrd)] + pub enum VersionMigrationStage { + MigrateSupportedVersion, + MigrateVersionNotifiers, + NotifyCurrentTargets(Option>), + MigrateAndNotifyOldTargets, + } + + impl Default for VersionMigrationStage { + fn default() -> Self { + Self::MigrateSupportedVersion + } + } /// The latest available query index. #[pallet::storage] - pub(super) type QueryCount = StorageValue<_, QueryId, ValueQuery>; + pub(super) type QueryCounter = StorageValue<_, QueryId, ValueQuery>; /// The ongoing queries. #[pallet::storage] @@ -255,8 +322,131 @@ pub mod pallet { #[pallet::getter(fn asset_trap)] pub(super) type AssetTraps = StorageMap<_, Identity, H256, u32, ValueQuery>; + /// Default version to encode XCM when latest version of destination is unknown. If `None`, + /// then the destinations whose XCM version is unknown are considered unreachable. + #[pallet::storage] + pub(super) type SafeXcmVersion = StorageValue<_, XcmVersion, OptionQuery>; + + /// Latest versions that we know various locations support. + #[pallet::storage] + pub(super) type SupportedVersion = StorageDoubleMap< + _, + Twox64Concat, + XcmVersion, + Blake2_128Concat, + VersionedMultiLocation, + XcmVersion, + OptionQuery, + >; + + /// All locations that we have requested version notifications from. + #[pallet::storage] + pub(super) type VersionNotifiers = StorageDoubleMap< + _, + Twox64Concat, + XcmVersion, + Blake2_128Concat, + VersionedMultiLocation, + QueryId, + OptionQuery, + >; + + /// The target locations that are subscribed to our version changes, as well as the most recent + /// of our versions we informed them of. + #[pallet::storage] + pub(super) type VersionNotifyTargets = StorageDoubleMap< + _, + Twox64Concat, + XcmVersion, + Blake2_128Concat, + VersionedMultiLocation, + (QueryId, u64, XcmVersion), + OptionQuery, + >; + + pub struct VersionDiscoveryQueueSize(PhantomData); + impl Get for VersionDiscoveryQueueSize { + fn get() -> u32 { + T::VERSION_DISCOVERY_QUEUE_SIZE + } + } + + /// Destinations whose latest XCM version we would like to know. Duplicates not allowed, and + /// the `u32` counter is the number of times that a send to the destination has been attempted, + /// which is used as a prioritization. + #[pallet::storage] + pub(super) type VersionDiscoveryQueue = StorageValue< + _, + BoundedVec<(VersionedMultiLocation, u32), VersionDiscoveryQueueSize>, + ValueQuery, + >; + + /// The current migration's stage, if any. + #[pallet::storage] + pub(super) type CurrentMigration = + StorageValue<_, VersionMigrationStage, OptionQuery>; + + #[pallet::genesis_config] + pub struct GenesisConfig { + /// The default version to encode outgoing XCM messages with. + pub safe_xcm_version: Option, + } + + #[cfg(feature = "std")] + impl Default for GenesisConfig { + fn default() -> Self { + Self { safe_xcm_version: Some(XCM_VERSION) } + } + } + + #[pallet::genesis_build] + impl GenesisBuild for GenesisConfig { + fn build(&self) { + SafeXcmVersion::::set(self.safe_xcm_version); + } + } + #[pallet::hooks] - impl Hooks> for Pallet {} + impl Hooks> for Pallet { + fn on_initialize(_n: BlockNumberFor) -> Weight { + let mut weight_used = 0; + if let Some(migration) = CurrentMigration::::get() { + // Consume 10% of block at most + let max_weight = T::BlockWeights::get().max_block / 10; + let (w, maybe_migration) = Self::check_xcm_version_change(migration, max_weight); + CurrentMigration::::set(maybe_migration); + weight_used.saturating_accrue(w); + } + + // Here we aim to get one successful version negotiation request sent per block, ordered + // by the destinations being most sent to. + let mut q = VersionDiscoveryQueue::::take().into_inner(); + // TODO: correct weights. + weight_used += T::DbWeight::get().read + T::DbWeight::get().write; + q.sort_by_key(|i| i.1); + while let Some((versioned_dest, _)) = q.pop() { + if let Ok(dest) = versioned_dest.try_into() { + if Self::request_version_notify(dest).is_ok() { + // TODO: correct weights. + weight_used += T::DbWeight::get().read + T::DbWeight::get().write; + break + } + } + } + // Should never fail since we only removed items. But better safe than panicking as it's + // way better to drop the queue than panic on initialize. + if let Ok(q) = BoundedVec::try_from(q) { + VersionDiscoveryQueue::::put(q); + } + weight_used + } + fn on_runtime_upgrade() -> Weight { + // Start a migration (this happens before on_initialize so it'll happen later in this + // block, which should be good enough)... + CurrentMigration::::put(VersionMigrationStage::default()); + T::DbWeight::get().write + } + } #[pallet::call] impl Pallet { @@ -448,9 +638,263 @@ pub mod pallet { Self::deposit_event(Event::Attempted(outcome)); Ok(()) } + + /// Extoll that a particular destination can be communicated with through a particular + /// version of XCM. + /// + /// - `origin`: Must be Root. + /// - `location`: The destination that is being described. + /// - `xcm_version`: The latest version of XCM that `location` supports. + #[pallet::weight(100_000_000u64)] + pub fn force_xcm_version( + origin: OriginFor, + location: Box, + xcm_version: XcmVersion, + ) -> DispatchResult { + ensure_root(origin)?; + let location = *location; + SupportedVersion::::insert( + XCM_VERSION, + LatestVersionedMultiLocation(&location), + xcm_version, + ); + Self::deposit_event(Event::SupportedVersionChanged(location, xcm_version)); + Ok(()) + } + + /// Set a safe XCM version (the version that XCM should be encoded with if the most recent + /// version a destination can accept is unknown). + /// + /// - `origin`: Must be Root. + /// - `maybe_xcm_version`: The default XCM encoding version, or `None` to disable. + #[pallet::weight(100_000_000u64)] + pub fn force_default_xcm_version( + origin: OriginFor, + maybe_xcm_version: Option, + ) -> DispatchResult { + ensure_root(origin)?; + SafeXcmVersion::::set(maybe_xcm_version); + Ok(()) + } + + /// Ask a location to notify us regarding their XCM version and any changes to it. + /// + /// - `origin`: Must be Root. + /// - `location`: The location to which we should subscribe for XCM version notifications. + #[pallet::weight(100_000_000u64)] + pub fn force_subscribe_version_notify( + origin: OriginFor, + location: Box, + ) -> DispatchResult { + ensure_root(origin)?; + let location = (*location).try_into().map_err(|()| Error::::BadLocation)?; + Self::request_version_notify(location).map_err(|e| { + match e { + XcmError::InvalidLocation => Error::::AlreadySubscribed, + _ => Error::::InvalidOrigin, + } + .into() + }) + } + + /// Require that a particular destination should no longer notify us regarding any XCM + /// version changes. + /// + /// - `origin`: Must be Root. + /// - `location`: The location to which we are currently subscribed for XCM version + /// notifications which we no longer desire. + #[pallet::weight(100_000_000u64)] + pub fn force_unsubscribe_version_notify( + origin: OriginFor, + location: Box, + ) -> DispatchResult { + ensure_root(origin)?; + let location = (*location).try_into().map_err(|()| Error::::BadLocation)?; + Self::unrequest_version_notify(location).map_err(|e| { + match e { + XcmError::InvalidLocation => Error::::NoSubscription, + _ => Error::::InvalidOrigin, + } + .into() + }) + } } impl Pallet { + /// Will always make progress, and will do its best not to use much more than `weight_cutoff` + /// in doing so. + pub(crate) fn check_xcm_version_change( + mut stage: VersionMigrationStage, + weight_cutoff: Weight, + ) -> (Weight, Option) { + let mut weight_used = 0; + + // TODO: Correct weights for the components of this: + let todo_sv_migrate_weight: Weight = T::DbWeight::get().read + T::DbWeight::get().write; + let todo_vn_migrate_weight: Weight = T::DbWeight::get().read + T::DbWeight::get().write; + let todo_vnt_already_notified_weight: Weight = T::DbWeight::get().read; + let todo_vnt_notify_weight: Weight = + T::DbWeight::get().read + T::DbWeight::get().write * 3; + let todo_vnt_migrate_weight: Weight = + T::DbWeight::get().read + T::DbWeight::get().write; + let todo_vnt_migrate_fail_weight: Weight = + T::DbWeight::get().read + T::DbWeight::get().write; + let todo_vnt_notify_migrate_weight: Weight = + T::DbWeight::get().read + T::DbWeight::get().write * 3; + + use VersionMigrationStage::*; + + if stage == MigrateSupportedVersion { + // We assume that supported XCM version only ever increases, so just cycle through lower + // XCM versioned from the current. + for v in 0..XCM_VERSION { + for (old_key, value) in SupportedVersion::::drain_prefix(v) { + if let Ok(new_key) = old_key.into_latest() { + SupportedVersion::::insert(XCM_VERSION, new_key, value); + } + weight_used.saturating_accrue(todo_sv_migrate_weight); + if weight_used >= weight_cutoff { + return (weight_used, Some(stage)) + } + } + } + stage = MigrateVersionNotifiers; + } + if stage == MigrateVersionNotifiers { + for v in 0..XCM_VERSION { + for (old_key, value) in VersionNotifiers::::drain_prefix(v) { + if let Ok(new_key) = old_key.into_latest() { + VersionNotifiers::::insert(XCM_VERSION, new_key, value); + } + weight_used.saturating_accrue(todo_vn_migrate_weight); + if weight_used >= weight_cutoff { + return (weight_used, Some(stage)) + } + } + } + stage = NotifyCurrentTargets(None); + } + + let xcm_version = T::AdvertisedXcmVersion::get(); + + if let NotifyCurrentTargets(maybe_last_raw_key) = stage { + let mut iter = match maybe_last_raw_key { + Some(k) => VersionNotifyTargets::::iter_prefix_from(XCM_VERSION, k), + None => VersionNotifyTargets::::iter_prefix(XCM_VERSION), + }; + while let Some((key, value)) = iter.next() { + let (query_id, max_weight, target_xcm_version) = value; + let new_key: MultiLocation = match key.clone().try_into() { + Ok(k) if target_xcm_version != xcm_version => k, + _ => { + // We don't early return here since we need to be certain that we + // make some progress. + weight_used.saturating_accrue(todo_vnt_already_notified_weight); + continue + }, + }; + let response = Response::Version(xcm_version); + let message = Xcm(vec![QueryResponse { query_id, response, max_weight }]); + let event = match T::XcmRouter::send_xcm(new_key.clone(), message) { + Ok(()) => { + let value = (query_id, max_weight, xcm_version); + VersionNotifyTargets::::insert(XCM_VERSION, key, value); + Event::VersionChangeNotified(new_key, xcm_version) + }, + Err(e) => { + VersionNotifyTargets::::remove(XCM_VERSION, key); + Event::NotifyTargetSendFail(new_key, query_id, e.into()) + }, + }; + Self::deposit_event(event); + weight_used.saturating_accrue(todo_vnt_notify_weight); + if weight_used >= weight_cutoff { + let last = Some(iter.last_raw_key().into()); + return (weight_used, Some(NotifyCurrentTargets(last))) + } + } + stage = MigrateAndNotifyOldTargets; + } + if stage == MigrateAndNotifyOldTargets { + for v in 0..XCM_VERSION { + for (old_key, value) in VersionNotifyTargets::::drain_prefix(v) { + let (query_id, max_weight, target_xcm_version) = value; + let new_key = match MultiLocation::try_from(old_key.clone()) { + Ok(k) => k, + Err(()) => { + Self::deposit_event(Event::NotifyTargetMigrationFail( + old_key, value.0, + )); + weight_used.saturating_accrue(todo_vnt_migrate_fail_weight); + if weight_used >= weight_cutoff { + return (weight_used, Some(stage)) + } + continue + }, + }; + + let versioned_key = LatestVersionedMultiLocation(&new_key); + if target_xcm_version == xcm_version { + VersionNotifyTargets::::insert(XCM_VERSION, versioned_key, value); + weight_used.saturating_accrue(todo_vnt_migrate_weight); + } else { + // Need to notify target. + let response = Response::Version(xcm_version); + let message = + Xcm(vec![QueryResponse { query_id, response, max_weight }]); + let event = match T::XcmRouter::send_xcm(new_key.clone(), message) { + Ok(()) => { + VersionNotifyTargets::::insert( + XCM_VERSION, + versioned_key, + (query_id, max_weight, xcm_version), + ); + Event::VersionChangeNotified(new_key, xcm_version) + }, + Err(e) => Event::NotifyTargetSendFail(new_key, query_id, e.into()), + }; + Self::deposit_event(event); + weight_used.saturating_accrue(todo_vnt_notify_migrate_weight); + } + if weight_used >= weight_cutoff { + return (weight_used, Some(stage)) + } + } + } + } + (weight_used, None) + } + + /// Request that `dest` informs us of its version. + pub fn request_version_notify(dest: MultiLocation) -> XcmResult { + let versioned_dest = VersionedMultiLocation::from(dest.clone()); + let already = VersionNotifiers::::contains_key(XCM_VERSION, &versioned_dest); + ensure!(!already, XcmError::InvalidLocation); + let query_id = QueryCounter::::mutate(|q| { + let r = *q; + q.saturating_inc(); + r + }); + // TODO #3735: Correct weight. + let instruction = SubscribeVersion { query_id, max_response_weight: 0 }; + T::XcmRouter::send_xcm(dest, Xcm(vec![instruction]))?; + VersionNotifiers::::insert(XCM_VERSION, &versioned_dest, query_id); + let query_status = + QueryStatus::VersionNotifier { origin: versioned_dest, is_active: false }; + Queries::::insert(query_id, query_status); + Ok(()) + } + + /// Request that `dest` ceases informing us of its version. + pub fn unrequest_version_notify(dest: MultiLocation) -> XcmResult { + let versioned_dest = LatestVersionedMultiLocation(&dest); + let query_id = VersionNotifiers::::take(XCM_VERSION, versioned_dest) + .ok_or(XcmError::InvalidLocation)?; + T::XcmRouter::send_xcm(dest.clone(), Xcm(vec![UnsubscribeVersion]))?; + Queries::::remove(query_id); + Ok(()) + } + /// Relay an XCM `message` from a given `interior` location in this context to a given `dest` /// location. A null `dest` is not handled. pub fn send_xcm( @@ -475,9 +919,9 @@ pub mod pallet { maybe_notify: Option<(u8, u8)>, timeout: T::BlockNumber, ) -> u64 { - QueryCount::::mutate(|q| { + QueryCounter::::mutate(|q| { let r = *q; - *q += 1; + q.saturating_inc(); Queries::::insert( r, QueryStatus::Pending { responder: responder.into(), maybe_notify, timeout }, @@ -579,8 +1023,68 @@ pub mod pallet { None } } + + /// Note that a particular destination to whom we would like to send a message is unknown + /// and queue it for version discovery. + fn note_unknown_version(dest: &MultiLocation) { + let versioned_dest = VersionedMultiLocation::from(dest.clone()); + VersionDiscoveryQueue::::mutate(|q| { + if let Some(index) = q.iter().position(|i| &i.0 == &versioned_dest) { + // exists - just bump the count. + q[index].1.saturating_inc(); + } else { + let _ = q.try_push((versioned_dest, 1)); + } + }); + } + } + + impl WrapVersion for Pallet { + fn wrap_version( + dest: &MultiLocation, + xcm: impl Into>, + ) -> Result, ()> { + SupportedVersion::::get(XCM_VERSION, LatestVersionedMultiLocation(dest)) + .or_else(|| { + Self::note_unknown_version(dest); + SafeXcmVersion::::get() + }) + .ok_or(()) + .and_then(|v| xcm.into().into_version(v.min(XCM_VERSION))) + } } + impl VersionChangeNotifier for Pallet { + /// Start notifying `location` should the XCM version of this chain change. + /// + /// When it does, this type should ensure a `QueryResponse` message is sent with the given + /// `query_id` & `max_weight` and with a `response` of `Repsonse::Version`. This should happen + /// until/unless `stop` is called with the correct `query_id`. + /// + /// If the `location` has an ongoing notification and when this function is called, then an + /// error should be returned. + fn start(dest: &MultiLocation, query_id: QueryId, max_weight: u64) -> XcmResult { + let versioned_dest = LatestVersionedMultiLocation(dest); + let already = VersionNotifyTargets::::contains_key(XCM_VERSION, versioned_dest); + ensure!(!already, XcmError::InvalidLocation); + + let xcm_version = T::AdvertisedXcmVersion::get(); + let response = Response::Version(xcm_version); + let instruction = QueryResponse { query_id, response, max_weight }; + T::XcmRouter::send_xcm(dest.clone(), Xcm(vec![instruction]))?; + + let value = (query_id, max_weight, xcm_version); + VersionNotifyTargets::::insert(XCM_VERSION, versioned_dest, value); + Ok(()) + } + + /// Stop notifying `location` should the XCM change. This is a no-op if there was never a + /// subscription. + fn stop(dest: &MultiLocation) -> XcmResult { + VersionNotifyTargets::::remove(XCM_VERSION, LatestVersionedMultiLocation(dest)); + Ok(()) + } + } impl DropAssets for Pallet { fn drop_assets(origin: &MultiLocation, assets: Assets) -> Weight { if assets.is_empty() { @@ -590,7 +1094,7 @@ pub mod pallet { let hash = BlakeTwo256::hash_of(&(&origin, &versioned)); AssetTraps::::mutate(hash, |n| *n += 1); Self::deposit_event(Event::AssetsTrapped(hash, origin.clone(), versioned)); - // TODO: Put the real weight in there. + // TODO #3735: Put the real weight in there. 0 } } @@ -623,10 +1127,13 @@ pub mod pallet { impl OnResponse for Pallet { fn expecting_response(origin: &MultiLocation, query_id: QueryId) -> bool { - if let Some(QueryStatus::Pending { responder, .. }) = Queries::::get(query_id) { - return MultiLocation::try_from(responder).map_or(false, |r| origin == &r) + match Queries::::get(query_id) { + Some(QueryStatus::Pending { responder, .. }) => + MultiLocation::try_from(responder).map_or(false, |r| origin == &r), + Some(QueryStatus::VersionNotifier { origin: r, .. }) => + MultiLocation::try_from(r).map_or(false, |r| origin == &r), + _ => false, } - false } fn on_response( @@ -635,87 +1142,133 @@ pub mod pallet { response: Response, max_weight: Weight, ) -> Weight { - if let Some(QueryStatus::Pending { responder, maybe_notify, .. }) = - Queries::::get(query_id) - { - if let Ok(responder) = MultiLocation::try_from(responder) { - if origin == &responder { - return match maybe_notify { - Some((pallet_index, call_index)) => { - // This is a bit horrible, but we happen to know that the `Call` will - // be built by `(pallet_index: u8, call_index: u8, QueryId, Response)`. - // So we just encode that and then re-encode to a real Call. - let bare = (pallet_index, call_index, query_id, response); - if let Ok(call) = bare.using_encoded(|mut bytes| { - ::Call::decode(&mut bytes) - }) { - Queries::::remove(query_id); - let weight = call.get_dispatch_info().weight; - if weight > max_weight { - let e = Event::NotifyOverweight( - query_id, - pallet_index, - call_index, - weight, - max_weight, - ); - Self::deposit_event(e); - return 0 - } - let dispatch_origin = Origin::Response(origin.clone()).into(); - match call.dispatch(dispatch_origin) { - Ok(post_info) => { - let e = - Event::Notified(query_id, pallet_index, call_index); - Self::deposit_event(e); - post_info.actual_weight - }, - Err(error_and_info) => { - let e = Event::NotifyDispatchError( - query_id, - pallet_index, - call_index, - ); - Self::deposit_event(e); - // Not much to do with the result as it is. It's up to the parachain to ensure that the - // message makes sense. - error_and_info.post_info.actual_weight - }, - } - .unwrap_or(weight) - } else { - let e = Event::NotifyDecodeFailed( + match (response, Queries::::get(query_id)) { + ( + Response::Version(v), + Some(QueryStatus::VersionNotifier { origin: expected_origin, is_active }), + ) => { + let origin: MultiLocation = match expected_origin.try_into() { + Ok(o) if &o == origin => o, + Ok(o) => { + Self::deposit_event(Event::InvalidResponder( + origin.clone(), + query_id, + Some(o), + )); + return 0 + }, + _ => { + Self::deposit_event(Event::InvalidResponder( + origin.clone(), + query_id, + None, + )); + // TODO #3735: Correct weight for this. + return 0 + }, + }; + // TODO #3735: Check max_weight is correct. + if !is_active { + Queries::::insert( + query_id, + QueryStatus::VersionNotifier { + origin: origin.clone().into(), + is_active: true, + }, + ); + } + // We're being notified of a version change. + SupportedVersion::::insert( + XCM_VERSION, + LatestVersionedMultiLocation(&origin), + v, + ); + Self::deposit_event(Event::SupportedVersionChanged(origin, v)); + 0 + }, + (response, Some(QueryStatus::Pending { responder, maybe_notify, .. })) => { + let responder = match MultiLocation::try_from(responder) { + Ok(r) => r, + Err(_) => { + Self::deposit_event(Event::InvalidResponderVersion( + origin.clone(), + query_id, + )); + return 0 + }, + }; + if origin != &responder { + Self::deposit_event(Event::InvalidResponder( + origin.clone(), + query_id, + Some(responder), + )); + return 0 + } + return match maybe_notify { + Some((pallet_index, call_index)) => { + // This is a bit horrible, but we happen to know that the `Call` will + // be built by `(pallet_index: u8, call_index: u8, QueryId, Response)`. + // So we just encode that and then re-encode to a real Call. + let bare = (pallet_index, call_index, query_id, response); + if let Ok(call) = bare + .using_encoded(|mut bytes| ::Call::decode(&mut bytes)) + { + Queries::::remove(query_id); + let weight = call.get_dispatch_info().weight; + if weight > max_weight { + let e = Event::NotifyOverweight( query_id, pallet_index, call_index, + weight, + max_weight, ); Self::deposit_event(e); - 0 + return 0 } - }, - None => { - let e = Event::ResponseReady(query_id, response.clone()); + let dispatch_origin = Origin::Response(origin.clone()).into(); + match call.dispatch(dispatch_origin) { + Ok(post_info) => { + let e = Event::Notified(query_id, pallet_index, call_index); + Self::deposit_event(e); + post_info.actual_weight + }, + Err(error_and_info) => { + let e = Event::NotifyDispatchError( + query_id, + pallet_index, + call_index, + ); + Self::deposit_event(e); + // Not much to do with the result as it is. It's up to the parachain to ensure that the + // message makes sense. + error_and_info.post_info.actual_weight + }, + } + .unwrap_or(weight) + } else { + let e = + Event::NotifyDecodeFailed(query_id, pallet_index, call_index); Self::deposit_event(e); - let at = frame_system::Pallet::::current_block_number(); - let response = response.into(); - Queries::::insert(query_id, QueryStatus::Ready { response, at }); 0 - }, - } - } else { - Self::deposit_event(Event::InvalidResponder( - origin.clone(), - query_id, - responder, - )); + } + }, + None => { + let e = Event::ResponseReady(query_id, response.clone()); + Self::deposit_event(e); + let at = frame_system::Pallet::::current_block_number(); + let response = response.into(); + Queries::::insert(query_id, QueryStatus::Ready { response, at }); + 0 + }, } - } else { - Self::deposit_event(Event::InvalidResponderVersion(origin.clone(), query_id)); - } - } else { - Self::deposit_event(Event::UnexpectedResponse(origin.clone(), query_id)); + }, + _ => { + Self::deposit_event(Event::UnexpectedResponse(origin.clone(), query_id)); + return 0 + }, } - 0 } } } diff --git a/xcm/pallet-xcm/src/mock.rs b/xcm/pallet-xcm/src/mock.rs index 804a39646a16..c841d896acab 100644 --- a/xcm/pallet-xcm/src/mock.rs +++ b/xcm/pallet-xcm/src/mock.rs @@ -22,11 +22,11 @@ use sp_runtime::{testing::Header, traits::IdentityLookup, AccountId32}; pub use sp_std::{cell::RefCell, fmt::Debug, marker::PhantomData}; use xcm::latest::prelude::*; use xcm_builder::{ - AccountId32Aliases, AllowKnownQueryResponses, AllowTopLevelPaidExecutionFrom, Case, - ChildParachainAsNative, ChildParachainConvertsVia, ChildSystemParachainAsSuperuser, - CurrencyAdapter as XcmCurrencyAdapter, FixedRateOfFungible, FixedWeightBounds, IsConcrete, - LocationInverter, SignedAccountId32AsNative, SignedToAccountId32, SovereignSignedViaLocation, - TakeWeightCredit, + AccountId32Aliases, AllowKnownQueryResponses, AllowSubscriptionsFrom, + AllowTopLevelPaidExecutionFrom, Case, ChildParachainAsNative, ChildParachainConvertsVia, + ChildSystemParachainAsSuperuser, CurrencyAdapter as XcmCurrencyAdapter, FixedRateOfFungible, + FixedWeightBounds, IsConcrete, LocationInverter, SignedAccountId32AsNative, + SignedToAccountId32, SovereignSignedViaLocation, TakeWeightCredit, }; use xcm_executor::XcmExecutor; @@ -133,9 +133,16 @@ construct_runtime!( thread_local! { pub static SENT_XCM: RefCell)>> = RefCell::new(Vec::new()); } -pub fn sent_xcm() -> Vec<(MultiLocation, Xcm<()>)> { +pub(crate) fn sent_xcm() -> Vec<(MultiLocation, Xcm<()>)> { SENT_XCM.with(|q| (*q.borrow()).clone()) } +pub(crate) fn take_sent_xcm() -> Vec<(MultiLocation, Xcm<()>)> { + SENT_XCM.with(|q| { + let mut r = Vec::new(); + std::mem::swap(&mut r, &mut *q.borrow_mut()); + r + }) +} /// Sender that never returns error, always sends pub struct TestSendXcm; impl SendXcm for TestSendXcm { @@ -236,6 +243,7 @@ pub type Barrier = ( TakeWeightCredit, AllowTopLevelPaidExecutionFrom, AllowKnownQueryResponses, + AllowSubscriptionsFrom, ); pub struct XcmConfig; @@ -253,10 +261,15 @@ impl xcm_executor::Config for XcmConfig { type ResponseHandler = XcmPallet; type AssetTrap = XcmPallet; type AssetClaims = XcmPallet; + type SubscriptionService = XcmPallet; } pub type LocalOriginToLocation = SignedToAccountId32; +parameter_types! { + pub static AdvertisedXcmVersion: pallet_xcm::XcmVersion = 2; +} + impl pallet_xcm::Config for Test { type Event = Event; type SendXcmOrigin = xcm_builder::EnsureXcmOrigin; @@ -270,6 +283,8 @@ impl pallet_xcm::Config for Test { type LocationInverter = LocationInverter; type Origin = Origin; type Call = Call; + const VERSION_DISCOVERY_QUEUE_SIZE: u32 = 100; + type AdvertisedXcmVersion = AdvertisedXcmVersion; } impl origin::Config for Test {} diff --git a/xcm/pallet-xcm/src/tests.rs b/xcm/pallet-xcm/src/tests.rs index 8bafde3cbb94..56f3579cc1e7 100644 --- a/xcm/pallet-xcm/src/tests.rs +++ b/xcm/pallet-xcm/src/tests.rs @@ -14,13 +14,20 @@ // You should have received a copy of the GNU General Public License // along with Polkadot. If not, see . -use crate::{mock::*, AssetTraps, QueryStatus}; -use frame_support::{assert_noop, assert_ok, traits::Currency}; +use crate::{ + mock::*, AssetTraps, CurrentMigration, Error, LatestVersionedMultiLocation, Queries, + QueryStatus, VersionDiscoveryQueue, VersionNotifiers, VersionNotifyTargets, +}; +use frame_support::{ + assert_noop, assert_ok, + traits::{Currency, Hooks}, +}; use polkadot_parachain::primitives::{AccountIdConversion, Id as ParaId}; use sp_runtime::traits::{BlakeTwo256, Hash}; use std::convert::TryInto; -use xcm::{latest::prelude::*, VersionedMultiAssets, VersionedXcm}; -use xcm_executor::XcmExecutor; +use xcm::prelude::*; +use xcm_builder::AllowKnownQueryResponses; +use xcm_executor::{traits::ShouldExecute, XcmExecutor}; const ALICE: AccountId = AccountId::new([0u8; 32]); const BOB: AccountId = AccountId::new([1u8; 32]); @@ -375,3 +382,423 @@ fn trapped_assets_can_be_claimed() { ); }); } + +#[test] +fn fake_latest_versioned_multilocation_works() { + use codec::Encode; + let remote = Parachain(1000).into(); + let versioned_remote = LatestVersionedMultiLocation(&remote); + assert_eq!(versioned_remote.encode(), VersionedMultiLocation::from(remote.clone()).encode()); +} + +#[test] +fn basic_subscription_works() { + new_test_ext_with_balances(vec![]).execute_with(|| { + let remote = Parachain(1000).into(); + assert_ok!(XcmPallet::force_subscribe_version_notify( + Origin::root(), + Box::new(remote.clone().into()), + )); + + assert_eq!( + Queries::::iter().collect::>(), + vec![( + 0, + QueryStatus::VersionNotifier { origin: remote.clone().into(), is_active: false } + )] + ); + assert_eq!( + VersionNotifiers::::iter().collect::>(), + vec![(2, remote.clone().into(), 0)] + ); + + assert_eq!( + take_sent_xcm(), + vec![( + remote.clone(), + Xcm(vec![SubscribeVersion { query_id: 0, max_response_weight: 0 }]), + ),] + ); + + let weight = BaseXcmWeight::get(); + let mut message = Xcm::<()>(vec![ + // Remote supports XCM v1 + QueryResponse { query_id: 0, max_weight: 0, response: Response::Version(1) }, + ]); + assert_ok!(AllowKnownQueryResponses::::should_execute( + &remote, + &mut message, + weight, + &mut 0 + )); + }); +} + +#[test] +fn subscriptions_increment_id() { + new_test_ext_with_balances(vec![]).execute_with(|| { + let remote = Parachain(1000).into(); + assert_ok!(XcmPallet::force_subscribe_version_notify( + Origin::root(), + Box::new(remote.clone().into()), + )); + + let remote2 = Parachain(1001).into(); + assert_ok!(XcmPallet::force_subscribe_version_notify( + Origin::root(), + Box::new(remote2.clone().into()), + )); + + assert_eq!( + take_sent_xcm(), + vec![ + ( + remote.clone(), + Xcm(vec![SubscribeVersion { query_id: 0, max_response_weight: 0 }]), + ), + ( + remote2.clone(), + Xcm(vec![SubscribeVersion { query_id: 1, max_response_weight: 0 }]), + ), + ] + ); + }); +} + +#[test] +fn double_subscription_fails() { + new_test_ext_with_balances(vec![]).execute_with(|| { + let remote = Parachain(1000).into(); + assert_ok!(XcmPallet::force_subscribe_version_notify( + Origin::root(), + Box::new(remote.clone().into()), + )); + assert_noop!( + XcmPallet::force_subscribe_version_notify( + Origin::root(), + Box::new(remote.clone().into()) + ), + Error::::AlreadySubscribed, + ); + }) +} + +#[test] +fn unsubscribe_works() { + new_test_ext_with_balances(vec![]).execute_with(|| { + let remote = Parachain(1000).into(); + assert_ok!(XcmPallet::force_subscribe_version_notify( + Origin::root(), + Box::new(remote.clone().into()), + )); + assert_ok!(XcmPallet::force_unsubscribe_version_notify( + Origin::root(), + Box::new(remote.clone().into()) + )); + assert_noop!( + XcmPallet::force_unsubscribe_version_notify( + Origin::root(), + Box::new(remote.clone().into()) + ), + Error::::NoSubscription, + ); + + assert_eq!( + take_sent_xcm(), + vec![ + ( + remote.clone(), + Xcm(vec![SubscribeVersion { query_id: 0, max_response_weight: 0 }]), + ), + (remote.clone(), Xcm(vec![UnsubscribeVersion]),), + ] + ); + }); +} + +/// Parachain 1000 is asking us for a version subscription. +#[test] +fn subscription_side_works() { + new_test_ext_with_balances(vec![]).execute_with(|| { + AdvertisedXcmVersion::set(1); + + let remote = Parachain(1000).into(); + let weight = BaseXcmWeight::get(); + let message = Xcm(vec![SubscribeVersion { query_id: 0, max_response_weight: 0 }]); + let r = XcmExecutor::::execute_xcm(remote.clone(), message, weight); + assert_eq!(r, Outcome::Complete(weight)); + + let instr = QueryResponse { query_id: 0, max_weight: 0, response: Response::Version(1) }; + assert_eq!(take_sent_xcm(), vec![(remote.clone(), Xcm(vec![instr]))]); + + // A runtime upgrade which doesn't alter the version sends no notifications. + XcmPallet::on_runtime_upgrade(); + XcmPallet::on_initialize(1); + assert_eq!(take_sent_xcm(), vec![]); + + // New version. + AdvertisedXcmVersion::set(2); + + // A runtime upgrade which alters the version does send notifications. + XcmPallet::on_runtime_upgrade(); + XcmPallet::on_initialize(2); + let instr = QueryResponse { query_id: 0, max_weight: 0, response: Response::Version(2) }; + assert_eq!(take_sent_xcm(), vec![(remote.clone(), Xcm(vec![instr]))]); + }); +} + +#[test] +fn subscription_side_upgrades_work_with_notify() { + new_test_ext_with_balances(vec![]).execute_with(|| { + AdvertisedXcmVersion::set(1); + + // An entry from a previous runtime with v0 XCM. + let v0_location = xcm::v0::MultiLocation::X1(xcm::v0::Junction::Parachain(1000)); + let v0_location = VersionedMultiLocation::from(v0_location); + VersionNotifyTargets::::insert(0, v0_location, (69, 0, 1)); + let v1_location = Parachain(1001).into().versioned(); + VersionNotifyTargets::::insert(1, v1_location, (70, 0, 1)); + let v2_location = Parachain(1002).into().versioned(); + VersionNotifyTargets::::insert(2, v2_location, (71, 0, 1)); + + // New version. + AdvertisedXcmVersion::set(2); + + // A runtime upgrade which alters the version does send notifications. + XcmPallet::on_runtime_upgrade(); + XcmPallet::on_initialize(1); + + let instr0 = QueryResponse { query_id: 69, max_weight: 0, response: Response::Version(2) }; + let instr1 = QueryResponse { query_id: 70, max_weight: 0, response: Response::Version(2) }; + let instr2 = QueryResponse { query_id: 71, max_weight: 0, response: Response::Version(2) }; + let mut sent = take_sent_xcm(); + sent.sort_by_key(|k| match (k.1).0[0] { + QueryResponse { query_id: q, .. } => q, + _ => 0, + }); + assert_eq!( + sent, + vec![ + (Parachain(1000).into(), Xcm(vec![instr0])), + (Parachain(1001).into(), Xcm(vec![instr1])), + (Parachain(1002).into(), Xcm(vec![instr2])), + ] + ); + + let mut contents = VersionNotifyTargets::::iter().collect::>(); + contents.sort_by_key(|k| k.2); + assert_eq!( + contents, + vec![ + (2, Parachain(1000).into().versioned(), (69, 0, 2)), + (2, Parachain(1001).into().versioned(), (70, 0, 2)), + (2, Parachain(1002).into().versioned(), (71, 0, 2)), + ] + ); + }); +} + +#[test] +fn subscription_side_upgrades_work_without_notify() { + new_test_ext_with_balances(vec![]).execute_with(|| { + // An entry from a previous runtime with v0 XCM. + let v0_location = xcm::v0::MultiLocation::X1(xcm::v0::Junction::Parachain(1000)); + let v0_location = VersionedMultiLocation::from(v0_location); + VersionNotifyTargets::::insert(0, v0_location, (69, 0, 2)); + let v1_location = Parachain(1001).into().versioned(); + VersionNotifyTargets::::insert(1, v1_location, (70, 0, 2)); + let v2_location = Parachain(1002).into().versioned(); + VersionNotifyTargets::::insert(2, v2_location, (71, 0, 2)); + + // A runtime upgrade which alters the version does send notifications. + XcmPallet::on_runtime_upgrade(); + XcmPallet::on_initialize(1); + + let mut contents = VersionNotifyTargets::::iter().collect::>(); + contents.sort_by_key(|k| k.2); + assert_eq!( + contents, + vec![ + (2, Parachain(1000).into().versioned(), (69, 0, 2)), + (2, Parachain(1001).into().versioned(), (70, 0, 2)), + (2, Parachain(1002).into().versioned(), (71, 0, 2)), + ] + ); + }); +} + +#[test] +fn subscriber_side_subscription_works() { + new_test_ext_with_balances(vec![]).execute_with(|| { + let remote = Parachain(1000).into(); + assert_ok!(XcmPallet::force_subscribe_version_notify( + Origin::root(), + Box::new(remote.clone().into()), + )); + take_sent_xcm(); + + // Assume subscription target is working ok. + + let weight = BaseXcmWeight::get(); + let message = Xcm(vec![ + // Remote supports XCM v1 + QueryResponse { query_id: 0, max_weight: 0, response: Response::Version(1) }, + ]); + let r = XcmExecutor::::execute_xcm(remote.clone(), message, weight); + assert_eq!(r, Outcome::Complete(weight)); + assert_eq!(take_sent_xcm(), vec![]); + + // This message cannot be sent to a v1 remote. + let v2_msg = Xcm::<()>(vec![Trap(0)]); + assert_eq!(XcmPallet::wrap_version(&remote, v2_msg.clone()), Err(())); + + let message = Xcm(vec![ + // Remote upgraded to XCM v2 + QueryResponse { query_id: 0, max_weight: 0, response: Response::Version(2) }, + ]); + let r = XcmExecutor::::execute_xcm(remote.clone(), message, weight); + assert_eq!(r, Outcome::Complete(weight)); + + // This message can now be sent to remote as it's v2. + assert_eq!( + XcmPallet::wrap_version(&remote, v2_msg.clone()), + Ok(VersionedXcm::from(v2_msg)) + ); + }); +} + +/// We should autosubscribe when we don't know the remote's version. +#[test] +fn auto_subscription_works() { + new_test_ext_with_balances(vec![]).execute_with(|| { + let remote0 = Parachain(1000).into(); + let remote1 = Parachain(1001).into(); + + assert_ok!(XcmPallet::force_default_xcm_version(Origin::root(), Some(1))); + + // Wrapping a version for a destination we don't know elicits a subscription. + let v1_msg = xcm::v1::Xcm::<()>::QueryResponse { + query_id: 1, + response: xcm::v1::Response::Assets(vec![].into()), + }; + let v2_msg = Xcm::<()>(vec![Trap(0)]); + assert_eq!( + XcmPallet::wrap_version(&remote0, v1_msg.clone()), + Ok(VersionedXcm::from(v1_msg.clone())), + ); + assert_eq!(XcmPallet::wrap_version(&remote0, v2_msg.clone()), Err(())); + let expected = vec![(remote0.clone().into(), 2)]; + assert_eq!(VersionDiscoveryQueue::::get().into_inner(), expected); + + assert_eq!(XcmPallet::wrap_version(&remote0, v2_msg.clone()), Err(())); + assert_eq!(XcmPallet::wrap_version(&remote1, v2_msg.clone()), Err(())); + let expected = vec![(remote0.clone().into(), 3), (remote1.clone().into(), 1)]; + assert_eq!(VersionDiscoveryQueue::::get().into_inner(), expected); + + XcmPallet::on_initialize(1); + assert_eq!( + take_sent_xcm(), + vec![( + remote0.clone(), + Xcm(vec![SubscribeVersion { query_id: 0, max_response_weight: 0 }]), + )] + ); + + // Assume remote0 is working ok and XCM version 2. + + let weight = BaseXcmWeight::get(); + let message = Xcm(vec![ + // Remote supports XCM v2 + QueryResponse { query_id: 0, max_weight: 0, response: Response::Version(2) }, + ]); + let r = XcmExecutor::::execute_xcm(remote0.clone(), message, weight); + assert_eq!(r, Outcome::Complete(weight)); + + // This message can now be sent to remote0 as it's v2. + assert_eq!( + XcmPallet::wrap_version(&remote0, v2_msg.clone()), + Ok(VersionedXcm::from(v2_msg.clone())) + ); + + XcmPallet::on_initialize(2); + assert_eq!( + take_sent_xcm(), + vec![( + remote1.clone(), + Xcm(vec![SubscribeVersion { query_id: 1, max_response_weight: 0 }]), + )] + ); + + // Assume remote1 is working ok and XCM version 1. + + let weight = BaseXcmWeight::get(); + let message = Xcm(vec![ + // Remote supports XCM v1 + QueryResponse { query_id: 1, max_weight: 0, response: Response::Version(1) }, + ]); + let r = XcmExecutor::::execute_xcm(remote1.clone(), message, weight); + assert_eq!(r, Outcome::Complete(weight)); + + // v2 messages cannot be sent to remote1... + assert_eq!(XcmPallet::wrap_version(&remote1, v1_msg.clone()), Ok(VersionedXcm::V1(v1_msg))); + assert_eq!(XcmPallet::wrap_version(&remote1, v2_msg.clone()), Err(())); + }) +} + +#[test] +fn subscription_side_upgrades_work_with_multistage_notify() { + new_test_ext_with_balances(vec![]).execute_with(|| { + AdvertisedXcmVersion::set(1); + + // An entry from a previous runtime with v0 XCM. + let v0_location = xcm::v0::MultiLocation::X1(xcm::v0::Junction::Parachain(1000)); + let v0_location = VersionedMultiLocation::from(v0_location); + VersionNotifyTargets::::insert(0, v0_location, (69, 0, 1)); + let v1_location = Parachain(1001).into().versioned(); + VersionNotifyTargets::::insert(1, v1_location, (70, 0, 1)); + let v2_location = Parachain(1002).into().versioned(); + VersionNotifyTargets::::insert(2, v2_location, (71, 0, 1)); + + // New version. + AdvertisedXcmVersion::set(2); + + // A runtime upgrade which alters the version does send notifications. + XcmPallet::on_runtime_upgrade(); + let mut maybe_migration = CurrentMigration::::take(); + let mut counter = 0; + while let Some(migration) = maybe_migration.take() { + counter += 1; + let (_, m) = XcmPallet::check_xcm_version_change(migration, 0); + maybe_migration = m; + } + assert_eq!(counter, 4); + + let instr0 = QueryResponse { query_id: 69, max_weight: 0, response: Response::Version(2) }; + let instr1 = QueryResponse { query_id: 70, max_weight: 0, response: Response::Version(2) }; + let instr2 = QueryResponse { query_id: 71, max_weight: 0, response: Response::Version(2) }; + let mut sent = take_sent_xcm(); + sent.sort_by_key(|k| match (k.1).0[0] { + QueryResponse { query_id: q, .. } => q, + _ => 0, + }); + assert_eq!( + sent, + vec![ + (Parachain(1000).into(), Xcm(vec![instr0])), + (Parachain(1001).into(), Xcm(vec![instr1])), + (Parachain(1002).into(), Xcm(vec![instr2])), + ] + ); + + let mut contents = VersionNotifyTargets::::iter().collect::>(); + contents.sort_by_key(|k| k.2); + assert_eq!( + contents, + vec![ + (2, Parachain(1000).into().versioned(), (69, 0, 2)), + (2, Parachain(1001).into().versioned(), (70, 0, 2)), + (2, Parachain(1002).into().versioned(), (71, 0, 2)), + ] + ); + }); +} diff --git a/xcm/src/lib.rs b/xcm/src/lib.rs index b9aad191ef0d..cfa5735559aa 100644 --- a/xcm/src/lib.rs +++ b/xcm/src/lib.rs @@ -45,6 +45,9 @@ pub use double_encoded::DoubleEncoded; /// Maximum nesting level for XCM decoding. pub const MAX_XCM_DECODE_DEPTH: u32 = 8; +/// A version of XCM. +pub type Version = u32; + #[derive(Clone, Eq, PartialEq, Debug)] pub enum Unsupported {} impl Encode for Unsupported {} @@ -54,6 +57,17 @@ impl Decode for Unsupported { } } +/// Attempt to convert `self` into a particular version of itself. +pub trait IntoVersion: Sized { + /// Consume `self` and return same value expressed in some particular `version` of XCM. + fn into_version(self, version: Version) -> Result; + + /// Consume `self` and return same value expressed the latest version of XCM. + fn into_latest(self) -> Result { + self.into_version(latest::VERSION) + } +} + /// A single `MultiLocation` value, together with its version code. #[derive(Derivative, Encode, Decode)] #[derivative(Clone(bound = ""), Eq(bound = ""), PartialEq(bound = ""), Debug(bound = ""))] @@ -64,6 +78,16 @@ pub enum VersionedMultiLocation { V1(v1::MultiLocation), } +impl IntoVersion for VersionedMultiLocation { + fn into_version(self, n: Version) -> Result { + Ok(match n { + 0 => Self::V0(self.try_into()?), + 1 | 2 => Self::V1(self.try_into()?), + _ => return Err(()), + }) + } +} + impl From for VersionedMultiLocation { fn from(x: v0::MultiLocation) -> Self { VersionedMultiLocation::V0(x) @@ -109,6 +133,17 @@ pub enum VersionedResponse { V2(v2::Response), } +impl IntoVersion for VersionedResponse { + fn into_version(self, n: Version) -> Result { + Ok(match n { + 0 => Self::V0(self.try_into()?), + 1 => Self::V1(self.try_into()?), + 2 => Self::V2(self.try_into()?), + _ => return Err(()), + }) + } +} + impl From for VersionedResponse { fn from(x: v0::Response) -> Self { VersionedResponse::V0(x) @@ -173,6 +208,16 @@ pub enum VersionedMultiAsset { V1(v1::MultiAsset), } +impl IntoVersion for VersionedMultiAsset { + fn into_version(self, n: Version) -> Result { + Ok(match n { + 0 => Self::V0(self.try_into()?), + 1 | 2 => Self::V1(self.try_into()?), + _ => return Err(()), + }) + } +} + impl From for VersionedMultiAsset { fn from(x: v0::MultiAsset) -> Self { VersionedMultiAsset::V0(x) @@ -217,8 +262,8 @@ pub enum VersionedMultiAssets { V1(v1::MultiAssets), } -impl VersionedMultiAssets { - pub fn into_version(self, n: u32) -> Result { +impl IntoVersion for VersionedMultiAssets { + fn into_version(self, n: Version) -> Result { Ok(match n { 0 => Self::V0(self.try_into()?), 1 | 2 => Self::V1(self.try_into()?), @@ -272,6 +317,17 @@ pub enum VersionedXcm { V2(v2::Xcm), } +impl IntoVersion for VersionedXcm { + fn into_version(self, n: Version) -> Result { + Ok(match n { + 0 => Self::V0(self.try_into()?), + 1 => Self::V1(self.try_into()?), + 2 => Self::V2(self.try_into()?), + _ => return Err(()), + }) + } +} + impl From> for VersionedXcm { fn from(x: v0::Xcm) -> Self { VersionedXcm::V0(x) @@ -383,6 +439,14 @@ pub type AlwaysLatest = AlwaysV1; /// `WrapVersion` implementation which attempts to always convert the XCM to the release version before wrapping it. pub type AlwaysRelease = AlwaysV0; +pub mod prelude { + pub use super::{ + latest::prelude::*, AlwaysLatest, AlwaysRelease, AlwaysV0, AlwaysV1, AlwaysV2, IntoVersion, + Unsupported, Version as XcmVersion, VersionedMultiAsset, VersionedMultiAssets, + VersionedMultiLocation, VersionedResponse, VersionedXcm, WrapVersion, + }; +} + pub mod opaque { pub mod v0 { // Everything from v0 diff --git a/xcm/src/v0/mod.rs b/xcm/src/v0/mod.rs index dd043554ef8b..9ec5150242be 100644 --- a/xcm/src/v0/mod.rs +++ b/xcm/src/v0/mod.rs @@ -326,6 +326,7 @@ impl TryFrom for Response { fn try_from(new_response: Response1) -> result::Result { Ok(match new_response { Response1::Assets(assets) => Self::Assets(assets.try_into()?), + Response1::Version(..) => return Err(()), }) } } @@ -379,6 +380,7 @@ impl TryFrom> for Xcm { who: MultiLocation1 { interior: who, parents: 0 }.try_into()?, message: alloc::boxed::Box::new((*message).try_into()?), }, + Xcm1::SubscribeVersion { .. } | Xcm1::UnsubscribeVersion => return Err(()), }) } } diff --git a/xcm/src/v1/mod.rs b/xcm/src/v1/mod.rs index dc0f6a251a98..02bbd5f42712 100644 --- a/xcm/src/v1/mod.rs +++ b/xcm/src/v1/mod.rs @@ -79,6 +79,8 @@ pub mod prelude { pub enum Response { /// Some assets. Assets(MultiAssets), + /// An XCM version. + Version(super::Version), } /// Cross-Consensus Message: A message from one consensus system to another. @@ -270,6 +272,25 @@ pub enum Xcm { /// Errors: #[codec(index = 10)] RelayedFrom { who: InteriorMultiLocation, message: alloc::boxed::Box> }, + + /// Ask the destination system to respond with the most recent version of XCM that they + /// support in a `QueryResponse` instruction. Any changes to this should also elicit similar + /// responses when they happen. + /// + /// Kind: *Instruction* + #[codec(index = 11)] + SubscribeVersion { + #[codec(compact)] + query_id: u64, + #[codec(compact)] + max_response_weight: u64, + }, + + /// Cancel the effect of a previous `SubscribeVersion` instruction. + /// + /// Kind: *Instruction* + #[codec(index = 12)] + UnsubscribeVersion, } impl Xcm { @@ -302,6 +323,9 @@ impl Xcm { Transact { origin_type, require_weight_at_most, call: call.into() }, RelayedFrom { who, message } => RelayedFrom { who, message: alloc::boxed::Box::new((*message).into()) }, + SubscribeVersion { query_id, max_response_weight } => + SubscribeVersion { query_id, max_response_weight }, + UnsubscribeVersion => UnsubscribeVersion, } } } @@ -427,6 +451,9 @@ impl TryFrom> for Xcm { HrmpChannelClosing { initiator, sender, recipient }, Instruction::Transact { origin_type, require_weight_at_most, call } => Transact { origin_type, require_weight_at_most, call }, + Instruction::SubscribeVersion { query_id, max_response_weight } => + SubscribeVersion { query_id, max_response_weight }, + Instruction::UnsubscribeVersion => UnsubscribeVersion, _ => return Err(()), }) } @@ -438,6 +465,7 @@ impl TryFrom for Response { fn try_from(response: NewResponse) -> result::Result { match response { NewResponse::Assets(assets) => Ok(Self::Assets(assets)), + NewResponse::Version(version) => Ok(Self::Version(version)), _ => Err(()), } } diff --git a/xcm/src/v1/multilocation.rs b/xcm/src/v1/multilocation.rs index f2c255bd1125..a2a087443b3c 100644 --- a/xcm/src/v1/multilocation.rs +++ b/xcm/src/v1/multilocation.rs @@ -54,23 +54,28 @@ pub struct MultiLocation { pub interior: Junctions, } -/// A relative location which is constrained to be an interior location of the context. -/// -/// See also `MultiLocation`. -pub type InteriorMultiLocation = Junctions; - impl Default for MultiLocation { fn default() -> Self { - Self::here() + Self { parents: 0, interior: Junctions::Here } } } +/// A relative location which is constrained to be an interior location of the context. +/// +/// See also `MultiLocation`. +pub type InteriorMultiLocation = Junctions; + impl MultiLocation { /// Creates a new `MultiLocation` with the given number of parents and interior junctions. pub fn new(parents: u8, junctions: Junctions) -> MultiLocation { MultiLocation { parents, interior: junctions } } + /// Consume `self` and return the equivalent `VersionedMultiLocation` value. + pub fn versioned(self) -> crate::VersionedMultiLocation { + self.into() + } + /// Creates a new `MultiLocation` with 0 parents and a `Here` interior. /// /// The resulting `MultiLocation` can be interpreted as the "current consensus system". diff --git a/xcm/src/v2/mod.rs b/xcm/src/v2/mod.rs index c89c20df747d..533aef468701 100644 --- a/xcm/src/v2/mod.rs +++ b/xcm/src/v2/mod.rs @@ -37,6 +37,12 @@ pub use super::v1::{ MultiLocation, NetworkId, OriginKind, Parent, ParentThen, WildFungibility, WildMultiAsset, }; +/// This module's XCM version. +pub const VERSION: super::Version = 2; + +/// An identifier for a query. +pub type QueryId = u64; + #[derive(Derivative, Default, Encode, Decode)] #[derivative(Clone(bound = ""), Eq(bound = ""), PartialEq(bound = ""), Debug(bound = ""))] #[codec(encode_bound())] @@ -116,11 +122,12 @@ pub mod prelude { MultiAssetFilter::{self, *}, MultiAssets, MultiLocation, NetworkId::{self, *}, - OriginKind, Outcome, Parent, ParentThen, Response, Result as XcmResult, SendError, - SendResult, SendXcm, + OriginKind, Outcome, Parent, ParentThen, QueryId, Response, Result as XcmResult, + SendError, SendResult, SendXcm, WeightLimit::{self, *}, WildFungibility::{self, Fungible as WildFungible, NonFungible as WildNonFungible}, WildMultiAsset::{self, *}, + VERSION as XCM_VERSION, }; } pub use super::{Instruction, Xcm}; @@ -142,6 +149,8 @@ pub enum Response { Assets(MultiAssets), /// The outcome of an XCM instruction. ExecutionResult(result::Result<(), (u32, Error)>), + /// An XCM version. + Version(super::Version), } impl Default for Response { @@ -239,7 +248,7 @@ pub enum Instruction { /// Errors: QueryResponse { #[codec(compact)] - query_id: u64, + query_id: QueryId, response: Response, #[codec(compact)] max_weight: u64, @@ -291,7 +300,12 @@ pub enum Instruction { /// Kind: *Instruction*. /// /// Errors: - Transact { origin_type: OriginKind, require_weight_at_most: u64, call: DoubleEncoded }, + Transact { + origin_type: OriginKind, + #[codec(compact)] + require_weight_at_most: u64, + call: DoubleEncoded, + }, /// A message to notify about a new incoming HRMP channel. This message is meant to be sent by the /// relay-chain to a para. @@ -377,7 +391,7 @@ pub enum Instruction { /// Errors: ReportError { #[codec(compact)] - query_id: u64, + query_id: QueryId, dest: MultiLocation, #[codec(compact)] max_response_weight: u64, @@ -395,7 +409,12 @@ pub enum Instruction { /// Kind: *Instruction* /// /// Errors: - DepositAsset { assets: MultiAssetFilter, max_assets: u32, beneficiary: MultiLocation }, + DepositAsset { + assets: MultiAssetFilter, + #[codec(compact)] + max_assets: u32, + beneficiary: MultiLocation, + }, /// Remove the asset(s) (`assets`) from the Holding Register and place equivalent assets under /// the ownership of `dest` within this consensus system (i.e. deposit them into its sovereign @@ -418,6 +437,7 @@ pub enum Instruction { /// Errors: DepositReserveAsset { assets: MultiAssetFilter, + #[codec(compact)] max_assets: u32, dest: MultiLocation, xcm: Xcm<()>, @@ -487,7 +507,7 @@ pub enum Instruction { /// Errors: QueryHolding { #[codec(compact)] - query_id: u64, + query_id: QueryId, dest: MultiLocation, assets: MultiAssetFilter, #[codec(compact)] @@ -571,7 +591,24 @@ pub enum Instruction { /// /// Errors: /// - `Trap`: All circumstances, whose inner value is the same as this item's inner value. - Trap(u64), + Trap(#[codec(compact)] u64), + + /// Ask the destination system to respond with the most recent version of XCM that they + /// support in a `QueryResponse` instruction. Any changes to this should also elicit similar + /// responses when they happen. + /// + /// Kind: *Instruction* + SubscribeVersion { + #[codec(compact)] + query_id: QueryId, + #[codec(compact)] + max_response_weight: u64, + }, + + /// Cancel the effect of a previous `SubscribeVersion` instruction. + /// + /// Kind: *Instruction* + UnsubscribeVersion, } impl Xcm { @@ -626,6 +663,9 @@ impl Instruction { ClearError => ClearError, ClaimAsset { assets, ticket } => ClaimAsset { assets, ticket }, Trap(code) => Trap(code), + SubscribeVersion { query_id, max_response_weight } => + SubscribeVersion { query_id, max_response_weight }, + UnsubscribeVersion => UnsubscribeVersion, } } } @@ -646,6 +686,7 @@ impl TryFrom for Response { fn try_from(old_response: OldResponse) -> result::Result { match old_response { OldResponse::Assets(assets) => Ok(Self::Assets(assets)), + OldResponse::Version(version) => Ok(Self::Version(version)), } } } @@ -695,6 +736,9 @@ impl TryFrom> for Xcm { vec![Transact { origin_type, require_weight_at_most, call }], // We don't handle this one at all due to nested XCM. OldXcm::RelayedFrom { .. } => return Err(()), + OldXcm::SubscribeVersion { query_id, max_response_weight } => + vec![SubscribeVersion { query_id, max_response_weight }], + OldXcm::UnsubscribeVersion => vec![UnsubscribeVersion], })) } } diff --git a/xcm/src/v2/traits.rs b/xcm/src/v2/traits.rs index 0db5f7ef3ba7..0349a7de193f 100644 --- a/xcm/src/v2/traits.rs +++ b/xcm/src/v2/traits.rs @@ -99,6 +99,8 @@ pub enum Error { Trap(u64), /// The given claim could not be recognized/found. UnknownClaim, + /// The location given was invalid for some reason specific to the operation at hand. + InvalidLocation, } impl From<()> for Error { diff --git a/xcm/xcm-builder/src/barriers.rs b/xcm/xcm-builder/src/barriers.rs index d0261d5d09ef..b1305666fa09 100644 --- a/xcm/xcm-builder/src/barriers.rs +++ b/xcm/xcm-builder/src/barriers.rs @@ -31,7 +31,6 @@ pub struct TakeWeightCredit; impl ShouldExecute for TakeWeightCredit { fn should_execute( _origin: &MultiLocation, - _top_level: bool, _message: &mut Xcm, max_weight: Weight, weight_credit: &mut Weight, @@ -44,19 +43,17 @@ impl ShouldExecute for TakeWeightCredit { /// Allows execution from `origin` if it is contained in `T` (i.e. `T::Contains(origin)`) taking /// payments into account. /// -/// Only allows for `TeleportAsset`, `WithdrawAsset` and `ReserveAssetDeposit` XCMs because they are -/// the only ones that place assets in the Holding Register to pay for execution. +/// Only allows for `TeleportAsset`, `WithdrawAsset`, `ClaimAsset` and `ReserveAssetDeposit` XCMs +/// because they are the only ones that place assets in the Holding Register to pay for execution. pub struct AllowTopLevelPaidExecutionFrom(PhantomData); impl> ShouldExecute for AllowTopLevelPaidExecutionFrom { fn should_execute( origin: &MultiLocation, - top_level: bool, message: &mut Xcm, max_weight: Weight, _weight_credit: &mut Weight, ) -> Result<(), ()> { ensure!(T::contains(origin), ()); - ensure!(top_level, ()); let mut iter = message.0.iter_mut(); let i = iter.next().ok_or(())?; match i { @@ -90,7 +87,6 @@ pub struct AllowUnpaidExecutionFrom(PhantomData); impl> ShouldExecute for AllowUnpaidExecutionFrom { fn should_execute( origin: &MultiLocation, - _top_level: bool, _message: &mut Xcm, _max_weight: Weight, _weight_credit: &mut Weight, @@ -117,7 +113,6 @@ pub struct AllowKnownQueryResponses(PhantomData ShouldExecute for AllowKnownQueryResponses { fn should_execute( origin: &MultiLocation, - _top_level: bool, message: &mut Xcm, _max_weight: Weight, _weight_credit: &mut Weight, @@ -130,3 +125,21 @@ impl ShouldExecute for AllowKnownQueryResponses(PhantomData); +impl> ShouldExecute for AllowSubscriptionsFrom { + fn should_execute( + origin: &MultiLocation, + message: &mut Xcm, + _max_weight: Weight, + _weight_credit: &mut Weight, + ) -> Result<(), ()> { + ensure!(T::contains(origin), ()); + match (message.0.len(), message.0.first()) { + (1, Some(SubscribeVersion { .. })) | (1, Some(UnsubscribeVersion)) => Ok(()), + _ => Err(()), + } + } +} diff --git a/xcm/xcm-builder/src/lib.rs b/xcm/xcm-builder/src/lib.rs index d2e2d2e23f38..e2caff56bf36 100644 --- a/xcm/xcm-builder/src/lib.rs +++ b/xcm/xcm-builder/src/lib.rs @@ -41,8 +41,8 @@ pub use origin_conversion::{ mod barriers; pub use barriers::{ - AllowKnownQueryResponses, AllowTopLevelPaidExecutionFrom, AllowUnpaidExecutionFrom, - IsChildSystemParachain, TakeWeightCredit, + AllowKnownQueryResponses, AllowSubscriptionsFrom, AllowTopLevelPaidExecutionFrom, + AllowUnpaidExecutionFrom, IsChildSystemParachain, TakeWeightCredit, }; mod currency_adapter; diff --git a/xcm/xcm-builder/src/mock.rs b/xcm/xcm-builder/src/mock.rs index 3f00b5019558..8f6759582f9a 100644 --- a/xcm/xcm-builder/src/mock.rs +++ b/xcm/xcm-builder/src/mock.rs @@ -14,6 +14,7 @@ // You should have received a copy of the GNU General Public License // along with Polkadot. If not, see . +use crate::barriers::AllowSubscriptionsFrom; pub use crate::{ AllowKnownQueryResponses, AllowTopLevelPaidExecutionFrom, AllowUnpaidExecutionFrom, FixedRateOfFungible, FixedWeightBounds, LocationInverter, TakeWeightCredit, @@ -35,7 +36,7 @@ pub use sp_std::{ marker::PhantomData, }; pub use xcm::latest::prelude::*; -use xcm_executor::traits::{ClaimAssets, DropAssets}; +use xcm_executor::traits::{ClaimAssets, DropAssets, VersionChangeNotifier}; pub use xcm_executor::{ traits::{ConvertOrigin, FilterAssetLocation, InvertLocation, OnResponse, TransactAsset}, Assets, Config, @@ -258,6 +259,7 @@ parameter_types! { // Nothing is allowed to be paid/unpaid by default. pub static AllowUnpaidFrom: Vec = vec![]; pub static AllowPaidFrom: Vec = vec![]; + pub static AllowSubsFrom: Vec = vec![]; // 1_000_000_000_000 => 1 unit of asset for 1 unit of Weight. pub static WeightPrice: (AssetId, u128) = (From::from(Here), 1_000_000_000_000); pub static MaxInstructions: u32 = 100; @@ -268,6 +270,7 @@ pub type TestBarrier = ( AllowKnownQueryResponses, AllowTopLevelPaidExecutionFrom>, AllowUnpaidExecutionFrom>, + AllowSubscriptionsFrom>, ); parameter_types! { @@ -301,6 +304,26 @@ impl ClaimAssets for TestAssetTrap { } } +parameter_types! { + pub static SubscriptionRequests: Vec<(MultiLocation, Option<(QueryId, u64)>)> = vec![]; +} +pub struct TestSubscriptionService; + +impl VersionChangeNotifier for TestSubscriptionService { + fn start(location: &MultiLocation, query_id: QueryId, max_weight: u64) -> XcmResult { + let mut r = SubscriptionRequests::get(); + r.push((location.clone(), Some((query_id, max_weight)))); + SubscriptionRequests::set(r); + Ok(()) + } + fn stop(location: &MultiLocation) -> XcmResult { + let mut r = SubscriptionRequests::get(); + r.push((location.clone(), None)); + SubscriptionRequests::set(r); + Ok(()) + } +} + pub struct TestConfig; impl Config for TestConfig { type Call = TestCall; @@ -316,4 +339,5 @@ impl Config for TestConfig { type ResponseHandler = TestResponseHandler; type AssetTrap = TestAssetTrap; type AssetClaims = TestAssetTrap; + type SubscriptionService = TestSubscriptionService; } diff --git a/xcm/xcm-builder/src/tests.rs b/xcm/xcm-builder/src/tests.rs index c7913c1281b0..746d9cbb6ce7 100644 --- a/xcm/xcm-builder/src/tests.rs +++ b/xcm/xcm-builder/src/tests.rs @@ -1,4 +1,4 @@ -// Copyright 2020 Parity Technologies (UK) Ltd. +// Copyright 2020 Parity Technologies query_id: (), max_response_weight: () query_id: (), max_response_weight: () (UK) Ltd. // This file is part of Polkadot. // Polkadot is free software: you can redistribute it and/or modify @@ -57,23 +57,11 @@ fn take_weight_credit_barrier_should_work() { let mut message = Xcm::<()>(vec![TransferAsset { assets: (Parent, 100).into(), beneficiary: Here.into() }]); let mut weight_credit = 10; - let r = TakeWeightCredit::should_execute( - &Parent.into(), - true, - &mut message, - 10, - &mut weight_credit, - ); + let r = TakeWeightCredit::should_execute(&Parent.into(), &mut message, 10, &mut weight_credit); assert_eq!(r, Ok(())); assert_eq!(weight_credit, 0); - let r = TakeWeightCredit::should_execute( - &Parent.into(), - true, - &mut message, - 10, - &mut weight_credit, - ); + let r = TakeWeightCredit::should_execute(&Parent.into(), &mut message, 10, &mut weight_credit); assert_eq!(r, Err(())); assert_eq!(weight_credit, 0); } @@ -87,7 +75,6 @@ fn allow_unpaid_should_work() { let r = AllowUnpaidExecutionFrom::>::should_execute( &Parachain(1).into(), - true, &mut message, 10, &mut 0, @@ -96,7 +83,6 @@ fn allow_unpaid_should_work() { let r = AllowUnpaidExecutionFrom::>::should_execute( &Parent.into(), - true, &mut message, 10, &mut 0, @@ -113,7 +99,6 @@ fn allow_paid_should_work() { let r = AllowTopLevelPaidExecutionFrom::>::should_execute( &Parachain(1).into(), - true, &mut message, 10, &mut 0, @@ -129,7 +114,6 @@ fn allow_paid_should_work() { let r = AllowTopLevelPaidExecutionFrom::>::should_execute( &Parent.into(), - true, &mut underpaying_message, 30, &mut 0, @@ -145,7 +129,6 @@ fn allow_paid_should_work() { let r = AllowTopLevelPaidExecutionFrom::>::should_execute( &Parachain(1).into(), - true, &mut paying_message, 30, &mut 0, @@ -154,7 +137,6 @@ fn allow_paid_should_work() { let r = AllowTopLevelPaidExecutionFrom::>::should_execute( &Parent.into(), - true, &mut paying_message, 30, &mut 0, @@ -480,6 +462,120 @@ fn reserve_transfer_should_work() { ); } +#[test] +fn simple_version_subscriptions_should_work() { + AllowSubsFrom::set(vec![Parent.into()]); + + let origin = Parachain(1000).into(); + let message = Xcm::(vec![ + SetAppendix(Xcm(vec![])), + SubscribeVersion { query_id: 42, max_response_weight: 5000 }, + ]); + let weight_limit = 20; + let r = XcmExecutor::::execute_xcm(origin, message, weight_limit); + assert_eq!(r, Outcome::Error(XcmError::Barrier)); + + let origin = Parachain(1000).into(); + let message = + Xcm::(vec![SubscribeVersion { query_id: 42, max_response_weight: 5000 }]); + let weight_limit = 10; + let r = XcmExecutor::::execute_xcm(origin, message.clone(), weight_limit); + assert_eq!(r, Outcome::Error(XcmError::Barrier)); + + let origin = Parent.into(); + let r = XcmExecutor::::execute_xcm(origin, message, weight_limit); + assert_eq!(r, Outcome::Complete(10)); + + assert_eq!(SubscriptionRequests::get(), vec![(Parent.into(), Some((42, 5000)))]); +} + +#[test] +fn version_subscription_instruction_should_work() { + let origin = Parachain(1000).into(); + let message = Xcm::(vec![ + DescendOrigin(X1(AccountIndex64 { index: 1, network: Any })), + SubscribeVersion { query_id: 42, max_response_weight: 5000 }, + ]); + let weight_limit = 20; + let r = XcmExecutor::::execute_xcm_in_credit( + origin.clone(), + message.clone(), + weight_limit, + weight_limit, + ); + assert_eq!(r, Outcome::Incomplete(20, XcmError::BadOrigin)); + + let message = Xcm::(vec![ + SetAppendix(Xcm(vec![])), + SubscribeVersion { query_id: 42, max_response_weight: 5000 }, + ]); + let r = XcmExecutor::::execute_xcm_in_credit( + origin, + message.clone(), + weight_limit, + weight_limit, + ); + assert_eq!(r, Outcome::Complete(20)); + + assert_eq!(SubscriptionRequests::get(), vec![(Parachain(1000).into(), Some((42, 5000)))]); +} + +#[test] +fn simple_version_unsubscriptions_should_work() { + AllowSubsFrom::set(vec![Parent.into()]); + + let origin = Parachain(1000).into(); + let message = Xcm::(vec![SetAppendix(Xcm(vec![])), UnsubscribeVersion]); + let weight_limit = 20; + let r = XcmExecutor::::execute_xcm(origin, message, weight_limit); + assert_eq!(r, Outcome::Error(XcmError::Barrier)); + + let origin = Parachain(1000).into(); + let message = Xcm::(vec![UnsubscribeVersion]); + let weight_limit = 10; + let r = XcmExecutor::::execute_xcm(origin, message.clone(), weight_limit); + assert_eq!(r, Outcome::Error(XcmError::Barrier)); + + let origin = Parent.into(); + let r = XcmExecutor::::execute_xcm(origin, message, weight_limit); + assert_eq!(r, Outcome::Complete(10)); + + assert_eq!(SubscriptionRequests::get(), vec![(Parent.into(), None)]); + assert_eq!(sent_xcm(), vec![]); +} + +#[test] +fn version_unsubscription_instruction_should_work() { + let origin = Parachain(1000).into(); + + // Not allowed to do it when origin has been changed. + let message = Xcm::(vec![ + DescendOrigin(X1(AccountIndex64 { index: 1, network: Any })), + UnsubscribeVersion, + ]); + let weight_limit = 20; + let r = XcmExecutor::::execute_xcm_in_credit( + origin.clone(), + message.clone(), + weight_limit, + weight_limit, + ); + assert_eq!(r, Outcome::Incomplete(20, XcmError::BadOrigin)); + + // Fine to do it when origin is untouched. + let message = Xcm::(vec![SetAppendix(Xcm(vec![])), UnsubscribeVersion]); + let r = XcmExecutor::::execute_xcm_in_credit( + origin, + message.clone(), + weight_limit, + weight_limit, + ); + assert_eq!(r, Outcome::Complete(20)); + + assert_eq!(SubscriptionRequests::get(), vec![(Parachain(1000).into(), None)]); + assert_eq!(sent_xcm(), vec![]); +} + #[test] fn transacting_should_work() { AllowUnpaidFrom::set(vec![Parent.into()]); diff --git a/xcm/xcm-builder/tests/mock/mod.rs b/xcm/xcm-builder/tests/mock/mod.rs index e28352485fb5..ee5c77afe587 100644 --- a/xcm/xcm-builder/tests/mock/mod.rs +++ b/xcm/xcm-builder/tests/mock/mod.rs @@ -169,6 +169,7 @@ impl xcm_executor::Config for XcmConfig { type ResponseHandler = XcmPallet; type AssetTrap = XcmPallet; type AssetClaims = XcmPallet; + type SubscriptionService = XcmPallet; } pub type LocalOriginToLocation = SignedToAccountId32; @@ -187,6 +188,8 @@ impl pallet_xcm::Config for Runtime { type Weigher = FixedWeightBounds; type Call = Call; type Origin = Origin; + const VERSION_DISCOVERY_QUEUE_SIZE: u32 = 100; + type AdvertisedXcmVersion = pallet_xcm::CurrentXcmVersion; } impl origin::Config for Runtime {} diff --git a/xcm/xcm-executor/src/config.rs b/xcm/xcm-executor/src/config.rs index 153a9de9a794..e7d81dc8328c 100644 --- a/xcm/xcm-executor/src/config.rs +++ b/xcm/xcm-executor/src/config.rs @@ -16,7 +16,7 @@ use crate::traits::{ ClaimAssets, ConvertOrigin, DropAssets, FilterAssetLocation, InvertLocation, OnResponse, - ShouldExecute, TransactAsset, WeightBounds, WeightTrader, + ShouldExecute, TransactAsset, VersionChangeNotifier, WeightBounds, WeightTrader, }; use frame_support::{ dispatch::{Dispatchable, Parameter}, @@ -65,4 +65,7 @@ pub trait Config { /// The handler for when there is an instruction to claim assets. type AssetClaims: ClaimAssets; + + /// How we handle version subscription requests. + type SubscriptionService: VersionChangeNotifier; } diff --git a/xcm/xcm-executor/src/lib.rs b/xcm/xcm-executor/src/lib.rs index 85daaa4a5ebe..604d6baa328d 100644 --- a/xcm/xcm-executor/src/lib.rs +++ b/xcm/xcm-executor/src/lib.rs @@ -32,7 +32,7 @@ use xcm::latest::{ pub mod traits; use traits::{ ClaimAssets, ConvertOrigin, DropAssets, FilterAssetLocation, InvertLocation, OnResponse, - ShouldExecute, TransactAsset, WeightBounds, WeightTrader, + ShouldExecute, TransactAsset, VersionChangeNotifier, WeightBounds, WeightTrader, }; mod assets; @@ -44,8 +44,9 @@ pub use config::Config; pub struct XcmExecutor { holding: Assets, origin: Option, + original_origin: MultiLocation, trader: Config::Trader, - /// The most recent error result and instruction index into the fragment in which it occured, + /// The most recent error result and instruction index into the fragment in which it occurred, /// if any. error: Option<(u32, XcmError)>, /// The surplus weight, defined as the amount by which `max_weight` is @@ -87,17 +88,13 @@ impl ExecuteXcm for XcmExecutor { return Outcome::Error(XcmError::WeightLimitReached(xcm_weight)) } - if let Err(_) = Config::Barrier::should_execute( - &origin, - true, - &mut message, - xcm_weight, - &mut weight_credit, - ) { + if let Err(_) = + Config::Barrier::should_execute(&origin, &mut message, xcm_weight, &mut weight_credit) + { return Outcome::Error(XcmError::Barrier) } - let mut vm = Self::new(origin.clone()); + let mut vm = Self::new(origin); while !message.0.is_empty() { let result = vm.execute(message); @@ -118,7 +115,8 @@ impl ExecuteXcm for XcmExecutor { let mut weight_used = xcm_weight.saturating_sub(vm.total_surplus); if !vm.holding.is_empty() { - weight_used.saturating_accrue(Config::AssetTrap::drop_assets(&origin, vm.holding)); + let trap_weight = Config::AssetTrap::drop_assets(&vm.original_origin, vm.holding); + weight_used.saturating_accrue(trap_weight); }; match vm.error { @@ -134,7 +132,8 @@ impl XcmExecutor { fn new(origin: MultiLocation) -> Self { Self { holding: Assets::new(), - origin: Some(origin), + origin: Some(origin.clone()), + original_origin: origin, trader: Config::Trader::new(), error: None, total_surplus: 0, @@ -419,6 +418,18 @@ impl XcmExecutor { Ok(()) }, Trap(code) => Err(XcmError::Trap(code)), + SubscribeVersion { query_id, max_response_weight } => { + let origin = self.origin.as_ref().ok_or(XcmError::BadOrigin)?.clone(); + // We don't allow derivative origins to subscribe since it would otherwise pose a + // DoS risk. + ensure!(self.original_origin == origin, XcmError::BadOrigin); + Config::SubscriptionService::start(&origin, query_id, max_response_weight) + }, + UnsubscribeVersion => { + let origin = self.origin.as_ref().ok_or(XcmError::BadOrigin)?; + ensure!(&self.original_origin == origin, XcmError::BadOrigin); + Config::SubscriptionService::stop(origin) + }, ExchangeAsset { .. } => Err(XcmError::Unimplemented), HrmpNewChannelOpenRequest { .. } => Err(XcmError::Unimplemented), HrmpChannelAccepted { .. } => Err(XcmError::Unimplemented), diff --git a/xcm/xcm-executor/src/traits/mod.rs b/xcm/xcm-executor/src/traits/mod.rs index 94ef8bd4bd0f..1312771e719b 100644 --- a/xcm/xcm-executor/src/traits/mod.rs +++ b/xcm/xcm-executor/src/traits/mod.rs @@ -27,7 +27,7 @@ pub use matches_fungible::MatchesFungible; mod matches_fungibles; pub use matches_fungibles::{Error, MatchesFungibles}; mod on_response; -pub use on_response::OnResponse; +pub use on_response::{OnResponse, VersionChangeNotifier}; mod should_execute; pub use should_execute::ShouldExecute; mod transact_asset; diff --git a/xcm/xcm-executor/src/traits/on_response.rs b/xcm/xcm-executor/src/traits/on_response.rs index 158e448161e2..a34d5264c093 100644 --- a/xcm/xcm-executor/src/traits/on_response.rs +++ b/xcm/xcm-executor/src/traits/on_response.rs @@ -15,7 +15,7 @@ // along with Polkadot. If not, see . use frame_support::weights::Weight; -use xcm::latest::{MultiLocation, Response}; +use xcm::latest::{Error as XcmError, MultiLocation, QueryId, Response, Result as XcmResult}; /// Define what needs to be done upon receiving a query response. pub trait OnResponse { @@ -42,3 +42,29 @@ impl OnResponse for () { 0 } } + +/// Trait for a type which handles notifying a destination of XCM version changes. +pub trait VersionChangeNotifier { + /// Start notifying `location` should the XCM version of this chain change. + /// + /// When it does, this type should ensure a `QueryResponse` message is sent with the given + /// `query_id` & `max_weight` and with a `response` of `Repsonse::Version`. This should happen + /// until/unless `stop` is called with the correct `query_id`. + /// + /// If the `location` has an ongoing notification and when this function is called, then an + /// error should be returned. + fn start(location: &MultiLocation, query_id: QueryId, max_weight: u64) -> XcmResult; + + /// Stop notifying `location` should the XCM change. Returns an error if there is no existing + /// notification set up. + fn stop(location: &MultiLocation) -> XcmResult; +} + +impl VersionChangeNotifier for () { + fn start(_: &MultiLocation, _: QueryId, _: u64) -> XcmResult { + Err(XcmError::Unimplemented) + } + fn stop(_: &MultiLocation) -> XcmResult { + Err(XcmError::Unimplemented) + } +} diff --git a/xcm/xcm-executor/src/traits/should_execute.rs b/xcm/xcm-executor/src/traits/should_execute.rs index 08c74334517b..5f94db0066b4 100644 --- a/xcm/xcm-executor/src/traits/should_execute.rs +++ b/xcm/xcm-executor/src/traits/should_execute.rs @@ -26,8 +26,6 @@ pub trait ShouldExecute { /// Returns `true` if the given `message` may be executed. /// /// - `origin`: The origin (sender) of the message. - /// - `top_level`: `true` indicates the initial XCM coming from the `origin`, `false` indicates - /// an embedded XCM executed internally as part of another message or an `Order`. /// - `message`: The message itself. /// - `max_weight`: The (possibly over-) estimation of the weight of execution of the message. /// - `weight_credit`: The pre-established amount of weight that the system has determined this @@ -35,7 +33,6 @@ pub trait ShouldExecute { /// payment, but could in principle be due to other factors. fn should_execute( origin: &MultiLocation, - top_level: bool, message: &mut Xcm, max_weight: Weight, weight_credit: &mut Weight, @@ -46,22 +43,20 @@ pub trait ShouldExecute { impl ShouldExecute for Tuple { fn should_execute( origin: &MultiLocation, - top_level: bool, message: &mut Xcm, max_weight: Weight, weight_credit: &mut Weight, ) -> Result<(), ()> { for_tuples!( #( - match Tuple::should_execute(origin, top_level, message, max_weight, weight_credit) { + match Tuple::should_execute(origin, message, max_weight, weight_credit) { Ok(()) => return Ok(()), _ => (), } )* ); log::trace!( target: "xcm::should_execute", - "did not pass barrier: origin: {:?}, top_level: {:?}, message: {:?}, max_weight: {:?}, weight_credit: {:?}", + "did not pass barrier: origin: {:?}, message: {:?}, max_weight: {:?}, weight_credit: {:?}", origin, - top_level, message, max_weight, weight_credit, diff --git a/xcm/xcm-simulator/example/src/parachain.rs b/xcm/xcm-simulator/example/src/parachain.rs index faa2d8b3fc0a..0d2c74f8ba7c 100644 --- a/xcm/xcm-simulator/example/src/parachain.rs +++ b/xcm/xcm-simulator/example/src/parachain.rs @@ -145,6 +145,7 @@ impl Config for XcmConfig { type ResponseHandler = (); type AssetTrap = (); type AssetClaims = (); + type SubscriptionService = (); } #[frame_support::pallet] @@ -305,6 +306,8 @@ impl pallet_xcm::Config for Runtime { type LocationInverter = LocationInverter; type Origin = Origin; type Call = Call; + const VERSION_DISCOVERY_QUEUE_SIZE: u32 = 100; + type AdvertisedXcmVersion = pallet_xcm::CurrentXcmVersion; } type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; diff --git a/xcm/xcm-simulator/example/src/relay_chain.rs b/xcm/xcm-simulator/example/src/relay_chain.rs index f3173b25e075..3b27d800a510 100644 --- a/xcm/xcm-simulator/example/src/relay_chain.rs +++ b/xcm/xcm-simulator/example/src/relay_chain.rs @@ -135,6 +135,7 @@ impl Config for XcmConfig { type ResponseHandler = (); type AssetTrap = (); type AssetClaims = (); + type SubscriptionService = (); } pub type LocalOriginToLocation = SignedToAccountId32; @@ -153,6 +154,8 @@ impl pallet_xcm::Config for Runtime { type LocationInverter = LocationInverter; type Origin = Origin; type Call = Call; + const VERSION_DISCOVERY_QUEUE_SIZE: u32 = 100; + type AdvertisedXcmVersion = pallet_xcm::CurrentXcmVersion; } parameter_types! {