diff --git a/node/test/service/src/lib.rs b/node/test/service/src/lib.rs index bf5193469fa3..f8207829852b 100644 --- a/node/test/service/src/lib.rs +++ b/node/test/service/src/lib.rs @@ -276,7 +276,7 @@ impl PolkadotTestNode { function: impl Into, caller: Sr25519Keyring, ) -> Result { - let extrinsic = construct_extrinsic(&*self.client, function, caller); + let extrinsic = construct_extrinsic(&*self.client, function, caller, 0); self.rpc_handlers.send_transaction(extrinsic.into()).await } @@ -332,12 +332,12 @@ pub fn construct_extrinsic( client: &Client, function: impl Into, caller: Sr25519Keyring, + nonce: u32, ) -> UncheckedExtrinsic { let function = function.into(); let current_block_hash = client.info().best_hash; let current_block = client.info().best_number.saturated_into(); let genesis_block = client.hash(0).unwrap().unwrap(); - let nonce = 0; let period = BlockHashCount::get().checked_next_power_of_two().map(|c| c / 2).unwrap_or(2) as u64; let tip = 0; @@ -384,5 +384,5 @@ pub fn construct_transfer_extrinsic( value, )); - construct_extrinsic(client, function, origin) + construct_extrinsic(client, function, origin, 0) } diff --git a/runtime/common/src/xcm_sender.rs b/runtime/common/src/xcm_sender.rs index 602f45bbadf6..af1f783cfa9a 100644 --- a/runtime/common/src/xcm_sender.rs +++ b/runtime/common/src/xcm_sender.rs @@ -19,7 +19,7 @@ use parity_scale_codec::Encode; use runtime_parachains::{configuration, dmp}; use sp_std::marker::PhantomData; -use xcm::opaque::latest::*; +use xcm::latest::prelude::*; /// XCM sender for relay chain. It only sends downward message. pub struct ChildParachainRouter(PhantomData<(T, W)>); @@ -27,22 +27,22 @@ pub struct ChildParachainRouter(PhantomData<(T, W)>); impl SendXcm for ChildParachainRouter { - fn send_xcm(dest: MultiLocation, msg: Xcm) -> Result { + fn send_xcm(dest: MultiLocation, msg: Xcm<()>) -> SendResult { match dest { - MultiLocation { parents: 0, interior: Junctions::X1(Junction::Parachain(id)) } => { + MultiLocation { parents: 0, interior: X1(Parachain(id)) } => { // Downward message passing. let versioned_xcm = - W::wrap_version(&dest, msg).map_err(|()| Error::DestinationUnsupported)?; + W::wrap_version(&dest, msg).map_err(|()| SendError::DestinationUnsupported)?; let config = >::config(); >::queue_downward_message( &config, id.into(), versioned_xcm.encode(), ) - .map_err(Into::::into)?; + .map_err(Into::::into)?; Ok(()) }, - dest => Err(Error::CannotReachDestination(dest, msg)), + dest => Err(SendError::CannotReachDestination(dest, msg)), } } } diff --git a/runtime/kusama/src/lib.rs b/runtime/kusama/src/lib.rs index 350d70a94a7e..7dfe9008eef8 100644 --- a/runtime/kusama/src/lib.rs +++ b/runtime/kusama/src/lib.rs @@ -1254,6 +1254,9 @@ type LocalOriginConverter = ( parameter_types! { /// The amount of weight an XCM operation takes. This is a safe overestimate. pub const BaseXcmWeight: Weight = 1_000_000_000; + /// Maximum number of instructions in a single XCM fragment. A sanity check against weight + /// calculations getting too crazy. + pub const MaxInstructions: u32 = 100; } /// The XCM router. When we want to send an XCM message, we use this type. It amalgamates all of our @@ -1289,7 +1292,7 @@ impl xcm_executor::Config for XcmConfig { type IsTeleporter = TrustedTeleporters; type LocationInverter = LocationInverter; type Barrier = Barrier; - type Weigher = FixedWeightBounds; + type Weigher = FixedWeightBounds; // The weight trader piggybacks on the existing transaction-fee conversion logic. type Trader = UsingComponents>; type ResponseHandler = (); @@ -1324,8 +1327,10 @@ impl pallet_xcm::Config for Runtime { type XcmExecutor = XcmExecutor; type XcmTeleportFilter = Everything; type XcmReserveTransferFilter = Everything; - type Weigher = FixedWeightBounds; + type Weigher = FixedWeightBounds; type LocationInverter = LocationInverter; + type Origin = Origin; + type Call = Call; } parameter_types! { diff --git a/runtime/parachains/src/dmp.rs b/runtime/parachains/src/dmp.rs index 6ca7f09fc773..08def77f9c35 100644 --- a/runtime/parachains/src/dmp.rs +++ b/runtime/parachains/src/dmp.rs @@ -22,7 +22,7 @@ use frame_support::pallet_prelude::*; use primitives::v1::{DownwardMessage, Hash, Id as ParaId, InboundDownwardMessage}; use sp_runtime::traits::{BlakeTwo256, Hash as HashT, SaturatedConversion}; use sp_std::{fmt, prelude::*}; -use xcm::latest::Error as XcmError; +use xcm::latest::SendError; pub use pallet::*; @@ -33,10 +33,10 @@ pub enum QueueDownwardMessageError { ExceedsMaxMessageSize, } -impl From for XcmError { +impl From for SendError { fn from(err: QueueDownwardMessageError) -> Self { match err { - QueueDownwardMessageError::ExceedsMaxMessageSize => XcmError::ExceedsMaxMessageSize, + QueueDownwardMessageError::ExceedsMaxMessageSize => SendError::ExceedsMaxMessageSize, } } } diff --git a/runtime/parachains/src/hrmp.rs b/runtime/parachains/src/hrmp.rs index 1a85cbe9451e..c31f658faf36 100644 --- a/runtime/parachains/src/hrmp.rs +++ b/runtime/parachains/src/hrmp.rs @@ -1007,13 +1007,13 @@ impl Pallet { let notification_bytes = { use parity_scale_codec::Encode as _; - use xcm::opaque::{v1::Xcm, VersionedXcm}; + use xcm::opaque::{latest::prelude::*, VersionedXcm}; - VersionedXcm::from(Xcm::HrmpNewChannelOpenRequest { + VersionedXcm::from(Xcm(vec![HrmpNewChannelOpenRequest { sender: u32::from(origin), max_capacity: proposed_max_capacity, max_message_size: proposed_max_message_size, - }) + }])) .encode() }; if let Err(dmp::QueueDownwardMessageError::ExceedsMaxMessageSize) = @@ -1066,9 +1066,9 @@ impl Pallet { let notification_bytes = { use parity_scale_codec::Encode as _; - use xcm::opaque::{v1::Xcm, VersionedXcm}; - - VersionedXcm::from(Xcm::HrmpChannelAccepted { recipient: u32::from(origin) }).encode() + use xcm::opaque::{latest::prelude::*, VersionedXcm}; + let xcm = Xcm(vec![HrmpChannelAccepted { recipient: u32::from(origin) }]); + VersionedXcm::from(xcm).encode() }; if let Err(dmp::QueueDownwardMessageError::ExceedsMaxMessageSize) = >::queue_downward_message(&config, sender, notification_bytes) @@ -1106,13 +1106,13 @@ impl Pallet { let config = >::config(); let notification_bytes = { use parity_scale_codec::Encode as _; - use xcm::opaque::{v1::Xcm, VersionedXcm}; + use xcm::opaque::{latest::prelude::*, VersionedXcm}; - VersionedXcm::from(Xcm::HrmpChannelClosing { + VersionedXcm::from(Xcm(vec![HrmpChannelClosing { initiator: u32::from(origin), sender: u32::from(channel_id.sender), recipient: u32::from(channel_id.recipient), - }) + }])) .encode() }; let opposite_party = diff --git a/runtime/rococo/src/lib.rs b/runtime/rococo/src/lib.rs index 35d78487eca9..1ae220330b1c 100644 --- a/runtime/rococo/src/lib.rs +++ b/runtime/rococo/src/lib.rs @@ -631,6 +631,7 @@ parameter_types! { pub const RococoForTrick: (MultiAssetFilter, MultiLocation) = (Rococo::get(), Parachain(110).into()); pub const RococoForTrack: (MultiAssetFilter, MultiLocation) = (Rococo::get(), Parachain(120).into()); pub const RococoForStatemint: (MultiAssetFilter, MultiLocation) = (Rococo::get(), Parachain(1001).into()); + pub const MaxInstructions: u32 = 100; } pub type TrustedTeleporters = ( xcm_builder::Case, @@ -666,7 +667,7 @@ impl xcm_executor::Config for XcmConfig { type IsTeleporter = TrustedTeleporters; type LocationInverter = LocationInverter; type Barrier = Barrier; - type Weigher = FixedWeightBounds; + type Weigher = FixedWeightBounds; type Trader = UsingComponents>; type ResponseHandler = (); } @@ -696,8 +697,10 @@ impl pallet_xcm::Config for Runtime { type XcmExecutor = XcmExecutor; type XcmTeleportFilter = Everything; type XcmReserveTransferFilter = Everything; - type Weigher = FixedWeightBounds; + type Weigher = FixedWeightBounds; type LocationInverter = LocationInverter; + type Origin = Origin; + type Call = Call; } impl parachains_session_info::Config for Runtime {} diff --git a/runtime/test-runtime/src/lib.rs b/runtime/test-runtime/src/lib.rs index fa70d3ee91bd..99e8e277e677 100644 --- a/runtime/test-runtime/src/lib.rs +++ b/runtime/test-runtime/src/lib.rs @@ -494,6 +494,7 @@ impl parachains_ump::Config for Runtime { parameter_types! { pub const BaseXcmWeight: frame_support::weights::Weight = 1_000; pub const AnyNetwork: xcm::latest::NetworkId = xcm::latest::NetworkId::Any; + pub const MaxInstructions: u32 = 100; } pub type LocalOriginToLocation = xcm_builder::SignedToAccountId32; @@ -505,12 +506,14 @@ impl pallet_xcm::Config for Runtime { type ExecuteXcmOrigin = xcm_builder::EnsureXcmOrigin; type LocationInverter = xcm_config::InvertNothing; type SendXcmOrigin = xcm_builder::EnsureXcmOrigin; - type Weigher = xcm_builder::FixedWeightBounds; + type Weigher = xcm_builder::FixedWeightBounds; type XcmRouter = xcm_config::DoNothingRouter; type XcmExecuteFilter = Everything; type XcmExecutor = xcm_executor::XcmExecutor; type XcmTeleportFilter = Everything; type XcmReserveTransferFilter = Everything; + type Origin = Origin; + type Call = Call; } impl parachains_hrmp::Config for Runtime { @@ -523,6 +526,91 @@ impl parachains_scheduler::Config for Runtime {} impl paras_sudo_wrapper::Config for Runtime {} +impl pallet_test_notifier::Config for Runtime { + type Event = Event; + type Origin = Origin; + type Call = Call; +} + +#[frame_support::pallet] +pub mod pallet_test_notifier { + use frame_support::pallet_prelude::*; + use frame_system::pallet_prelude::*; + use pallet_xcm::{ensure_response, QueryId}; + use sp_runtime::DispatchResult; + use xcm::latest::prelude::*; + + #[pallet::pallet] + #[pallet::generate_store(pub(super) trait Store)] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: frame_system::Config + pallet_xcm::Config { + type Event: IsType<::Event> + From>; + type Origin: IsType<::Origin> + + Into::Origin>>; + type Call: IsType<::Call> + From>; + } + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + QueryPrepared(QueryId), + NotifyQueryPrepared(QueryId), + ResponseReceived(MultiLocation, QueryId, Response), + } + + #[pallet::error] + pub enum Error { + UnexpectedId, + BadAccountFormat, + } + + #[pallet::call] + impl Pallet { + #[pallet::weight(1_000_000)] + pub fn prepare_new_query(origin: OriginFor) -> DispatchResult { + let who = ensure_signed(origin)?; + let id = who + .using_encoded(|mut d| <[u8; 32]>::decode(&mut d)) + .map_err(|_| Error::::BadAccountFormat)?; + let qid = pallet_xcm::Pallet::::new_query( + Junction::AccountId32 { network: Any, id }.into(), + 100u32.into(), + ); + Self::deposit_event(Event::::QueryPrepared(qid)); + Ok(()) + } + + #[pallet::weight(1_000_000)] + pub fn prepare_new_notify_query(origin: OriginFor) -> DispatchResult { + let who = ensure_signed(origin)?; + let id = who + .using_encoded(|mut d| <[u8; 32]>::decode(&mut d)) + .map_err(|_| Error::::BadAccountFormat)?; + let call = Call::::notification_received(0, Default::default()); + let qid = pallet_xcm::Pallet::::new_notify_query( + Junction::AccountId32 { network: Any, id }.into(), + ::Call::from(call), + 100u32.into(), + ); + Self::deposit_event(Event::::NotifyQueryPrepared(qid)); + Ok(()) + } + + #[pallet::weight(1_000_000)] + pub fn notification_received( + origin: OriginFor, + query_id: QueryId, + response: Response, + ) -> DispatchResult { + let responder = ensure_response(::Origin::from(origin))?; + Self::deposit_event(Event::::ResponseReceived(responder, query_id, response)); + Ok(()) + } + } +} + construct_runtime! { pub enum Runtime where Block = Block, @@ -572,6 +660,8 @@ construct_runtime! { ParasDisputes: parachains_disputes::{Pallet, Storage, Event}, Sudo: pallet_sudo::{Pallet, Call, Storage, Config, Event}, + + TestNotifier: pallet_test_notifier::{Pallet, Call, Event}, } } diff --git a/runtime/test-runtime/src/xcm_config.rs b/runtime/test-runtime/src/xcm_config.rs index 7748d55e1875..3af079e92f43 100644 --- a/runtime/test-runtime/src/xcm_config.rs +++ b/runtime/test-runtime/src/xcm_config.rs @@ -15,10 +15,7 @@ // along with Polkadot. If not, see . use frame_support::{parameter_types, traits::Everything, weights::Weight}; -use xcm::latest::{ - Error as XcmError, Junctions::Here, MultiAsset, MultiLocation, NetworkId, Parent, - Result as XcmResult, SendXcm, Xcm, -}; +use xcm::latest::prelude::*; use xcm_builder::{AllowUnpaidExecutionFrom, FixedWeightBounds, SignedToAccountId32}; use xcm_executor::{ traits::{InvertLocation, TransactAsset, WeightTrader}, @@ -27,6 +24,7 @@ use xcm_executor::{ parameter_types! { pub const OurNetwork: NetworkId = NetworkId::Polkadot; + pub const MaxInstructions: u32 = 100; } /// Type to convert an `Origin` type value into a `MultiLocation` value which represents an interior location @@ -38,7 +36,7 @@ pub type LocalOriginToLocation = ( pub struct DoNothingRouter; impl SendXcm for DoNothingRouter { - fn send_xcm(_dest: MultiLocation, _msg: Xcm<()>) -> XcmResult { + fn send_xcm(_dest: MultiLocation, _msg: Xcm<()>) -> SendResult { Ok(()) } } @@ -85,7 +83,7 @@ impl xcm_executor::Config for XcmConfig { type IsTeleporter = (); type LocationInverter = InvertNothing; type Barrier = Barrier; - type Weigher = FixedWeightBounds; + type Weigher = FixedWeightBounds; type Trader = DummyWeightTrader; - type ResponseHandler = (); + type ResponseHandler = super::Xcm; } diff --git a/runtime/westend/src/lib.rs b/runtime/westend/src/lib.rs index dc7604a5ea3a..ba7ae594aa6d 100644 --- a/runtime/westend/src/lib.rs +++ b/runtime/westend/src/lib.rs @@ -911,6 +911,7 @@ pub type XcmRouter = ( parameter_types! { pub const WestendForWestmint: (MultiAssetFilter, MultiLocation) = (Wild(AllOf { fun: WildFungible, id: Concrete(WndLocation::get()) }), Parachain(1000).into()); + pub const MaxInstructions: u32 = 100; } pub type TrustedTeleporters = (xcm_builder::Case,); @@ -934,7 +935,7 @@ impl xcm_executor::Config for XcmConfig { type IsTeleporter = TrustedTeleporters; type LocationInverter = LocationInverter; type Barrier = Barrier; - type Weigher = FixedWeightBounds; + type Weigher = FixedWeightBounds; type Trader = UsingComponents>; type ResponseHandler = (); } @@ -957,8 +958,10 @@ impl pallet_xcm::Config for Runtime { type XcmExecutor = XcmExecutor; type XcmTeleportFilter = Everything; type XcmReserveTransferFilter = Everything; - type Weigher = FixedWeightBounds; + type Weigher = FixedWeightBounds; type LocationInverter = LocationInverter; + type Origin = Origin; + type Call = Call; } construct_runtime! { diff --git a/scripts/gitlab/lingua.dic b/scripts/gitlab/lingua.dic index 6e0ffd5c6c39..e7b4348cbaec 100644 --- a/scripts/gitlab/lingua.dic +++ b/scripts/gitlab/lingua.dic @@ -108,6 +108,7 @@ inverter/MS io IP/S isn +isolatable isolate/BG iterable jaeger/MS @@ -189,6 +190,7 @@ prometheus/MS provisioner/MS proxy/DMSG proxy/G +proxying PRs PVF/S README/MS @@ -200,6 +202,7 @@ responder/SM retriability reverify roundtrip/MS +routable rpc RPC/MS runtime/MS @@ -231,6 +234,7 @@ taskmanager/MS TCP teleport/D teleport/RG +teleports teleportation/SM teleporter/SM teleporters @@ -251,6 +255,7 @@ unordered unreceived unreserve unreserving +unroutable unservable/B untrusted untyped diff --git a/xcm/pallet-xcm/src/lib.rs b/xcm/pallet-xcm/src/lib.rs index c6772cb6df37..acc5a6269f23 100644 --- a/xcm/pallet-xcm/src/lib.rs +++ b/xcm/pallet-xcm/src/lib.rs @@ -33,7 +33,10 @@ use sp_std::{ prelude::*, vec, }; -use xcm::{latest::prelude::*, VersionedMultiAssets, VersionedMultiLocation, VersionedXcm}; +use xcm::{ + latest::prelude::*, VersionedMultiAssets, VersionedMultiLocation, VersionedResponse, + VersionedXcm, +}; use xcm_executor::traits::ConvertOrigin; use frame_support::PalletId; @@ -42,10 +45,13 @@ pub use pallet::*; #[frame_support::pallet] pub mod pallet { use super::*; - use frame_support::pallet_prelude::*; - use frame_system::pallet_prelude::*; - use sp_runtime::traits::AccountIdConversion; - use xcm_executor::traits::{InvertLocation, WeightBounds}; + use frame_support::{ + dispatch::{Dispatchable, GetDispatchInfo, PostDispatchInfo}, + pallet_prelude::*, + }; + use frame_system::{pallet_prelude::*, Config as SysConfig}; + use sp_runtime::traits::{AccountIdConversion, BlockNumberProvider}; + use xcm_executor::traits::{InvertLocation, OnResponse, WeightBounds}; #[pallet::pallet] #[pallet::generate_store(pub(super) trait Store)] @@ -59,7 +65,7 @@ pub mod pallet { /// Required origin for sending XCM messages. If successful, the it resolves to `MultiLocation` /// which exists as an interior location within this chain's XCM context. - type SendXcmOrigin: EnsureOrigin; + type SendXcmOrigin: EnsureOrigin<::Origin, Success = MultiLocation>; /// The type used to actually dispatch an XCM to its destination. type XcmRouter: SendXcm; @@ -67,13 +73,13 @@ pub mod pallet { /// Required origin for executing XCM messages, including the teleport functionality. If successful, /// then it resolves to `MultiLocation` which exists as an interior location within this chain's XCM /// context. - type ExecuteXcmOrigin: EnsureOrigin; + type ExecuteXcmOrigin: EnsureOrigin<::Origin, Success = MultiLocation>; /// Our XCM filter which messages to be executed using `XcmExecutor` must pass. - type XcmExecuteFilter: Contains<(MultiLocation, Xcm)>; + type XcmExecuteFilter: Contains<(MultiLocation, Xcm<::Call>)>; /// Something to execute an XCM message. - type XcmExecutor: ExecuteXcm; + type XcmExecutor: ExecuteXcm<::Call>; /// Our XCM filter which messages to be teleported using the dedicated extrinsic must pass. type XcmTeleportFilter: Contains<(MultiLocation, Vec)>; @@ -82,10 +88,19 @@ pub mod pallet { type XcmReserveTransferFilter: Contains<(MultiLocation, Vec)>; /// Means of measuring the weight consumed by an XCM message locally. - type Weigher: WeightBounds; + type Weigher: WeightBounds<::Call>; /// Means of inverting a location. type LocationInverter: InvertLocation; + + /// The outer `Origin` type. + type Origin: From + From<::Origin>; + + /// The outer `Call` type. + type Call: Parameter + + GetDispatchInfo + + IsType<::Call> + + Dispatchable::Origin, PostInfo = PostDispatchInfo>; } /// The maximum number of distinct assets allowed to be transferred in a single helper extrinsic. @@ -94,13 +109,90 @@ pub mod pallet { #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { + /// Execution of an XCM message was attempted. + /// + /// \[ outcome \] Attempted(xcm::latest::Outcome), + /// A XCM message was sent. + /// + /// \[ origin, destination, message \] Sent(MultiLocation, MultiLocation, Xcm<()>), + /// Query response received which does not match a registered query. This may be because a + /// matching query was never registered, it may be because it is a duplicate response, or + /// because the query timed out. + /// + /// \[ origin location, id \] + UnexpectedResponse(MultiLocation, QueryId), + /// Query response has been received and is ready for taking with `take_response`. There is + /// no registered notification call. + /// + /// \[ id, response \] + ResponseReady(QueryId, Response), + /// Query response has been received and query is removed. The registered notification has + /// been dispatched and executed successfully. + /// + /// \[ id, pallet index, call index \] + Notified(QueryId, u8, u8), + /// Query response has been received and query is removed. The registered notification could + /// not be dispatched because the dispatch weight is greater than the maximum weight + /// originally budgeted by this runtime for the query result. + /// + /// \[ id, pallet index, call index, actual weight, max budgeted weight \] + NotifyOverweight(QueryId, u8, u8, Weight, Weight), + /// Query response has been received and query is removed. There was a general error with + /// dispatching the notification call. + /// + /// \[ id, pallet index, call index \] + NotifyDispatchError(QueryId, u8, u8), + /// Query response has been received and query is removed. The dispatch was unable to be + /// decoded into a `Call`; this might be due to dispatch function having a signature which + /// is not `(origin, QueryId, Response)`. + /// + /// \[ id, pallet index, call index \] + NotifyDecodeFailed(QueryId, u8, u8), + /// Expected query response has been received but the origin location of the response does + /// not match that expected. The query remains registered for a later, valid, response to + /// be received and acted upon. + /// + /// \[ origin location, id, expected location \] + InvalidResponder(MultiLocation, QueryId, MultiLocation), + /// 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. + /// + /// This is unexpected (since a location placed in storage in a previously executing + /// runtime should be readable prior to query timeout) and dangerous since the possibly + /// valid response will be dropped. Manual governance intervention is probably going to be + /// needed. + /// + /// \[ origin location, id \] + InvalidResponderVersion(MultiLocation, QueryId), + /// Received query response has been read and removed. + /// + /// \[ id \] + ResponseTaken(QueryId), + } + + #[pallet::origin] + #[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug)] + pub enum Origin { + /// It comes from somewhere in the XCM space wanting to transact. + Xcm(MultiLocation), + /// It comes as an expected response from an XCM location. + Response(MultiLocation), + } + impl From for Origin { + fn from(location: MultiLocation) -> Origin { + Origin::Xcm(location) + } } #[pallet::error] pub enum Error { + /// The desired destination was unreachable, generally because there is a no way of routing + /// to it. Unreachable, + /// There was some other issue (i.e. not to do with routing) in sending the message. Perhaps + /// a lack of space for buffering the message. SendFailure, /// The message execution fails the filter. Filtered, @@ -118,6 +210,32 @@ pub mod pallet { BadVersion, } + /// The status of a query. + #[derive(Clone, Eq, PartialEq, Encode, Decode, RuntimeDebug)] + pub enum QueryStatus { + /// The query was sent but no response has yet been received. + Pending { + responder: VersionedMultiLocation, + maybe_notify: Option<(u8, u8)>, + timeout: BlockNumber, + }, + /// A response has been received. + Ready { response: VersionedResponse, at: BlockNumber }, + } + + /// Value of a query, must be unique for each query. + pub type QueryId = u64; + + /// The latest available query index. + #[pallet::storage] + pub(super) type QueryCount = StorageValue<_, QueryId, ValueQuery>; + + /// The ongoing queries. + #[pallet::storage] + #[pallet::getter(fn query)] + pub(super) type Queries = + StorageMap<_, Blake2_128Concat, QueryId, QueryStatus, OptionQuery>; + #[pallet::hooks] impl Hooks> for Pallet {} @@ -136,7 +254,7 @@ pub mod pallet { let message: Xcm<()> = (*message).try_into().map_err(|()| Error::::BadVersion)?; Self::send_xcm(interior, dest.clone(), message.clone()).map_err(|e| match e { - XcmError::CannotReachDestination(..) => Error::::Unreachable, + SendError::CannotReachDestination(..) => Error::::Unreachable, _ => Error::::SendFailure, })?; Self::deposit_event(Event::Sent(origin_location, dest, message)); @@ -161,14 +279,11 @@ pub mod pallet { let maybe_dest: Result = (*dest.clone()).try_into(); match (maybe_assets, maybe_dest) { (Ok(assets), Ok(dest)) => { - let mut message = Xcm::WithdrawAsset { - assets, - effects: sp_std::vec![ InitiateTeleport { - assets: Wild(All), - dest, - effects: sp_std::vec![], - } ] - }; + use sp_std::vec; + let mut message = Xcm(vec![ + WithdrawAsset(assets), + InitiateTeleport { assets: Wild(All), dest, xcm: Xcm(vec![]) }, + ]); T::Weigher::weight(&mut message).map_or(Weight::max_value(), |w| 100_000_000 + w) }, _ => Weight::max_value(), @@ -180,7 +295,6 @@ pub mod pallet { beneficiary: Box, assets: Box, fee_asset_item: u32, - dest_weight: Weight, ) -> DispatchResult { let origin_location = T::ExecuteXcmOrigin::ensure_origin(origin)?; let dest = MultiLocation::try_from(*dest).map_err(|()| Error::::BadVersion)?; @@ -201,24 +315,17 @@ pub mod pallet { .map_err(|_| Error::::CannotReanchor)?; let max_assets = assets.len() as u32; let assets = assets.into(); - let mut message = Xcm::WithdrawAsset { - assets, - effects: vec![InitiateTeleport { + let mut message = Xcm(vec![ + WithdrawAsset(assets), + InitiateTeleport { assets: Wild(All), dest, - effects: vec![ - BuyExecution { - fees, - // Zero weight for additional XCM (since there are none to execute) - weight: 0, - debt: dest_weight, - halt_on_error: false, - instructions: vec![], - }, + xcm: Xcm(vec![ + BuyExecution { fees, weight_limit: Unlimited }, DepositAsset { assets: Wild(All), max_assets, beneficiary }, - ], - }], - }; + ]), + }, + ]); let weight = T::Weigher::weight(&mut message).map_err(|()| Error::::UnweighableMessage)?; let outcome = @@ -239,13 +346,15 @@ pub mod pallet { /// an `AccountId32` value. /// - `assets`: The assets to be withdrawn. This should include the assets used to pay the fee on the /// `dest` side. - /// - `dest_weight`: Equal to the total weight on `dest` of the XCM message - /// `ReserveAssetDeposited { assets, effects: [ BuyExecution{..}, DepositAsset{..} ] }`. + /// - `fee_asset_item`: The index into `assets` of the item which should be used to pay + /// fees. #[pallet::weight({ match ((*assets.clone()).try_into(), (*dest.clone()).try_into()) { (Ok(assets), Ok(dest)) => { - let effects = sp_std::vec![]; - let mut message = Xcm::TransferReserveAsset { assets, dest, effects }; + use sp_std::vec; + let mut message = Xcm(vec![ + TransferReserveAsset { assets, dest, xcm: Xcm(vec![]) } + ]); T::Weigher::weight(&mut message).map_or(Weight::max_value(), |w| 100_000_000 + w) }, _ => Weight::max_value(), @@ -257,7 +366,6 @@ pub mod pallet { beneficiary: Box, assets: Box, fee_asset_item: u32, - dest_weight: Weight, ) -> DispatchResult { let origin_location = T::ExecuteXcmOrigin::ensure_origin(origin)?; let dest = (*dest).try_into().map_err(|()| Error::::BadVersion)?; @@ -277,21 +385,14 @@ pub mod pallet { .map_err(|_| Error::::CannotReanchor)?; let max_assets = assets.len() as u32; let assets = assets.into(); - let mut message = Xcm::TransferReserveAsset { + let mut message = Xcm(vec![TransferReserveAsset { assets, dest, - effects: vec![ - BuyExecution { - fees, - // Zero weight for additional instructions/orders (since there are none to execute) - weight: 0, - debt: dest_weight, // covers this, `TransferReserveAsset` xcm, and `DepositAsset` order. - halt_on_error: false, - instructions: vec![], - }, + xcm: Xcm(vec![ + BuyExecution { fees, weight_limit: Unlimited }, DepositAsset { assets: Wild(All), max_assets, beneficiary }, - ], - }; + ]), + }]); let weight = T::Weigher::weight(&mut message).map_err(|()| Error::::UnweighableMessage)?; let outcome = @@ -314,7 +415,7 @@ pub mod pallet { #[pallet::weight(max_weight.saturating_add(100_000_000u64))] pub fn execute( origin: OriginFor, - message: Box>, + message: Box::Call>>, max_weight: Weight, ) -> DispatchResult { let origin_location = T::ExecuteXcmOrigin::ensure_origin(origin)?; @@ -334,12 +435,10 @@ pub mod pallet { pub fn send_xcm( interior: Junctions, dest: MultiLocation, - message: Xcm<()>, - ) -> Result<(), XcmError> { - let message = if let Junctions::Here = interior { - message - } else { - Xcm::<()>::RelayedFrom { who: interior, message: Box::new(message) } + mut message: Xcm<()>, + ) -> Result<(), SendError> { + if interior != Junctions::Here { + message.0.insert(0, DescendOrigin(interior)) }; log::trace!(target: "xcm::send_xcm", "dest: {:?}, message: {:?}", &dest, &message); T::XcmRouter::send_xcm(dest, message) @@ -349,25 +448,215 @@ pub mod pallet { const ID: PalletId = PalletId(*b"py/xcmch"); AccountIdConversion::::into_account(&ID) } - } - /// Origin for the parachains module. - #[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug)] - #[pallet::origin] - pub enum Origin { - /// It comes from somewhere in the XCM space. - Xcm(MultiLocation), + fn do_new_query( + responder: MultiLocation, + maybe_notify: Option<(u8, u8)>, + timeout: T::BlockNumber, + ) -> u64 { + QueryCount::::mutate(|q| { + let r = *q; + *q += 1; + Queries::::insert( + r, + QueryStatus::Pending { responder: responder.into(), maybe_notify, timeout }, + ); + r + }) + } + + /// Consume `message` and return another which is equivalent to it except that it reports + /// back the outcome. + /// + /// - `message`: The message whose outcome should be reported. + /// - `responder`: The origin from which a response should be expected. + /// - `timeout`: The block number after which it is permissible for `notify` not to be + /// called even if a response is received. + /// + /// To check the status of the query, use `fn query()` passing the resultant `QueryId` + /// value. + pub fn report_outcome( + message: &mut Xcm<()>, + responder: MultiLocation, + timeout: T::BlockNumber, + ) -> QueryId { + let dest = T::LocationInverter::invert_location(&responder); + let query_id = Self::new_query(responder, timeout); + let report_error = Xcm(vec![ReportError { dest, query_id, max_response_weight: 0 }]); + message.0.insert(0, SetAppendix(report_error)); + query_id + } + + /// Consume `message` and return another which is equivalent to it except that it reports + /// back the outcome and dispatches `notify` on this chain. + /// + /// - `message`: The message whose outcome should be reported. + /// - `responder`: The origin from which a response should be expected. + /// - `notify`: A dispatchable function which will be called once the outcome of `message` + /// is known. It may be a dispatchable in any pallet of the local chain, but other than + /// the usual origin, it must accept exactly two arguments: `query_id: QueryId` and + /// `outcome: Response`, and in that order. It should expect that the origin is + /// `Origin::Response` and will contain the responder's location. + /// - `timeout`: The block number after which it is permissible for `notify` not to be + /// called even if a response is received. + /// + /// NOTE: `notify` gets called as part of handling an incoming message, so it should be + /// lightweight. Its weight is estimated during this function and stored ready for + /// weighing `ReportOutcome` on the way back. If it turns out to be heavier once it returns + /// then reporting the outcome will fail. Futhermore if the estimate is too high, then it + /// may be put in the overweight queue and need to be manually executed. + pub fn report_outcome_notify( + message: &mut Xcm<()>, + responder: MultiLocation, + notify: impl Into<::Call>, + timeout: T::BlockNumber, + ) { + let dest = T::LocationInverter::invert_location(&responder); + let notify: ::Call = notify.into(); + let max_response_weight = notify.get_dispatch_info().weight; + let query_id = Self::new_notify_query(responder, notify, timeout); + let report_error = Xcm(vec![ReportError { dest, query_id, max_response_weight }]); + message.0.insert(0, SetAppendix(report_error)); + } + + /// Attempt to create a new query ID and register it as a query that is yet to respond. + pub fn new_query(responder: MultiLocation, timeout: T::BlockNumber) -> u64 { + Self::do_new_query(responder, None, timeout) + } + + /// Attempt to create a new query ID and register it as a query that is yet to respond, and + /// which will call a dispatchable when a response happens. + pub fn new_notify_query( + responder: MultiLocation, + notify: impl Into<::Call>, + timeout: T::BlockNumber, + ) -> u64 { + let notify = + notify.into().using_encoded(|mut bytes| Decode::decode(&mut bytes)).expect( + "decode input is output of Call encode; Call guaranteed to have two enums; qed", + ); + Self::do_new_query(responder, Some(notify), timeout) + } + + /// Attempt to remove and return the response of query with ID `query_id`. + /// + /// Returns `None` if the response is not (yet) available. + pub fn take_response(query_id: QueryId) -> Option<(Response, T::BlockNumber)> { + if let Some(QueryStatus::Ready { response, at }) = Queries::::get(query_id) { + let response = response.try_into().ok()?; + Queries::::remove(query_id); + Self::deposit_event(Event::ResponseTaken(query_id)); + Some((response, at)) + } else { + None + } + } } - impl From for Origin { - fn from(location: MultiLocation) -> Origin { - Origin::Xcm(location) + impl OnResponse for Pallet { + /// Returns `true` if we are expecting a response from `origin` for query `query_id`. + 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) + } + false + } + + /// Handler for receiving a `response` from `origin` relating to `query_id`. + fn on_response( + origin: &MultiLocation, + query_id: QueryId, + 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( + query_id, + pallet_index, + call_index, + ); + Self::deposit_event(e); + 0 + } + }, + 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::InvalidResponder( + origin.clone(), + query_id, + responder, + )); + } + } else { + Self::deposit_event(Event::InvalidResponderVersion(origin.clone(), query_id)); + } + } else { + Self::deposit_event(Event::UnexpectedResponse(origin.clone(), query_id)); + } + 0 } } } -/// Ensure that the origin `o` represents a sibling parachain. -/// Returns `Ok` with the parachain ID of the sibling or an `Err` otherwise. +/// Ensure that the origin `o` represents an XCM (`Transact`) origin. +/// +/// Returns `Ok` with the location of the XCM sender or an `Err` otherwise. pub fn ensure_xcm(o: OuterOrigin) -> Result where OuterOrigin: Into>, @@ -378,6 +667,19 @@ where } } +/// Ensure that the origin `o` represents an XCM response origin. +/// +/// Returns `Ok` with the location of the responder or an `Err` otherwise. +pub fn ensure_response(o: OuterOrigin) -> Result +where + OuterOrigin: Into>, +{ + match o.into() { + Ok(Origin::Response(location)) => Ok(location), + _ => Err(BadOrigin), + } +} + /// Filter for `MultiLocation` to find those which represent a strict majority approval of an identified /// plurality. /// @@ -403,12 +705,10 @@ where fn try_origin(outer: O) -> Result { outer.try_with_caller(|caller| { - caller.try_into().and_then(|Origin::Xcm(location)| { - if F::contains(&location) { - Ok(location) - } else { - Err(Origin::Xcm(location).into()) - } + caller.try_into().and_then(|o| match o { + Origin::Xcm(location) if F::contains(&location) => Ok(location), + Origin::Xcm(location) => Err(Origin::Xcm(location).into()), + o => Err(o.into()), }) }) } @@ -419,6 +719,31 @@ where } } +/// `EnsureOrigin` implementation succeeding with a `MultiLocation` value to recognize and filter +/// the `Origin::Response` item. +pub struct EnsureResponse(PhantomData); +impl, F: Contains> EnsureOrigin + for EnsureResponse +where + O::PalletsOrigin: From + TryInto, +{ + type Success = MultiLocation; + + fn try_origin(outer: O) -> Result { + outer.try_with_caller(|caller| { + caller.try_into().and_then(|o| match o { + Origin::Response(responder) => Ok(responder), + o => Err(o.into()), + }) + }) + } + + #[cfg(feature = "runtime-benchmarks")] + fn successful_origin() -> O { + O::from(Origin::Response(Here.into())) + } +} + /// A simple passthrough where we reuse the `MultiLocation`-typed XCM origin as the inner value of /// this crate's `Origin::Xcm` value. pub struct XcmPassthrough(PhantomData); diff --git a/xcm/pallet-xcm/src/mock.rs b/xcm/pallet-xcm/src/mock.rs index ac612a0641f1..66c427b9ff3c 100644 --- a/xcm/pallet-xcm/src/mock.rs +++ b/xcm/pallet-xcm/src/mock.rs @@ -20,13 +20,10 @@ use polkadot_runtime_parachains::origin; use sp_core::H256; use sp_runtime::{testing::Header, traits::IdentityLookup, AccountId32}; pub use sp_std::{cell::RefCell, fmt::Debug, marker::PhantomData}; -use xcm::{ - latest::prelude::*, - opaque::latest::{Error as XcmError, MultiAsset, Result as XcmResult, SendXcm, Xcm}, -}; +use xcm::latest::prelude::*; use xcm_builder::{ - AccountId32Aliases, AllowTopLevelPaidExecutionFrom, Case, ChildParachainAsNative, - ChildParachainConvertsVia, ChildSystemParachainAsSuperuser, + AccountId32Aliases, AllowKnownQueryResponses, AllowTopLevelPaidExecutionFrom, Case, + ChildParachainAsNative, ChildParachainConvertsVia, ChildSystemParachainAsSuperuser, CurrencyAdapter as XcmCurrencyAdapter, FixedRateOfFungible, FixedWeightBounds, IsConcrete, LocationInverter, SignedAccountId32AsNative, SignedToAccountId32, SovereignSignedViaLocation, TakeWeightCredit, @@ -40,6 +37,85 @@ pub type Balance = u128; type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; type Block = frame_system::mocking::MockBlock; +#[frame_support::pallet] +pub mod pallet_test_notifier { + use crate::{ensure_response, QueryId}; + use frame_support::pallet_prelude::*; + use frame_system::pallet_prelude::*; + use sp_runtime::DispatchResult; + use xcm::latest::prelude::*; + + #[pallet::pallet] + #[pallet::generate_store(pub(super) trait Store)] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: frame_system::Config + crate::Config { + type Event: IsType<::Event> + From>; + type Origin: IsType<::Origin> + + Into::Origin>>; + type Call: IsType<::Call> + From>; + } + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + QueryPrepared(QueryId), + NotifyQueryPrepared(QueryId), + ResponseReceived(MultiLocation, QueryId, Response), + } + + #[pallet::error] + pub enum Error { + UnexpectedId, + BadAccountFormat, + } + + #[pallet::call] + impl Pallet { + #[pallet::weight(1_000_000)] + pub fn prepare_new_query(origin: OriginFor) -> DispatchResult { + let who = ensure_signed(origin)?; + let id = who + .using_encoded(|mut d| <[u8; 32]>::decode(&mut d)) + .map_err(|_| Error::::BadAccountFormat)?; + let qid = crate::Pallet::::new_query( + Junction::AccountId32 { network: Any, id }.into(), + 100u32.into(), + ); + Self::deposit_event(Event::::QueryPrepared(qid)); + Ok(()) + } + + #[pallet::weight(1_000_000)] + pub fn prepare_new_notify_query(origin: OriginFor) -> DispatchResult { + let who = ensure_signed(origin)?; + let id = who + .using_encoded(|mut d| <[u8; 32]>::decode(&mut d)) + .map_err(|_| Error::::BadAccountFormat)?; + let call = Call::::notification_received(0, Default::default()); + let qid = crate::Pallet::::new_notify_query( + Junction::AccountId32 { network: Any, id }.into(), + ::Call::from(call), + 100u32.into(), + ); + Self::deposit_event(Event::::NotifyQueryPrepared(qid)); + Ok(()) + } + + #[pallet::weight(1_000_000)] + pub fn notification_received( + origin: OriginFor, + query_id: QueryId, + response: Response, + ) -> DispatchResult { + let responder = ensure_response(::Origin::from(origin))?; + Self::deposit_event(Event::::ResponseReceived(responder, query_id, response)); + Ok(()) + } + } +} + construct_runtime!( pub enum Test where Block = Block, @@ -49,20 +125,21 @@ construct_runtime!( System: frame_system::{Pallet, Call, Storage, Config, Event}, Balances: pallet_balances::{Pallet, Call, Storage, Config, Event}, ParasOrigin: origin::{Pallet, Origin}, - XcmPallet: pallet_xcm::{Pallet, Call, Storage, Event}, + XcmPallet: pallet_xcm::{Pallet, Call, Storage, Event, Origin}, + TestNotifier: pallet_test_notifier::{Pallet, Call, Event}, } ); thread_local! { - pub static SENT_XCM: RefCell> = RefCell::new(Vec::new()); + pub static SENT_XCM: RefCell)>> = RefCell::new(Vec::new()); } -pub fn sent_xcm() -> Vec<(MultiLocation, Xcm)> { +pub fn sent_xcm() -> Vec<(MultiLocation, Xcm<()>)> { SENT_XCM.with(|q| (*q.borrow()).clone()) } /// Sender that never returns error, always sends pub struct TestSendXcm; impl SendXcm for TestSendXcm { - fn send_xcm(dest: MultiLocation, msg: Xcm) -> XcmResult { + fn send_xcm(dest: MultiLocation, msg: Xcm<()>) -> SendResult { SENT_XCM.with(|q| q.borrow_mut().push((dest, msg))); Ok(()) } @@ -70,9 +147,9 @@ impl SendXcm for TestSendXcm { /// Sender that returns error if `X8` junction and stops routing pub struct TestSendXcmErrX8; impl SendXcm for TestSendXcmErrX8 { - fn send_xcm(dest: MultiLocation, msg: Xcm) -> XcmResult { + fn send_xcm(dest: MultiLocation, msg: Xcm<()>) -> SendResult { if dest.len() == 8 { - Err(XcmError::Undefined) + Err(SendError::Transport("Destination location full")) } else { SENT_XCM.with(|q| q.borrow_mut().push((dest, msg))); Ok(()) @@ -152,9 +229,14 @@ parameter_types! { pub const BaseXcmWeight: Weight = 1_000; pub CurrencyPerSecond: (AssetId, u128) = (Concrete(RelayLocation::get()), 1); pub TrustedAssets: (MultiAssetFilter, MultiLocation) = (All.into(), Here.into()); + pub const MaxInstructions: u32 = 100; } -pub type Barrier = (TakeWeightCredit, AllowTopLevelPaidExecutionFrom); +pub type Barrier = ( + TakeWeightCredit, + AllowTopLevelPaidExecutionFrom, + AllowKnownQueryResponses, +); pub struct XcmConfig; impl xcm_executor::Config for XcmConfig { @@ -166,9 +248,9 @@ impl xcm_executor::Config for XcmConfig { type IsTeleporter = Case; type LocationInverter = LocationInverter; type Barrier = Barrier; - type Weigher = FixedWeightBounds; + type Weigher = FixedWeightBounds; type Trader = FixedRateOfFungible; - type ResponseHandler = (); + type ResponseHandler = XcmPallet; } pub type LocalOriginToLocation = SignedToAccountId32; @@ -182,25 +264,31 @@ impl pallet_xcm::Config for Test { type XcmExecutor = XcmExecutor; type XcmTeleportFilter = Everything; type XcmReserveTransferFilter = Everything; - type Weigher = FixedWeightBounds; + type Weigher = FixedWeightBounds; type LocationInverter = LocationInverter; + type Origin = Origin; + type Call = Call; } impl origin::Config for Test {} +impl pallet_test_notifier::Config for Test { + type Event = Event; + type Origin = Origin; + type Call = Call; +} + pub(crate) fn last_event() -> Event { System::events().pop().expect("Event expected").event } -pub(crate) fn buy_execution(fees: impl Into, debt: Weight) -> Order { - use xcm::opaque::latest::prelude::*; - Order::BuyExecution { - fees: fees.into(), - weight: 0, - debt, - halt_on_error: false, - instructions: vec![], - } +pub(crate) fn last_events(n: usize) -> Vec { + System::events().into_iter().map(|e| e.event).rev().take(n).rev().collect() +} + +pub(crate) fn buy_execution(fees: impl Into) -> Instruction { + use xcm::latest::prelude::*; + BuyExecution { fees: fees.into(), weight_limit: Unlimited } } pub(crate) fn new_test_ext_with_balances( diff --git a/xcm/pallet-xcm/src/tests.rs b/xcm/pallet-xcm/src/tests.rs index a790cf7374ce..540ee153a64e 100644 --- a/xcm/pallet-xcm/src/tests.rs +++ b/xcm/pallet-xcm/src/tests.rs @@ -14,11 +14,12 @@ // You should have received a copy of the GNU General Public License // along with Polkadot. If not, see . -use crate::mock::*; +use crate::{mock::*, QueryStatus}; use frame_support::{assert_noop, assert_ok, traits::Currency}; use polkadot_parachain::primitives::{AccountIdConversion, Id as ParaId}; use std::convert::TryInto; -use xcm::{v1::prelude::*, VersionedXcm}; +use xcm::{latest::prelude::*, VersionedXcm}; +use xcm_executor::XcmExecutor; const ALICE: AccountId = AccountId::new([0u8; 32]); const BOB: AccountId = AccountId::new([1u8; 32]); @@ -26,6 +27,111 @@ const PARA_ID: u32 = 2000; const INITIAL_BALANCE: u128 = 100; const SEND_AMOUNT: u128 = 10; +#[test] +fn report_outcome_notify_works() { + let balances = + vec![(ALICE, INITIAL_BALANCE), (ParaId::from(PARA_ID).into_account(), INITIAL_BALANCE)]; + let sender = AccountId32 { network: AnyNetwork::get(), id: ALICE.into() }.into(); + let mut message = Xcm(vec![TransferAsset { + assets: (Here, SEND_AMOUNT).into(), + beneficiary: sender.clone(), + }]); + let call = pallet_test_notifier::Call::notification_received(0, Default::default()); + let notify = Call::TestNotifier(call); + new_test_ext_with_balances(balances).execute_with(|| { + XcmPallet::report_outcome_notify(&mut message, Parachain(PARA_ID).into(), notify, 100); + assert_eq!( + message, + Xcm(vec![ + SetAppendix(Xcm(vec![ReportError { + query_id: 0, + dest: Parent.into(), + max_response_weight: 1_000_000 + },])), + TransferAsset { assets: (Here, SEND_AMOUNT).into(), beneficiary: sender.clone() }, + ]) + ); + let status = QueryStatus::Pending { + responder: MultiLocation::from(Parachain(PARA_ID)).into(), + maybe_notify: Some((4, 2)), + timeout: 100, + }; + assert_eq!(crate::Queries::::iter().collect::>(), vec![(0, status)]); + + let r = XcmExecutor::::execute_xcm( + Parachain(PARA_ID).into(), + Xcm(vec![QueryResponse { + query_id: 0, + response: Response::ExecutionResult(Ok(())), + max_weight: 1_000_000, + }]), + 1_000_000_000, + ); + assert_eq!(r, Outcome::Complete(1_000)); + assert_eq!( + last_events(2), + vec![ + Event::TestNotifier(pallet_test_notifier::Event::ResponseReceived( + Parachain(PARA_ID).into(), + 0, + Response::ExecutionResult(Ok(())), + )), + Event::XcmPallet(crate::Event::Notified(0, 4, 2)), + ] + ); + assert_eq!(crate::Queries::::iter().collect::>(), vec![]); + }); +} + +#[test] +fn report_outcome_works() { + let balances = + vec![(ALICE, INITIAL_BALANCE), (ParaId::from(PARA_ID).into_account(), INITIAL_BALANCE)]; + let sender = AccountId32 { network: AnyNetwork::get(), id: ALICE.into() }.into(); + let mut message = Xcm(vec![TransferAsset { + assets: (Here, SEND_AMOUNT).into(), + beneficiary: sender.clone(), + }]); + new_test_ext_with_balances(balances).execute_with(|| { + XcmPallet::report_outcome(&mut message, Parachain(PARA_ID).into(), 100); + assert_eq!( + message, + Xcm(vec![ + SetAppendix(Xcm(vec![ReportError { + query_id: 0, + dest: Parent.into(), + max_response_weight: 0 + },])), + TransferAsset { assets: (Here, SEND_AMOUNT).into(), beneficiary: sender.clone() }, + ]) + ); + let status = QueryStatus::Pending { + responder: MultiLocation::from(Parachain(PARA_ID)).into(), + maybe_notify: None, + timeout: 100, + }; + assert_eq!(crate::Queries::::iter().collect::>(), vec![(0, status)]); + + let r = XcmExecutor::::execute_xcm( + Parachain(PARA_ID).into(), + Xcm(vec![QueryResponse { + query_id: 0, + response: Response::ExecutionResult(Ok(())), + max_weight: 0, + }]), + 1_000_000_000, + ); + assert_eq!(r, Outcome::Complete(1_000)); + assert_eq!( + last_event(), + Event::XcmPallet(crate::Event::ResponseReady(0, Response::ExecutionResult(Ok(())),)) + ); + + let response = Some((Response::ExecutionResult(Ok(())), 1)); + assert_eq!(XcmPallet::take_response(0), response); + }); +} + /// Test sending an `XCM` message (`XCM::ReserveAssetDeposit`) /// /// Asserts that the expected message is sent and the event is emitted @@ -34,30 +140,26 @@ fn send_works() { let balances = vec![(ALICE, INITIAL_BALANCE), (ParaId::from(PARA_ID).into_account(), INITIAL_BALANCE)]; new_test_ext_with_balances(balances).execute_with(|| { - let weight = 2 * BaseXcmWeight::get(); let sender: MultiLocation = AccountId32 { network: AnyNetwork::get(), id: ALICE.into() }.into(); - let message = Xcm::ReserveAssetDeposited { - assets: (Parent, SEND_AMOUNT).into(), - effects: vec![ - buy_execution((Parent, SEND_AMOUNT), weight), - DepositAsset { assets: All.into(), max_assets: 1, beneficiary: sender.clone() }, - ], - }; - assert_ok!(XcmPallet::send( - Origin::signed(ALICE), - Box::new(RelayLocation::get().into()), - Box::new(VersionedXcm::from(message.clone())) - )); + let message = Xcm(vec![ + ReserveAssetDeposited((Parent, SEND_AMOUNT).into()), + ClearOrigin, + buy_execution((Parent, SEND_AMOUNT)), + DepositAsset { assets: All.into(), max_assets: 1, beneficiary: sender.clone() }, + ]); + let versioned_dest = Box::new(RelayLocation::get().into()); + let versioned_message = Box::new(VersionedXcm::from(message.clone())); + assert_ok!(XcmPallet::send(Origin::signed(ALICE), versioned_dest, versioned_message)); assert_eq!( sent_xcm(), vec![( Here.into(), - RelayedFrom { - who: sender.clone().try_into().unwrap(), - message: Box::new(message.clone()), - } - )] + Xcm(Some(DescendOrigin(sender.clone().try_into().unwrap())) + .into_iter() + .chain(message.0.clone().into_iter()) + .collect()) + )], ); assert_eq!( last_event(), @@ -75,16 +177,13 @@ fn send_fails_when_xcm_router_blocks() { let balances = vec![(ALICE, INITIAL_BALANCE), (ParaId::from(PARA_ID).into_account(), INITIAL_BALANCE)]; new_test_ext_with_balances(balances).execute_with(|| { - let weight = 2 * BaseXcmWeight::get(); let sender: MultiLocation = Junction::AccountId32 { network: AnyNetwork::get(), id: ALICE.into() }.into(); - let message = Xcm::ReserveAssetDeposited { - assets: (Parent, SEND_AMOUNT).into(), - effects: vec![ - buy_execution((Parent, SEND_AMOUNT), weight), - DepositAsset { assets: All.into(), max_assets: 1, beneficiary: sender.clone() }, - ], - }; + let message = Xcm(vec![ + ReserveAssetDeposited((Parent, SEND_AMOUNT).into()), + buy_execution((Parent, SEND_AMOUNT)), + DepositAsset { assets: All.into(), max_assets: 1, beneficiary: sender.clone() }, + ]); assert_noop!( XcmPallet::send( Origin::signed(ALICE), @@ -113,7 +212,6 @@ fn teleport_assets_works() { Box::new(AccountId32 { network: Any, id: BOB.into() }.into().into()), Box::new((Here, SEND_AMOUNT).into()), 0, - weight, )); assert_eq!(Balances::total_balance(&ALICE), INITIAL_BALANCE - SEND_AMOUNT); assert_eq!( @@ -142,7 +240,6 @@ fn reserve_transfer_assets_works() { Box::new(dest.clone().into()), Box::new((Here, SEND_AMOUNT).into()), 0, - weight )); // Alice spent amount assert_eq!(Balances::free_balance(ALICE), INITIAL_BALANCE - SEND_AMOUNT); @@ -153,13 +250,12 @@ fn reserve_transfer_assets_works() { sent_xcm(), vec![( Parachain(PARA_ID).into(), - Xcm::ReserveAssetDeposited { - assets: (Parent, SEND_AMOUNT).into(), - effects: vec![ - buy_execution((Parent, SEND_AMOUNT), weight), - DepositAsset { assets: All.into(), max_assets: 1, beneficiary: dest }, - ] - } + Xcm(vec![ + ReserveAssetDeposited((Parent, SEND_AMOUNT).into()), + ClearOrigin, + buy_execution((Parent, SEND_AMOUNT)), + DepositAsset { assets: All.into(), max_assets: 1, beneficiary: dest }, + ]), )] ); assert_eq!( @@ -184,13 +280,11 @@ fn execute_withdraw_to_deposit_works() { assert_eq!(Balances::total_balance(&ALICE), INITIAL_BALANCE); assert_ok!(XcmPallet::execute( Origin::signed(ALICE), - Box::new(VersionedXcm::from(Xcm::WithdrawAsset { - assets: (Here, SEND_AMOUNT).into(), - effects: vec![ - buy_execution((Here, SEND_AMOUNT), weight), - DepositAsset { assets: All.into(), max_assets: 1, beneficiary: dest } - ], - })), + Box::new(VersionedXcm::from(Xcm(vec![ + WithdrawAsset((Here, SEND_AMOUNT).into()), + buy_execution((Here, SEND_AMOUNT)), + DepositAsset { assets: All.into(), max_assets: 1, beneficiary: dest }, + ]))), weight )); assert_eq!(Balances::total_balance(&ALICE), INITIAL_BALANCE - SEND_AMOUNT); diff --git a/xcm/src/lib.rs b/xcm/src/lib.rs index 7e460e792f31..af53b7bddcf2 100644 --- a/xcm/src/lib.rs +++ b/xcm/src/lib.rs @@ -33,9 +33,10 @@ use parity_scale_codec::{Decode, Encode, Error as CodecError, Input}; pub mod v0; pub mod v1; +pub mod v2; pub mod latest { - pub use super::v1::*; + pub use super::v2::*; } mod double_encoded; @@ -97,6 +98,71 @@ impl TryFrom for v1::MultiLocation { } } +/// A single `Response` value, together with its version code. +#[derive(Derivative, Encode, Decode)] +#[derivative(Clone(bound = ""), Eq(bound = ""), PartialEq(bound = ""), Debug(bound = ""))] +#[codec(encode_bound())] +#[codec(decode_bound())] +pub enum VersionedResponse { + V0(v0::Response), + V1(v1::Response), + V2(v2::Response), +} + +impl From for VersionedResponse { + fn from(x: v0::Response) -> Self { + VersionedResponse::V0(x) + } +} + +impl From for VersionedResponse { + fn from(x: v1::Response) -> Self { + VersionedResponse::V1(x) + } +} + +impl> From for VersionedResponse { + fn from(x: T) -> Self { + VersionedResponse::V2(x.into()) + } +} + +impl TryFrom for v0::Response { + type Error = (); + fn try_from(x: VersionedResponse) -> Result { + use VersionedResponse::*; + match x { + V0(x) => Ok(x), + V1(x) => x.try_into(), + V2(x) => VersionedResponse::V1(x.try_into()?).try_into(), + } + } +} + +impl TryFrom for v1::Response { + type Error = (); + fn try_from(x: VersionedResponse) -> Result { + use VersionedResponse::*; + match x { + V0(x) => x.try_into(), + V1(x) => Ok(x), + V2(x) => x.try_into(), + } + } +} + +impl TryFrom for v2::Response { + type Error = (); + fn try_from(x: VersionedResponse) -> Result { + use VersionedResponse::*; + match x { + V0(x) => VersionedResponse::V1(x.try_into()?).try_into(), + V1(x) => x.try_into(), + V2(x) => Ok(x), + } + } +} + /// A single `MultiAsset` value, together with its version code. #[derive(Derivative, Encode, Decode)] #[derivative(Clone(bound = ""), Eq(bound = ""), PartialEq(bound = ""), Debug(bound = ""))] @@ -142,8 +208,6 @@ impl TryFrom for v1::MultiAsset { } /// A single `MultiAssets` value, together with its version code. -/// -/// NOTE: For XCM v0, this was `Vec`. #[derive(Derivative, Encode, Decode)] #[derivative(Clone(bound = ""), Eq(bound = ""), PartialEq(bound = ""), Debug(bound = ""))] #[codec(encode_bound())] @@ -195,6 +259,7 @@ impl TryFrom for v1::MultiAssets { pub enum VersionedXcm { V0(v0::Xcm), V1(v1::Xcm), + V2(v2::Xcm), } impl From> for VersionedXcm { @@ -209,12 +274,20 @@ impl From> for VersionedXcm { } } +impl From> for VersionedXcm { + fn from(x: v2::Xcm) -> Self { + VersionedXcm::V2(x) + } +} + impl TryFrom> for v0::Xcm { type Error = (); fn try_from(x: VersionedXcm) -> Result { + use VersionedXcm::*; match x { - VersionedXcm::V0(x) => Ok(x), - VersionedXcm::V1(x) => x.try_into(), + V0(x) => Ok(x), + V1(x) => x.try_into(), + V2(x) => V1(x.try_into()?).try_into(), } } } @@ -222,9 +295,23 @@ impl TryFrom> for v0::Xcm { impl TryFrom> for v1::Xcm { type Error = (); fn try_from(x: VersionedXcm) -> Result { + use VersionedXcm::*; + match x { + V0(x) => x.try_into(), + V1(x) => Ok(x), + V2(x) => x.try_into(), + } + } +} + +impl TryFrom> for v2::Xcm { + type Error = (); + fn try_from(x: VersionedXcm) -> Result { + use VersionedXcm::*; match x { - VersionedXcm::V0(x) => x.try_into(), - VersionedXcm::V1(x) => Ok(x), + V0(x) => V1(x.try_into()?).try_into(), + V1(x) => x.try_into(), + V2(x) => Ok(x), } } } @@ -269,6 +356,17 @@ impl WrapVersion for AlwaysV1 { } } +/// `WrapVersion` implementation which attempts to always convert the XCM to version 1 before wrapping it. +pub struct AlwaysV2; +impl WrapVersion for AlwaysV2 { + fn wrap_version( + _: &latest::MultiLocation, + xcm: impl Into>, + ) -> Result, ()> { + Ok(VersionedXcm::::V2(xcm.into().try_into()?)) + } +} + /// `WrapVersion` implementation which attempts to always convert the XCM to the latest version before wrapping it. pub type AlwaysLatest = AlwaysV1; @@ -288,9 +386,15 @@ pub mod opaque { // Then override with the opaque types in v1 pub use crate::v1::opaque::{Order, Xcm}; } + pub mod v2 { + // Everything from v1 + pub use crate::v2::*; + // Then override with the opaque types in v2 + pub use crate::v2::opaque::{Instruction, Xcm}; + } pub mod latest { - pub use super::v1::*; + pub use super::v2::*; } /// The basic `VersionedXcm` type which just uses the `Vec` as an encoded call. diff --git a/xcm/src/v0/mod.rs b/xcm/src/v0/mod.rs index d58147c172db..dd043554ef8b 100644 --- a/xcm/src/v0/mod.rs +++ b/xcm/src/v0/mod.rs @@ -295,7 +295,7 @@ impl Xcm { }, TeleportAsset { assets, effects } => TeleportAsset { assets, effects: effects.into_iter().map(Order::into).collect() }, - QueryResponse { query_id: u64, response } => QueryResponse { query_id: u64, response }, + QueryResponse { query_id, response } => QueryResponse { query_id, response }, TransferAsset { assets, dest } => TransferAsset { assets, dest }, TransferReserveAsset { assets, dest, effects } => TransferReserveAsset { assets, dest, effects }, @@ -356,8 +356,8 @@ impl TryFrom> for Xcm { .map(Order::try_from) .collect::>()?, }, - Xcm1::QueryResponse { query_id: u64, response } => - QueryResponse { query_id: u64, response: response.try_into()? }, + Xcm1::QueryResponse { query_id, response } => + QueryResponse { query_id, response: response.try_into()? }, Xcm1::TransferAsset { assets, beneficiary } => TransferAsset { assets: assets.try_into()?, dest: beneficiary.try_into()? }, Xcm1::TransferReserveAsset { assets, dest, effects } => TransferReserveAsset { diff --git a/xcm/src/v1/mod.rs b/xcm/src/v1/mod.rs index f06472498e2d..dc0f6a251a98 100644 --- a/xcm/src/v1/mod.rs +++ b/xcm/src/v1/mod.rs @@ -16,7 +16,10 @@ //! Version 1 of the Cross-Consensus Message format data structures. -use super::v0::{Response as Response0, Xcm as Xcm0}; +use super::{ + v0::{Response as OldResponse, Xcm as OldXcm}, + v2::{Instruction, Response as NewResponse, Xcm as NewXcm}, +}; use crate::DoubleEncoded; use alloc::vec::Vec; use core::{ @@ -28,7 +31,7 @@ use derivative::Derivative; use parity_scale_codec::{self, Decode, Encode}; mod junction; -pub mod multiasset; +mod multiasset; mod multilocation; mod order; mod traits; // the new multiasset. @@ -38,7 +41,9 @@ pub use multiasset::{ AssetId, AssetInstance, Fungibility, MultiAsset, MultiAssetFilter, MultiAssets, WildFungibility, WildMultiAsset, }; -pub use multilocation::{Ancestor, AncestorThen, Junctions, MultiLocation, Parent, ParentThen}; +pub use multilocation::{ + Ancestor, AncestorThen, InteriorMultiLocation, Junctions, MultiLocation, Parent, ParentThen, +}; pub use order::Order; pub use traits::{Error, ExecuteXcm, Outcome, Result, SendXcm}; @@ -48,30 +53,23 @@ pub use super::v0::{BodyId, BodyPart, NetworkId, OriginKind}; /// A prelude for importing all types typically used when interacting with XCM messages. pub mod prelude { pub use super::{ - super::v0::{ - BodyId, BodyPart, - NetworkId::{self, *}, - }, junction::Junction::{self, *}, - multiasset::{ - AssetId::{self, *}, - AssetInstance::{self, *}, - Fungibility::{self, *}, - MultiAsset, - MultiAssetFilter::{self, *}, - MultiAssets, - WildFungibility::{self, Fungible as WildFungible, NonFungible as WildNonFungible}, - WildMultiAsset::{self, *}, - }, - multilocation::{ - Ancestor, AncestorThen, - Junctions::{self, *}, - MultiLocation, Parent, ParentThen, - }, opaque, order::Order::{self, *}, - traits::{Error as XcmError, ExecuteXcm, Outcome, Result as XcmResult, SendXcm}, - OriginKind, Response, + Ancestor, AncestorThen, + AssetId::{self, *}, + AssetInstance::{self, *}, + BodyId, BodyPart, Error as XcmError, ExecuteXcm, + Fungibility::{self, *}, + InteriorMultiLocation, + Junctions::{self, *}, + MultiAsset, + MultiAssetFilter::{self, *}, + MultiAssets, MultiLocation, + NetworkId::{self, *}, + OriginKind, Outcome, Parent, ParentThen, Response, Result as XcmResult, SendXcm, + WildFungibility::{self, Fungible as WildFungible, NonFungible as WildNonFungible}, + WildMultiAsset::{self, *}, Xcm::{self, *}, }; } @@ -271,7 +269,7 @@ pub enum Xcm { /// /// Errors: #[codec(index = 10)] - RelayedFrom { who: Junctions, message: alloc::boxed::Box> }, + RelayedFrom { who: InteriorMultiLocation, message: alloc::boxed::Box> }, } impl Xcm { @@ -291,7 +289,7 @@ impl Xcm { assets, effects: effects.into_iter().map(Order::into).collect(), }, - QueryResponse { query_id: u64, response } => QueryResponse { query_id: u64, response }, + QueryResponse { query_id, response } => QueryResponse { query_id, response }, TransferAsset { assets, beneficiary } => TransferAsset { assets, beneficiary }, TransferReserveAsset { assets, dest, effects } => TransferReserveAsset { assets, dest, effects }, @@ -317,46 +315,46 @@ pub mod opaque { } // Convert from a v0 response to a v1 response -impl TryFrom for Response { +impl TryFrom for Response { type Error = (); - fn try_from(old_response: Response0) -> result::Result { + fn try_from(old_response: OldResponse) -> result::Result { match old_response { - Response0::Assets(assets) => Ok(Self::Assets(assets.try_into()?)), + OldResponse::Assets(assets) => Ok(Self::Assets(assets.try_into()?)), } } } -impl TryFrom> for Xcm { +impl TryFrom> for Xcm { type Error = (); - fn try_from(old: Xcm0) -> result::Result, ()> { + fn try_from(old: OldXcm) -> result::Result, ()> { use Xcm::*; Ok(match old { - Xcm0::WithdrawAsset { assets, effects } => WithdrawAsset { + OldXcm::WithdrawAsset { assets, effects } => WithdrawAsset { assets: assets.try_into()?, effects: effects .into_iter() .map(Order::try_from) .collect::>()?, }, - Xcm0::ReserveAssetDeposit { assets, effects } => ReserveAssetDeposited { + OldXcm::ReserveAssetDeposit { assets, effects } => ReserveAssetDeposited { assets: assets.try_into()?, effects: effects .into_iter() .map(Order::try_from) .collect::>()?, }, - Xcm0::TeleportAsset { assets, effects } => ReceiveTeleportedAsset { + OldXcm::TeleportAsset { assets, effects } => ReceiveTeleportedAsset { assets: assets.try_into()?, effects: effects .into_iter() .map(Order::try_from) .collect::>()?, }, - Xcm0::QueryResponse { query_id: u64, response } => - QueryResponse { query_id: u64, response: response.try_into()? }, - Xcm0::TransferAsset { assets, dest } => + OldXcm::QueryResponse { query_id, response } => + QueryResponse { query_id, response: response.try_into()? }, + OldXcm::TransferAsset { assets, dest } => TransferAsset { assets: assets.try_into()?, beneficiary: dest.try_into()? }, - Xcm0::TransferReserveAsset { assets, dest, effects } => TransferReserveAsset { + OldXcm::TransferReserveAsset { assets, dest, effects } => TransferReserveAsset { assets: assets.try_into()?, dest: dest.try_into()?, effects: effects @@ -364,17 +362,83 @@ impl TryFrom> for Xcm { .map(Order::try_from) .collect::>()?, }, - Xcm0::HrmpNewChannelOpenRequest { sender, max_message_size, max_capacity } => + OldXcm::HrmpNewChannelOpenRequest { sender, max_message_size, max_capacity } => HrmpNewChannelOpenRequest { sender, max_message_size, max_capacity }, - Xcm0::HrmpChannelAccepted { recipient } => HrmpChannelAccepted { recipient }, - Xcm0::HrmpChannelClosing { initiator, sender, recipient } => + OldXcm::HrmpChannelAccepted { recipient } => HrmpChannelAccepted { recipient }, + OldXcm::HrmpChannelClosing { initiator, sender, recipient } => HrmpChannelClosing { initiator, sender, recipient }, - Xcm0::Transact { origin_type, require_weight_at_most, call } => + OldXcm::Transact { origin_type, require_weight_at_most, call } => Transact { origin_type, require_weight_at_most, call: call.into() }, - Xcm0::RelayedFrom { who, message } => RelayedFrom { + OldXcm::RelayedFrom { who, message } => RelayedFrom { who: MultiLocation::try_from(who)?.try_into()?, message: alloc::boxed::Box::new((*message).try_into()?), }, }) } } + +impl TryFrom> for Xcm { + type Error = (); + fn try_from(old: NewXcm) -> result::Result, ()> { + use Xcm::*; + let mut iter = old.0.into_iter(); + let instruction = iter.next().ok_or(())?; + Ok(match instruction { + Instruction::WithdrawAsset(assets) => { + let effects = iter.map(Order::try_from).collect::>()?; + WithdrawAsset { assets, effects } + }, + Instruction::ReserveAssetDeposited(assets) => { + if !matches!(iter.next(), Some(Instruction::ClearOrigin)) { + return Err(()) + } + let effects = iter.map(Order::try_from).collect::>()?; + ReserveAssetDeposited { assets, effects } + }, + Instruction::ReceiveTeleportedAsset(assets) => { + if !matches!(iter.next(), Some(Instruction::ClearOrigin)) { + return Err(()) + } + let effects = iter.map(Order::try_from).collect::>()?; + ReceiveTeleportedAsset { assets, effects } + }, + Instruction::QueryResponse { query_id, response, max_weight } => { + // Cannot handle special response weights. + if max_weight > 0 { + return Err(()) + } + QueryResponse { query_id, response: response.try_into()? } + }, + Instruction::TransferAsset { assets, beneficiary } => + TransferAsset { assets, beneficiary }, + Instruction::TransferReserveAsset { assets, dest, xcm } => TransferReserveAsset { + assets, + dest, + effects: xcm + .0 + .into_iter() + .map(Order::try_from) + .collect::>()?, + }, + Instruction::HrmpNewChannelOpenRequest { sender, max_message_size, max_capacity } => + HrmpNewChannelOpenRequest { sender, max_message_size, max_capacity }, + Instruction::HrmpChannelAccepted { recipient } => HrmpChannelAccepted { recipient }, + Instruction::HrmpChannelClosing { initiator, sender, recipient } => + HrmpChannelClosing { initiator, sender, recipient }, + Instruction::Transact { origin_type, require_weight_at_most, call } => + Transact { origin_type, require_weight_at_most, call }, + _ => return Err(()), + }) + } +} + +// Convert from a v1 response to a v2 response +impl TryFrom for Response { + type Error = (); + fn try_from(response: NewResponse) -> result::Result { + match response { + NewResponse::Assets(assets) => Ok(Self::Assets(assets)), + _ => Err(()), + } + } +} diff --git a/xcm/src/v1/multilocation.rs b/xcm/src/v1/multilocation.rs index 87c963187672..f2c255bd1125 100644 --- a/xcm/src/v1/multilocation.rs +++ b/xcm/src/v1/multilocation.rs @@ -22,26 +22,30 @@ use parity_scale_codec::{Decode, Encode}; /// A relative path between state-bearing consensus systems. /// -/// A location in a consensus system is defined as an *isolatable state machine* held within global consensus. The -/// location in question need not have a sophisticated consensus algorithm of its own; a single account within -/// Ethereum, for example, could be considered a location. +/// A location in a consensus system is defined as an *isolatable state machine* held within global +/// consensus. The location in question need not have a sophisticated consensus algorithm of its +/// own; a single account within Ethereum, for example, could be considered a location. /// /// A very-much non-exhaustive list of types of location include: /// - A (normal, layer-1) block chain, e.g. the Bitcoin mainnet or a parachain. /// - A layer-0 super-chain, e.g. the Polkadot Relay chain. /// - A layer-2 smart contract, e.g. an ERC-20 on Ethereum. -/// - A logical functional component of a chain, e.g. a single instance of a pallet on a Frame-based Substrate chain. +/// - A logical functional component of a chain, e.g. a single instance of a pallet on a Frame-based +/// Substrate chain. /// - An account. /// -/// A `MultiLocation` is a *relative identifier*, meaning that it can only be used to define the relative path -/// between two locations, and cannot generally be used to refer to a location universally. It is comprised of a -/// number of *junctions*, each morphing the previous location, either diving down into one of its internal locations, -/// called a *sub-consensus*, or going up into its parent location. +/// A `MultiLocation` is a *relative identifier*, meaning that it can only be used to define the +/// relative path between two locations, and cannot generally be used to refer to a location +/// universally. It is comprised of an integer number of parents specifying the number of times to +/// "escape" upwards into the containing consensus system and then a number of *junctions*, each +/// diving down and specifying some interior portion of state (which may be considered a +/// "sub-consensus" system). /// -/// The `parents` field of this struct indicates the number of parent junctions that exist at the -/// beginning of this `MultiLocation`. A corollary of such a property is that no parent junctions -/// can be added in the middle or at the end of a `MultiLocation`, thus ensuring well-formedness -/// of each and every `MultiLocation` that can be constructed. +/// This specific `MultiLocation` implementation uses a `Junctions` datatype which is a Rust `enum` +/// in order to make pattern matching easier. There are occasions where it is important to ensure +/// that a value is strictly an interior location, in those cases, `Junctions` may be used. +/// +/// The `MultiLocation` value of `Null` simply refers to the interpreting consensus system. #[derive(Clone, Decode, Encode, Eq, PartialEq, Ord, PartialOrd, Debug)] pub struct MultiLocation { /// The number of parent junctions at the beginning of this `MultiLocation`. @@ -50,6 +54,11 @@ 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() @@ -724,7 +733,7 @@ impl Junctions { /// /// # Example /// ```rust - /// # use xcm::latest::{Junctions::*, Junction::*}; + /// # use xcm::v1::{Junctions::*, Junction::*}; /// # fn main() { /// let mut m = X3(Parachain(2), PalletInstance(3), OnlyChild); /// assert_eq!(m.match_and_split(&X2(Parachain(2), PalletInstance(3))), Some(&OnlyChild)); diff --git a/xcm/src/v1/order.rs b/xcm/src/v1/order.rs index 8911c7f5ac13..9b8a7591f183 100644 --- a/xcm/src/v1/order.rs +++ b/xcm/src/v1/order.rs @@ -16,10 +16,9 @@ //! Version 1 of the Cross-Consensus Message format data structures. -use super::{ - super::v0::Order as Order0, MultiAsset, MultiAssetFilter, MultiAssets, MultiLocation, Xcm, -}; -use alloc::vec::Vec; +use super::{MultiAsset, MultiAssetFilter, MultiAssets, MultiLocation, Xcm}; +use crate::{v0::Order as OldOrder, v2::Instruction}; +use alloc::{vec, vec::Vec}; use core::{ convert::{TryFrom, TryInto}, result, @@ -184,18 +183,18 @@ impl Order { } } -impl TryFrom> for Order { +impl TryFrom> for Order { type Error = (); - fn try_from(old: Order0) -> result::Result, ()> { + fn try_from(old: OldOrder) -> result::Result, ()> { use Order::*; Ok(match old { - Order0::Null => Noop, - Order0::DepositAsset { assets, dest } => DepositAsset { + OldOrder::Null => Noop, + OldOrder::DepositAsset { assets, dest } => DepositAsset { assets: assets.try_into()?, max_assets: 1, beneficiary: dest.try_into()?, }, - Order0::DepositReserveAsset { assets, dest, effects } => DepositReserveAsset { + OldOrder::DepositReserveAsset { assets, dest, effects } => DepositReserveAsset { assets: assets.try_into()?, max_assets: 1, dest: dest.try_into()?, @@ -204,9 +203,9 @@ impl TryFrom> for Order { .map(Order::<()>::try_from) .collect::>()?, }, - Order0::ExchangeAsset { give, receive } => + OldOrder::ExchangeAsset { give, receive } => ExchangeAsset { give: give.try_into()?, receive: receive.try_into()? }, - Order0::InitiateReserveWithdraw { assets, reserve, effects } => + OldOrder::InitiateReserveWithdraw { assets, reserve, effects } => InitiateReserveWithdraw { assets: assets.try_into()?, reserve: reserve.try_into()?, @@ -215,7 +214,7 @@ impl TryFrom> for Order { .map(Order::<()>::try_from) .collect::>()?, }, - Order0::InitiateTeleport { assets, dest, effects } => InitiateTeleport { + OldOrder::InitiateTeleport { assets, dest, effects } => InitiateTeleport { assets: assets.try_into()?, dest: dest.try_into()?, effects: effects @@ -223,9 +222,9 @@ impl TryFrom> for Order { .map(Order::<()>::try_from) .collect::>()?, }, - Order0::QueryHolding { query_id, dest, assets } => + OldOrder::QueryHolding { query_id, dest, assets } => QueryHolding { query_id, dest: dest.try_into()?, assets: assets.try_into()? }, - Order0::BuyExecution { fees, weight, debt, halt_on_error, xcm } => { + OldOrder::BuyExecution { fees, weight, debt, halt_on_error, xcm } => { let instructions = xcm.into_iter().map(Xcm::::try_from).collect::>()?; BuyExecution { fees: fees.try_into()?, weight, debt, halt_on_error, instructions } @@ -233,3 +232,60 @@ impl TryFrom> for Order { }) } } + +impl TryFrom> for Order { + type Error = (); + fn try_from(old: Instruction) -> result::Result, ()> { + use Order::*; + Ok(match old { + Instruction::DepositAsset { assets, max_assets, beneficiary } => + DepositAsset { assets, max_assets, beneficiary }, + Instruction::DepositReserveAsset { assets, max_assets, dest, xcm } => + DepositReserveAsset { + assets, + max_assets, + dest, + effects: xcm + .0 + .into_iter() + .map(Order::<()>::try_from) + .collect::>()?, + }, + Instruction::ExchangeAsset { give, receive } => ExchangeAsset { give, receive }, + Instruction::InitiateReserveWithdraw { assets, reserve, xcm } => + InitiateReserveWithdraw { + assets, + reserve, + effects: xcm + .0 + .into_iter() + .map(Order::<()>::try_from) + .collect::>()?, + }, + Instruction::InitiateTeleport { assets, dest, xcm } => InitiateTeleport { + assets, + dest, + effects: xcm + .0 + .into_iter() + .map(Order::<()>::try_from) + .collect::>()?, + }, + Instruction::QueryHolding { query_id, dest, assets, max_response_weight } => { + // Cannot handle special response weights. + if max_response_weight > 0 { + return Err(()) + } + QueryHolding { query_id, dest, assets } + }, + Instruction::BuyExecution { fees, weight_limit } => { + let instructions = vec![]; + let halt_on_error = true; + let weight = 0; + let debt = Option::::from(weight_limit).ok_or(())?; + BuyExecution { fees, weight, debt, halt_on_error, instructions } + }, + _ => return Err(()), + }) + } +} diff --git a/xcm/src/v2/mod.rs b/xcm/src/v2/mod.rs new file mode 100644 index 000000000000..42d0283652d8 --- /dev/null +++ b/xcm/src/v2/mod.rs @@ -0,0 +1,761 @@ +// Copyright 2020 Parity Technologies (UK) Ltd. +// This file is part of Cumulus. + +// Substrate 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. + +// Substrate 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 Cumulus. If not, see . + +//! Version 1 of the Cross-Consensus Message format data structures. + +use super::v1::{Order as OldOrder, Response as OldResponse, Xcm as OldXcm}; +use crate::DoubleEncoded; +use alloc::{vec, vec::Vec}; +use core::{ + convert::{TryFrom, TryInto}, + fmt::Debug, + result, +}; +use derivative::Derivative; +use parity_scale_codec::{self, Decode, Encode}; + +mod traits; + +pub use traits::{Error, ExecuteXcm, Outcome, Result, SendError, SendResult, SendXcm}; +// These parts of XCM v1 have been unchanged in XCM v2, and are re-imported here. +pub use super::v1::{ + Ancestor, AncestorThen, AssetId, AssetInstance, BodyId, BodyPart, Fungibility, + InteriorMultiLocation, Junction, Junctions, MultiAsset, MultiAssetFilter, MultiAssets, + MultiLocation, NetworkId, OriginKind, Parent, ParentThen, WildFungibility, WildMultiAsset, +}; + +#[derive(Derivative, Default, Encode, Decode)] +#[derivative(Clone(bound = ""), Eq(bound = ""), PartialEq(bound = ""), Debug(bound = ""))] +#[codec(encode_bound())] +#[codec(decode_bound())] +pub struct Xcm(pub Vec>); + +impl Xcm { + /// Create an empty instance. + pub fn new() -> Self { + Self(vec![]) + } + + /// Return `true` if no instructions are held in `self`. + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Return the number of instructions held in `self`. + pub fn len(&self) -> usize { + self.0.len() + } + + /// Consume and either return `self` if it contains some instructions, or if it's empty, then + /// instead return the result of `f`. + pub fn or_else(self, f: impl FnOnce() -> Self) -> Self { + if self.0.is_empty() { + f() + } else { + self + } + } + + /// Return the first instruction, if any. + pub fn first(&self) -> Option<&Instruction> { + self.0.first() + } + + /// Return the last instruction, if any. + pub fn last(&self) -> Option<&Instruction> { + self.0.last() + } + + /// Return the only instruction, contained in `Self`, iff only one exists (`None` otherwise). + pub fn only(&self) -> Option<&Instruction> { + if self.0.len() == 1 { + self.0.first() + } else { + None + } + } + + /// Return the only instruction, contained in `Self`, iff only one exists (returns `self` + /// otherwise). + pub fn into_only(mut self) -> core::result::Result, Self> { + if self.0.len() == 1 { + self.0.pop().ok_or(self) + } else { + Err(self) + } + } +} + +/// A prelude for importing all types typically used when interacting with XCM messages. +pub mod prelude { + mod contents { + pub use super::super::{ + Ancestor, AncestorThen, + AssetId::{self, *}, + AssetInstance::{self, *}, + BodyId, BodyPart, Error as XcmError, ExecuteXcm, + Fungibility::{self, *}, + Instruction::*, + InteriorMultiLocation, + Junction::{self, *}, + Junctions::{self, *}, + MultiAsset, + MultiAssetFilter::{self, *}, + MultiAssets, MultiLocation, + NetworkId::{self, *}, + OriginKind, Outcome, Parent, ParentThen, Response, Result as XcmResult, SendError, + SendResult, SendXcm, + WeightLimit::{self, *}, + WildFungibility::{self, Fungible as WildFungible, NonFungible as WildNonFungible}, + WildMultiAsset::{self, *}, + }; + } + pub use super::{Instruction, Xcm}; + pub use contents::*; + pub mod opaque { + pub use super::{ + super::opaque::{Instruction, Xcm}, + contents::*, + }; + } +} + +/// Response data to a query. +#[derive(Clone, Eq, PartialEq, Encode, Decode, Debug)] +pub enum Response { + /// No response. Serves as a neutral default. + Null, + /// Some assets. + Assets(MultiAssets), + /// The outcome of an XCM instruction. + ExecutionResult(result::Result<(), (u32, Error)>), +} + +impl Default for Response { + fn default() -> Self { + Self::Null + } +} + +/// An optional weight limit. +#[derive(Clone, Eq, PartialEq, Encode, Decode, Debug)] +pub enum WeightLimit { + /// No weight limit imposed. + Unlimited, + /// Weight limit imposed of the inner value. + Limited(#[codec(compact)] u64), +} + +impl From> for WeightLimit { + fn from(x: Option) -> Self { + match x { + Some(w) => WeightLimit::Limited(w), + None => WeightLimit::Unlimited, + } + } +} + +impl From for Option { + fn from(x: WeightLimit) -> Self { + match x { + WeightLimit::Limited(w) => Some(w), + WeightLimit::Unlimited => None, + } + } +} + +/// Cross-Consensus Message: A message from one consensus system to another. +/// +/// Consensus systems that may send and receive messages include blockchains and smart contracts. +/// +/// All messages are delivered from a known *origin*, expressed as a `MultiLocation`. +/// +/// This is the inner XCM format and is version-sensitive. Messages are typically passed using the outer +/// XCM format, known as `VersionedXcm`. +#[derive(Derivative, Encode, Decode)] +#[derivative(Clone(bound = ""), Eq(bound = ""), PartialEq(bound = ""), Debug(bound = ""))] +#[codec(encode_bound())] +#[codec(decode_bound())] +pub enum Instruction { + /// Withdraw asset(s) (`assets`) from the ownership of `origin` and place them into the Holding + /// Register. + /// + /// - `assets`: The asset(s) to be withdrawn into holding. + /// + /// Kind: *Instruction*. + /// + /// Errors: + WithdrawAsset(MultiAssets), + + /// Asset(s) (`assets`) have been received into the ownership of this system on the `origin` + /// system and equivalent derivatives should be placed into the Holding Register. + /// + /// - `assets`: The asset(s) that are minted into holding. + /// + /// Safety: `origin` must be trusted to have received and be storing `assets` such that they + /// may later be withdrawn should this system send a corresponding message. + /// + /// Kind: *Trusted Indication*. + /// + /// Errors: + ReserveAssetDeposited(MultiAssets), + + /// Asset(s) (`assets`) have been destroyed on the `origin` system and equivalent assets should + /// be created and placed into the Holding Register. + /// + /// - `assets`: The asset(s) that are minted into the Holding Register. + /// + /// Safety: `origin` must be trusted to have irrevocably destroyed the corresponding `assets` + /// prior as a consequence of sending this message. + /// + /// Kind: *Trusted Indication*. + /// + /// Errors: + ReceiveTeleportedAsset(MultiAssets), + + /// Respond with information that the local system is expecting. + /// + /// - `query_id`: The identifier of the query that resulted in this message being sent. + /// - `response`: The message content. + /// - `max_weight`: The maximum weight that handling this response should take. + /// + /// Safety: No concerns. + /// + /// Kind: *Information*. + /// + /// Errors: + QueryResponse { + #[codec(compact)] + query_id: u64, + response: Response, + #[codec(compact)] + max_weight: u64, + }, + + /// Withdraw asset(s) (`assets`) from the ownership of `origin` and place equivalent assets + /// under the ownership of `beneficiary`. + /// + /// - `assets`: The asset(s) to be withdrawn. + /// - `beneficiary`: The new owner for the assets. + /// + /// Safety: No concerns. + /// + /// Kind: *Instruction*. + /// + /// Errors: + TransferAsset { assets: MultiAssets, beneficiary: MultiLocation }, + + /// Withdraw asset(s) (`assets`) from the ownership of `origin` and place equivalent assets + /// under the ownership of `dest` within this consensus system (i.e. its sovereign account). + /// + /// Send an onward XCM message to `dest` of `ReserveAssetDeposited` with the given + /// `xcm`. + /// + /// - `assets`: The asset(s) to be withdrawn. + /// - `dest`: The location whose sovereign account will own the assets and thus the effective + /// beneficiary for the assets and the notification target for the reserve asset deposit + /// message. + /// - `xcm`: The instructions that should follow the `ReserveAssetDeposited` + /// instruction, which is sent onwards to `dest`. + /// + /// Safety: No concerns. + /// + /// Kind: *Instruction*. + /// + /// Errors: + TransferReserveAsset { assets: MultiAssets, dest: MultiLocation, xcm: Xcm<()> }, + + /// Apply the encoded transaction `call`, whose dispatch-origin should be `origin` as expressed + /// by the kind of origin `origin_type`. + /// + /// - `origin_type`: The means of expressing the message origin as a dispatch origin. + /// - `max_weight`: The weight of `call`; this should be at least the chain's calculated weight + /// and will be used in the weight determination arithmetic. + /// - `call`: The encoded transaction to be applied. + /// + /// Safety: No concerns. + /// + /// Kind: *Instruction*. + /// + /// Errors: + Transact { origin_type: OriginKind, 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. + /// + /// - `sender`: The sender in the to-be opened channel. Also, the initiator of the channel opening. + /// - `max_message_size`: The maximum size of a message proposed by the sender. + /// - `max_capacity`: The maximum number of messages that can be queued in the channel. + /// + /// Safety: The message should originate directly from the relay-chain. + /// + /// Kind: *System Notification* + HrmpNewChannelOpenRequest { + #[codec(compact)] + sender: u32, + #[codec(compact)] + max_message_size: u32, + #[codec(compact)] + max_capacity: u32, + }, + + /// A message to notify about that a previously sent open channel request has been accepted by + /// the recipient. That means that the channel will be opened during the next relay-chain session + /// change. This message is meant to be sent by the relay-chain to a para. + /// + /// Safety: The message should originate directly from the relay-chain. + /// + /// Kind: *System Notification* + /// + /// Errors: + HrmpChannelAccepted { + // NOTE: We keep this as a structured item to a) keep it consistent with the other Hrmp + // items; and b) because the field's meaning is not obvious/mentioned from the item name. + #[codec(compact)] + recipient: u32, + }, + + /// A message to notify that the other party in an open channel decided to close it. In particular, + /// `initiator` is going to close the channel opened from `sender` to the `recipient`. The close + /// will be enacted at the next relay-chain session change. This message is meant to be sent by + /// the relay-chain to a para. + /// + /// Safety: The message should originate directly from the relay-chain. + /// + /// Kind: *System Notification* + /// + /// Errors: + HrmpChannelClosing { + #[codec(compact)] + initiator: u32, + #[codec(compact)] + sender: u32, + #[codec(compact)] + recipient: u32, + }, + + /// Clear the origin. + /// + /// This may be used by the XCM author to ensure that later instructions cannot command the + /// authority of the origin (e.g. if they are being relayed from an untrusted source, as often + /// the case with `ReserveAssetDeposited`). + /// + /// Safety: No concerns. + /// + /// Kind: *Instruction*. + /// + /// Errors: + ClearOrigin, + + /// Mutate the origin to some interior location. + /// + /// Kind: *Instruction* + /// + /// Errors: + DescendOrigin(InteriorMultiLocation), + + /// Immediately report the contents of the Error Register to the given destination via XCM. + /// + /// A `QueryResponse` message of type `ExecutionOutcome` is sent to `dest` with the given + /// `query_id` and the outcome of the XCM. + /// + /// Kind: *Instruction* + /// + /// Errors: + ReportError { + #[codec(compact)] + query_id: u64, + dest: MultiLocation, + #[codec(compact)] + max_response_weight: u64, + }, + + /// Remove the asset(s) (`assets`) from the Holding Register and place equivalent assets under + /// the ownership of `beneficiary` within this consensus system. + /// + /// - `assets`: The asset(s) to remove from holding. + /// - `max_assets`: The maximum number of unique assets/asset instances to remove from holding. + /// Only the first `max_assets` assets/instances of those matched by `assets` will be removed, + /// prioritized under standard asset ordering. Any others will remain in holding. + /// - `beneficiary`: The new owner for the assets. + /// + /// Errors: + DepositAsset { assets: MultiAssetFilter, 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 + /// account). + /// + /// Send an onward XCM message to `dest` of `ReserveAssetDeposited` with the given `effects`. + /// + /// - `assets`: The asset(s) to remove from holding. + /// - `max_assets`: The maximum number of unique assets/asset instances to remove from holding. + /// Only the first `max_assets` assets/instances of those matched by `assets` will be removed, + /// prioritized under standard asset ordering. Any others will remain in holding. + /// - `dest`: The location whose sovereign account will own the assets and thus the effective + /// beneficiary for the assets and the notification target for the reserve asset deposit + /// message. + /// - `xcm`: The orders that should follow the `ReserveAssetDeposited` instruction + /// which is sent onwards to `dest`. + /// + /// Errors: + DepositReserveAsset { + assets: MultiAssetFilter, + max_assets: u32, + dest: MultiLocation, + xcm: Xcm<()>, + }, + + /// Remove the asset(s) (`give`) from the Holding Register and replace them with alternative + /// assets. + /// + /// The minimum amount of assets to be received into the Holding Register for the order not to + /// fail may be stated. + /// + /// - `give`: The asset(s) to remove from holding. + /// - `receive`: The minimum amount of assets(s) which `give` should be exchanged for. + /// + /// Errors: + ExchangeAsset { give: MultiAssetFilter, receive: MultiAssets }, + + /// Remove the asset(s) (`assets`) from holding and send a `WithdrawAsset` XCM message to a + /// reserve location. + /// + /// - `assets`: The asset(s) to remove from holding. + /// - `reserve`: A valid location that acts as a reserve for all asset(s) in `assets`. The + /// sovereign account of this consensus system *on the reserve location* will have appropriate + /// assets withdrawn and `effects` will be executed on them. There will typically be only one + /// valid location on any given asset/chain combination. + /// - `xcm`: The instructions to execute on the assets once withdrawn *on the reserve + /// location*. + /// + /// Errors: + InitiateReserveWithdraw { assets: MultiAssetFilter, reserve: MultiLocation, xcm: Xcm<()> }, + + /// Remove the asset(s) (`assets`) from holding and send a `ReceiveTeleportedAsset` XCM message + /// to a `dest` location. + /// + /// - `assets`: The asset(s) to remove from holding. + /// - `dest`: A valid location that respects teleports coming from this location. + /// - `xcm`: The instructions to execute on the assets once arrived *on the destination + /// location*. + /// + /// NOTE: The `dest` location *MUST* respect this origin as a valid teleportation origin for all + /// `assets`. If it does not, then the assets may be lost. + /// + /// Errors: + InitiateTeleport { assets: MultiAssetFilter, dest: MultiLocation, xcm: Xcm<()> }, + + /// Send a `Balances` XCM message with the `assets` value equal to the holding contents, or a + /// portion thereof. + /// + /// - `query_id`: An identifier that will be replicated into the returned XCM message. + /// - `dest`: A valid destination for the returned XCM message. This may be limited to the + /// current origin. + /// - `assets`: A filter for the assets that should be reported back. The assets reported back + /// will be, asset-wise, *the lesser of this value and the holding register*. No wildcards + /// will be used when reporting assets back. + /// - `max_response_weight`: The maximum amount of weight that the `QueryResponse` item which + /// is sent as a reply may take to execute. NOTE: If this is unexpectedly large then the + /// response may not execute at all. + /// + /// Errors: + QueryHolding { + #[codec(compact)] + query_id: u64, + dest: MultiLocation, + assets: MultiAssetFilter, + #[codec(compact)] + max_response_weight: u64, + }, + + /// Pay for the execution of some XCM `xcm` and `orders` with up to `weight` + /// picoseconds of execution time, paying for this with up to `fees` from the Holding Register. + /// + /// - `fees`: The asset(s) to remove from the Holding Register to pay for fees. + /// - `weight_limit`: The maximum amount of weight to purchase; this must be at least the + /// expected maximum weight of the total XCM to be executed for the + /// `AllowTopLevelPaidExecutionFrom` barrier to allow the XCM be executed. + /// + /// Errors: + BuyExecution { fees: MultiAsset, weight_limit: WeightLimit }, + + /// Refund any surplus weight previously bought with `BuyExecution`. + RefundSurplus, + + /// Set code that should be called in the case of an error happening. + /// + /// An error occurring within execution of this code will _NOT_ result in the error register + /// being set, nor will an error handler be called due to it. The error handler and appendix + /// may each still be set. + /// + /// The apparent weight of this instruction is inclusive of the inner `Xcm`; the executing + /// weight however includes only the difference between the previous handler and the new + /// handler, which can reasonably be negative, which would result in a surplus. + SetErrorHandler(Xcm), + + /// Set code that should be called after code execution (including the error handler if any) + /// is finished. This will be called regardless of whether an error occurred. + /// + /// Any error occurring due to execution of this code will result in the error register being + /// set, and the error handler (if set) firing. + /// + /// The apparent weight of this instruction is inclusive of the inner `Xcm`; the executing + /// weight however includes only the difference between the previous appendix and the new + /// appendix, which can reasonably be negative, which would result in a surplus. + SetAppendix(Xcm), + + /// Clear the error register. + ClearError, +} + +impl Xcm { + pub fn into(self) -> Xcm { + Xcm::from(self) + } + pub fn from(xcm: Xcm) -> Self { + Self(xcm.0.into_iter().map(Instruction::::from).collect()) + } +} + +impl Instruction { + pub fn into(self) -> Instruction { + Instruction::from(self) + } + pub fn from(xcm: Instruction) -> Self { + use Instruction::*; + match xcm { + WithdrawAsset(assets) => WithdrawAsset(assets), + ReserveAssetDeposited(assets) => ReserveAssetDeposited(assets), + ReceiveTeleportedAsset(assets) => ReceiveTeleportedAsset(assets), + QueryResponse { query_id, response, max_weight } => + QueryResponse { query_id, response, max_weight }, + TransferAsset { assets, beneficiary } => TransferAsset { assets, beneficiary }, + TransferReserveAsset { assets, dest, xcm } => + TransferReserveAsset { assets, dest, xcm }, + HrmpNewChannelOpenRequest { sender, max_message_size, max_capacity } => + HrmpNewChannelOpenRequest { sender, max_message_size, max_capacity }, + HrmpChannelAccepted { recipient } => HrmpChannelAccepted { recipient }, + HrmpChannelClosing { initiator, sender, recipient } => + HrmpChannelClosing { initiator, sender, recipient }, + Transact { origin_type, require_weight_at_most, call } => + Transact { origin_type, require_weight_at_most, call: call.into() }, + ReportError { query_id, dest, max_response_weight } => + ReportError { query_id, dest, max_response_weight }, + DepositAsset { assets, max_assets, beneficiary } => + DepositAsset { assets, max_assets, beneficiary }, + DepositReserveAsset { assets, max_assets, dest, xcm } => + DepositReserveAsset { assets, max_assets, dest, xcm }, + ExchangeAsset { give, receive } => ExchangeAsset { give, receive }, + InitiateReserveWithdraw { assets, reserve, xcm } => + InitiateReserveWithdraw { assets, reserve, xcm }, + InitiateTeleport { assets, dest, xcm } => InitiateTeleport { assets, dest, xcm }, + QueryHolding { query_id, dest, assets, max_response_weight } => + QueryHolding { query_id, dest, assets, max_response_weight }, + BuyExecution { fees, weight_limit } => BuyExecution { fees, weight_limit }, + ClearOrigin => ClearOrigin, + DescendOrigin(who) => DescendOrigin(who), + RefundSurplus => RefundSurplus, + SetErrorHandler(xcm) => SetErrorHandler(xcm.into()), + SetAppendix(xcm) => SetAppendix(xcm.into()), + ClearError => ClearError, + } + } +} + +pub mod opaque { + /// The basic concrete type of `Xcm`, which doesn't make any assumptions about the + /// format of a call other than it is pre-encoded. + pub type Xcm = super::Xcm<()>; + + /// The basic concrete type of `Instruction`, which doesn't make any assumptions about the + /// format of a call other than it is pre-encoded. + pub type Instruction = super::Instruction<()>; +} + +// Convert from a v1 response to a v2 response +impl TryFrom for Response { + type Error = (); + fn try_from(old_response: OldResponse) -> result::Result { + match old_response { + OldResponse::Assets(assets) => Ok(Self::Assets(assets)), + } + } +} + +impl TryFrom> for Xcm { + type Error = (); + fn try_from(old: OldXcm) -> result::Result, ()> { + use Instruction::*; + Ok(Xcm(match old { + OldXcm::WithdrawAsset { assets, effects } => Some(Ok(WithdrawAsset(assets))) + .into_iter() + .chain(effects.into_iter().map(Instruction::try_from)) + .collect::, _>>()?, + OldXcm::ReserveAssetDeposited { assets, effects } => + Some(Ok(ReserveAssetDeposited(assets))) + .into_iter() + .chain(Some(Ok(ClearOrigin)).into_iter()) + .chain(effects.into_iter().map(Instruction::try_from)) + .collect::, _>>()?, + OldXcm::ReceiveTeleportedAsset { assets, effects } => + Some(Ok(ReceiveTeleportedAsset(assets))) + .into_iter() + .chain(Some(Ok(ClearOrigin)).into_iter()) + .chain(effects.into_iter().map(Instruction::try_from)) + .collect::, _>>()?, + OldXcm::QueryResponse { query_id, response } => vec![QueryResponse { + query_id, + response: response.try_into()?, + max_weight: 50_000_000, + }], + OldXcm::TransferAsset { assets, beneficiary } => + vec![TransferAsset { assets, beneficiary }], + OldXcm::TransferReserveAsset { assets, dest, effects } => vec![TransferReserveAsset { + assets, + dest, + xcm: Xcm(effects + .into_iter() + .map(Instruction::<()>::try_from) + .collect::>()?), + }], + OldXcm::HrmpNewChannelOpenRequest { sender, max_message_size, max_capacity } => + vec![HrmpNewChannelOpenRequest { sender, max_message_size, max_capacity }], + OldXcm::HrmpChannelAccepted { recipient } => vec![HrmpChannelAccepted { recipient }], + OldXcm::HrmpChannelClosing { initiator, sender, recipient } => + vec![HrmpChannelClosing { initiator, sender, recipient }], + OldXcm::Transact { origin_type, require_weight_at_most, call } => + 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(()), + })) + } +} + +impl TryFrom> for Instruction { + type Error = (); + fn try_from(old: OldOrder) -> result::Result, ()> { + use Instruction::*; + Ok(match old { + OldOrder::Noop => return Err(()), + OldOrder::DepositAsset { assets, max_assets, beneficiary } => + DepositAsset { assets, max_assets, beneficiary }, + OldOrder::DepositReserveAsset { assets, max_assets, dest, effects } => + DepositReserveAsset { + assets, + max_assets, + dest, + xcm: Xcm(effects + .into_iter() + .map(Instruction::<()>::try_from) + .collect::>()?), + }, + OldOrder::ExchangeAsset { give, receive } => ExchangeAsset { give, receive }, + OldOrder::InitiateReserveWithdraw { assets, reserve, effects } => + InitiateReserveWithdraw { + assets, + reserve, + xcm: Xcm(effects + .into_iter() + .map(Instruction::<()>::try_from) + .collect::>()?), + }, + OldOrder::InitiateTeleport { assets, dest, effects } => InitiateTeleport { + assets, + dest, + xcm: Xcm(effects + .into_iter() + .map(Instruction::<()>::try_from) + .collect::>()?), + }, + OldOrder::QueryHolding { query_id, dest, assets } => + QueryHolding { query_id, dest, assets, max_response_weight: 0 }, + OldOrder::BuyExecution { fees, debt, instructions, .. } => { + // We don't handle nested XCM. + if !instructions.is_empty() { + return Err(()) + } + BuyExecution { fees, weight_limit: WeightLimit::Limited(debt) } + }, + }) + } +} + +#[cfg(test)] +mod tests { + use super::{prelude::*, *}; + + #[test] + fn basic_roundtrip_works() { + let xcm = + Xcm::<()>(vec![TransferAsset { assets: (Here, 1).into(), beneficiary: Here.into() }]); + let old_xcm = + OldXcm::<()>::TransferAsset { assets: (Here, 1).into(), beneficiary: Here.into() }; + assert_eq!(old_xcm, OldXcm::<()>::try_from(xcm.clone()).unwrap()); + let new_xcm: Xcm<()> = old_xcm.try_into().unwrap(); + assert_eq!(new_xcm, xcm); + } + + #[test] + fn teleport_roundtrip_works() { + let xcm = Xcm::<()>(vec![ + ReceiveTeleportedAsset((Here, 1).into()), + ClearOrigin, + DepositAsset { assets: Wild(All), max_assets: 1, beneficiary: Here.into() }, + ]); + let old_xcm: OldXcm<()> = OldXcm::<()>::ReceiveTeleportedAsset { + assets: (Here, 1).into(), + effects: vec![OldOrder::DepositAsset { + assets: Wild(All), + max_assets: 1, + beneficiary: Here.into(), + }], + }; + assert_eq!(old_xcm, OldXcm::<()>::try_from(xcm.clone()).unwrap()); + let new_xcm: Xcm<()> = old_xcm.try_into().unwrap(); + assert_eq!(new_xcm, xcm); + } + + #[test] + fn reserve_deposit_roundtrip_works() { + let xcm = Xcm::<()>(vec![ + ReserveAssetDeposited((Here, 1).into()), + ClearOrigin, + BuyExecution { fees: (Here, 1).into(), weight_limit: Some(1).into() }, + DepositAsset { assets: Wild(All), max_assets: 1, beneficiary: Here.into() }, + ]); + let old_xcm: OldXcm<()> = OldXcm::<()>::ReserveAssetDeposited { + assets: (Here, 1).into(), + effects: vec![ + OldOrder::BuyExecution { + fees: (Here, 1).into(), + debt: 1, + weight: 0, + instructions: vec![], + halt_on_error: true, + }, + OldOrder::DepositAsset { + assets: Wild(All), + max_assets: 1, + beneficiary: Here.into(), + }, + ], + }; + assert_eq!(old_xcm, OldXcm::<()>::try_from(xcm.clone()).unwrap()); + let new_xcm: Xcm<()> = old_xcm.try_into().unwrap(); + assert_eq!(new_xcm, xcm); + } +} diff --git a/xcm/src/v2/traits.rs b/xcm/src/v2/traits.rs new file mode 100644 index 000000000000..facbdd089263 --- /dev/null +++ b/xcm/src/v2/traits.rs @@ -0,0 +1,308 @@ +// Copyright 2020 Parity Technologies (UK) Ltd. +// This file is part of Cumulus. + +// Substrate 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. + +// Substrate 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 Cumulus. If not, see . + +//! Cross-Consensus Message format data structures. + +use core::result; +use parity_scale_codec::{Decode, Encode}; + +use super::{MultiLocation, Xcm}; + +#[derive(Copy, Clone, Encode, Decode, Eq, PartialEq, Debug)] +pub enum Error { + Undefined, + /// An arithmetic overflow happened. + Overflow, + /// The operation is intentionally unsupported. + Unimplemented, + UnhandledXcmVersion, + /// The implementation does not handle a given XCM. + UnhandledXcmMessage, + /// The implementation does not handle an effect present in an XCM. + UnhandledEffect, + EscalationOfPrivilege, + UntrustedReserveLocation, + UntrustedTeleportLocation, + DestinationBufferOverflow, + MultiLocationFull, + FailedToDecode, + BadOrigin, + ExceedsMaxMessageSize, + /// An asset transaction (like withdraw or deposit) failed. + /// See implementers of the `TransactAsset` trait for sources. + /// Causes can include type conversion failures between id or balance types. + FailedToTransactAsset(#[codec(skip)] &'static str), + /// Execution of the XCM would potentially result in a greater weight used than the pre-specified + /// weight limit. The amount that is potentially required is the parameter. + WeightLimitReached(Weight), + /// An asset wildcard was passed where it was not expected (e.g. as the asset to withdraw in a + /// `WithdrawAsset` XCM). + Wildcard, + /// The case where an XCM message has specified a optional weight limit and the weight required for + /// processing is too great. + /// + /// Used by: + /// - `Transact` + TooMuchWeightRequired, + /// The fees specified by the XCM message were not found in the holding register. + /// + /// Used by: + /// - `BuyExecution` + NotHoldingFees, + /// The weight of an XCM message is not computable ahead of execution. This generally means at least part + /// of the message is invalid, which could be due to it containing overly nested structures or an invalid + /// nested data segment (e.g. for the call in `Transact`). + WeightNotComputable, + /// The XCM did not pass the barrier condition for execution. The barrier condition differs on different + /// chains and in different circumstances, but generally it means that the conditions surrounding the message + /// were not such that the chain considers the message worth spending time executing. Since most chains + /// lift the barrier to execution on appropriate payment, presentation of an NFT voucher, or based on the + /// message origin, it means that none of those were the case. + Barrier, + /// Indicates that it is not possible for a location to have an asset be withdrawn or transferred from its + /// ownership. This probably means it doesn't own (enough of) it, but may also indicate that it is under a + /// lock, hold, freeze or is otherwise unavailable. + NotWithdrawable, + /// Indicates that the consensus system cannot deposit an asset under the ownership of a particular location. + LocationCannotHold, + /// The assets given to purchase weight is are insufficient for the weight desired. + TooExpensive, + /// The given asset is not handled. + AssetNotFound, + /// The given message cannot be translated into a format that the destination can be expected to interpret. + DestinationUnsupported, + /// `execute_xcm` has been called too many times recursively. + RecursionLimitReached, + /// Destination is routable, but there is some issue with the transport mechanism. + /// + /// A human-readable explanation of the specific issue is provided. + Transport(#[codec(skip)] &'static str), + /// Destination is known to be unroutable. + Unroutable, + /// The weight required was not specified when it should have been. + UnknownWeightRequired, +} + +impl From<()> for Error { + fn from(_: ()) -> Self { + Self::Undefined + } +} + +impl From for Error { + fn from(e: SendError) -> Self { + match e { + SendError::CannotReachDestination(..) | SendError::Unroutable => Error::Unroutable, + SendError::Transport(s) => Error::Transport(s), + SendError::DestinationUnsupported => Error::DestinationUnsupported, + SendError::ExceedsMaxMessageSize => Error::ExceedsMaxMessageSize, + } + } +} + +pub type Result = result::Result<(), Error>; + +/// Local weight type; execution time in picoseconds. +pub type Weight = u64; + +/// Outcome of an XCM execution. +#[derive(Clone, Encode, Decode, Eq, PartialEq, Debug)] +pub enum Outcome { + /// Execution completed successfully; given weight was used. + Complete(Weight), + /// Execution started, but did not complete successfully due to the given error; given weight was used. + Incomplete(Weight, Error), + /// Execution did not start due to the given error. + Error(Error), +} + +impl Outcome { + pub fn ensure_complete(self) -> Result { + match self { + Outcome::Complete(_) => Ok(()), + Outcome::Incomplete(_, e) => Err(e), + Outcome::Error(e) => Err(e), + } + } + pub fn ensure_execution(self) -> result::Result { + match self { + Outcome::Complete(w) => Ok(w), + Outcome::Incomplete(w, _) => Ok(w), + Outcome::Error(e) => Err(e), + } + } + /// How much weight was used by the XCM execution attempt. + pub fn weight_used(&self) -> Weight { + match self { + Outcome::Complete(w) => *w, + Outcome::Incomplete(w, _) => *w, + Outcome::Error(_) => 0, + } + } +} + +/// Type of XCM message executor. +pub trait ExecuteXcm { + /// Execute some XCM `message` from `origin` using no more than `weight_limit` weight. The weight limit is + /// a basic hard-limit and the implementation may place further restrictions or requirements on weight and + /// other aspects. + fn execute_xcm(origin: MultiLocation, message: Xcm, weight_limit: Weight) -> Outcome { + log::debug!( + target: "xcm::execute_xcm", + "origin: {:?}, message: {:?}, weight_limit: {:?}", + origin, + message, + weight_limit, + ); + Self::execute_xcm_in_credit(origin, message, weight_limit, 0) + } + + /// Execute some XCM `message` from `origin` using no more than `weight_limit` weight. + /// + /// Some amount of `weight_credit` may be provided which, depending on the implementation, may allow + /// execution without associated payment. + fn execute_xcm_in_credit( + origin: MultiLocation, + message: Xcm, + weight_limit: Weight, + weight_credit: Weight, + ) -> Outcome; +} + +impl ExecuteXcm for () { + fn execute_xcm_in_credit( + _origin: MultiLocation, + _message: Xcm, + _weight_limit: Weight, + _weight_credit: Weight, + ) -> Outcome { + Outcome::Error(Error::Unimplemented) + } +} + +/// Error result value when attempting to send an XCM message. +#[derive(Clone, Encode, Decode, Eq, PartialEq, Debug)] +pub enum SendError { + /// The message and destination combination was not recognized as being reachable. + /// + /// This is not considered fatal: if there are alternative transport routes available, then + /// they may be attempted. For this reason, the destination and message are contained. + CannotReachDestination(MultiLocation, Xcm<()>), + /// Destination is routable, but there is some issue with the transport mechanism. This is + /// considered fatal. + /// A human-readable explanation of the specific issue is provided. + Transport(#[codec(skip)] &'static str), + /// Destination is known to be unroutable. This is considered fatal. + Unroutable, + /// The given message cannot be translated into a format that the destination can be expected + /// to interpret. + DestinationUnsupported, + /// Message could not be sent due to its size exceeding the maximum allowed by the transport + /// layer. + ExceedsMaxMessageSize, +} + +/// Result value when attempting to send an XCM message. +pub type SendResult = result::Result<(), SendError>; + +/// Utility for sending an XCM message. +/// +/// These can be amalgamated in tuples to form sophisticated routing systems. In tuple format, each router might return +/// `CannotReachDestination` to pass the execution to the next sender item. Note that each `CannotReachDestination` +/// might alter the destination and the XCM message for to the next router. +/// +/// +/// # Example +/// ```rust +/// # use xcm::v2::prelude::*; +/// # use parity_scale_codec::Encode; +/// +/// /// A sender that only passes the message through and does nothing. +/// struct Sender1; +/// impl SendXcm for Sender1 { +/// fn send_xcm(destination: MultiLocation, message: Xcm<()>) -> SendResult { +/// return Err(SendError::CannotReachDestination(destination, message)) +/// } +/// } +/// +/// /// A sender that accepts a message that has an X2 junction, otherwise stops the routing. +/// struct Sender2; +/// impl SendXcm for Sender2 { +/// fn send_xcm(destination: MultiLocation, message: Xcm<()>) -> SendResult { +/// if let MultiLocation { parents: 0, interior: X2(j1, j2) } = destination { +/// Ok(()) +/// } else { +/// Err(SendError::Unroutable) +/// } +/// } +/// } +/// +/// /// A sender that accepts a message from a parent, passing through otherwise. +/// struct Sender3; +/// impl SendXcm for Sender3 { +/// fn send_xcm(destination: MultiLocation, message: Xcm<()>) -> SendResult { +/// match destination { +/// MultiLocation { parents: 1, interior: Here } => Ok(()), +/// _ => Err(SendError::CannotReachDestination(destination, message)), +/// } +/// } +/// } +/// +/// // A call to send via XCM. We don't really care about this. +/// # fn main() { +/// let call: Vec = ().encode(); +/// let message = Xcm(vec![Instruction::Transact { +/// origin_type: OriginKind::Superuser, +/// require_weight_at_most: 0, +/// call: call.into(), +/// }]); +/// let destination = MultiLocation::parent(); +/// +/// assert!( +/// // Sender2 will block this. +/// <(Sender1, Sender2, Sender3) as SendXcm>::send_xcm(destination.clone(), message.clone()) +/// .is_err() +/// ); +/// +/// assert!( +/// // Sender3 will catch this. +/// <(Sender1, Sender3) as SendXcm>::send_xcm(destination.clone(), message.clone()) +/// .is_ok() +/// ); +/// # } +/// ``` +pub trait SendXcm { + /// Send an XCM `message` to a given `destination`. + /// + /// If it is not a destination which can be reached with this type but possibly could by others, then it *MUST* + /// return `CannotReachDestination`. Any other error will cause the tuple implementation to exit early without + /// trying other type fields. + fn send_xcm(destination: MultiLocation, message: Xcm<()>) -> SendResult; +} + +#[impl_trait_for_tuples::impl_for_tuples(30)] +impl SendXcm for Tuple { + fn send_xcm(destination: MultiLocation, message: Xcm<()>) -> SendResult { + for_tuples!( #( + // we shadow `destination` and `message` in each expansion for the next one. + let (destination, message) = match Tuple::send_xcm(destination, message) { + Err(SendError::CannotReachDestination(d, m)) => (d, m), + o @ _ => return o, + }; + )* ); + Err(SendError::CannotReachDestination(destination, message)) + } +} diff --git a/xcm/xcm-builder/src/barriers.rs b/xcm/xcm-builder/src/barriers.rs index ca1f399a9c45..4d82380a03c1 100644 --- a/xcm/xcm-builder/src/barriers.rs +++ b/xcm/xcm-builder/src/barriers.rs @@ -19,10 +19,10 @@ use frame_support::{ensure, traits::Contains, weights::Weight}; use polkadot_parachain::primitives::IsSystem; use sp_std::{marker::PhantomData, result::Result}; -use xcm::latest::{Junction, Junctions, MultiLocation, Order, Xcm}; +use xcm::latest::{Instruction::*, Junction, Junctions, MultiLocation, WeightLimit::*, Xcm}; use xcm_executor::traits::{OnResponse, ShouldExecute}; -/// Execution barrier that just takes `shallow_weight` from `weight_credit`. +/// Execution barrier that just takes `max_weight` from `weight_credit`. /// /// Useful to allow XCM execution by local chain users via extrinsics. /// E.g. `pallet_xcm::reserve_asset_transfer` to transfer a reserve asset @@ -30,42 +30,53 @@ use xcm_executor::traits::{OnResponse, ShouldExecute}; pub struct TakeWeightCredit; impl ShouldExecute for TakeWeightCredit { fn should_execute( - _origin: &MultiLocation, + _origin: &Option, _top_level: bool, - _message: &Xcm, - shallow_weight: Weight, + _message: &mut Xcm, + max_weight: Weight, weight_credit: &mut Weight, ) -> Result<(), ()> { - *weight_credit = weight_credit.checked_sub(shallow_weight).ok_or(())?; + *weight_credit = weight_credit.checked_sub(max_weight).ok_or(())?; Ok(()) } } -/// Allows execution from `origin` if it is contained in `T` (i.e. `T::Contains(origin)`) taking payments into -/// account. +/// 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` 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, + origin: &Option, top_level: bool, - message: &Xcm, - shallow_weight: Weight, + message: &mut Xcm, + max_weight: Weight, _weight_credit: &mut Weight, ) -> Result<(), ()> { + let origin = origin.as_ref().ok_or(())?; ensure!(T::contains(origin), ()); ensure!(top_level, ()); - match message { - Xcm::ReceiveTeleportedAsset { effects, .. } | - Xcm::WithdrawAsset { effects, .. } | - Xcm::ReserveAssetDeposited { effects, .. } - if matches!( - effects.first(), - Some(Order::BuyExecution { debt, ..}) if *debt >= shallow_weight - ) => - Ok(()), + let mut iter = message.0.iter_mut(); + let i = iter.next().ok_or(())?; + match i { + ReceiveTeleportedAsset(..) | WithdrawAsset(..) | ReserveAssetDeposited(..) => (), + _ => return Err(()), + } + let mut i = iter.next().ok_or(())?; + while let ClearOrigin = i { + i = iter.next().ok_or(())?; + } + match i { + BuyExecution { weight_limit: Limited(ref mut weight), .. } if *weight >= max_weight => { + *weight = max_weight; + Ok(()) + }, + BuyExecution { ref mut weight_limit, .. } if weight_limit == &Unlimited => { + *weight_limit = Limited(max_weight); + Ok(()) + }, _ => Err(()), } } @@ -76,12 +87,13 @@ impl> ShouldExecute for AllowTopLevelPaidExecutionFro pub struct AllowUnpaidExecutionFrom(PhantomData); impl> ShouldExecute for AllowUnpaidExecutionFrom { fn should_execute( - origin: &MultiLocation, + origin: &Option, _top_level: bool, - _message: &Xcm, - _shallow_weight: Weight, + _message: &mut Xcm, + _max_weight: Weight, _weight_credit: &mut Weight, ) -> Result<(), ()> { + let origin = origin.as_ref().ok_or(())?; ensure!(T::contains(origin), ()); Ok(()) } @@ -103,14 +115,15 @@ impl> Contains for IsChildSystemPara pub struct AllowKnownQueryResponses(PhantomData); impl ShouldExecute for AllowKnownQueryResponses { fn should_execute( - origin: &MultiLocation, + origin: &Option, _top_level: bool, - message: &Xcm, - _shallow_weight: Weight, + message: &mut Xcm, + _max_weight: Weight, _weight_credit: &mut Weight, ) -> Result<(), ()> { - match message { - Xcm::QueryResponse { query_id, .. } + let origin = origin.as_ref().ok_or(())?; + match message.0.first() { + Some(QueryResponse { query_id, .. }) if ResponseHandler::expecting_response(origin, *query_id) => Ok(()), _ => Err(()), diff --git a/xcm/xcm-builder/src/mock.rs b/xcm/xcm-builder/src/mock.rs index 3e96e7980210..21cb0c8650c4 100644 --- a/xcm/xcm-builder/src/mock.rs +++ b/xcm/xcm-builder/src/mock.rs @@ -107,7 +107,7 @@ pub fn sent_xcm() -> Vec<(MultiLocation, opaque::Xcm)> { } pub struct TestSendXcm; impl SendXcm for TestSendXcm { - fn send_xcm(dest: MultiLocation, msg: opaque::Xcm) -> XcmResult { + fn send_xcm(dest: MultiLocation, msg: opaque::Xcm) -> SendResult { SENT_XCM.with(|q| q.borrow_mut().push((dest, msg))); Ok(()) } @@ -222,9 +222,10 @@ impl OnResponse for TestResponseHandler { }) } fn on_response( - _origin: MultiLocation, + _origin: &MultiLocation, query_id: u64, response: xcm::latest::Response, + _max_weight: Weight, ) -> Weight { QUERIES.with(|q| { q.borrow_mut().entry(query_id).and_modify(|v| { @@ -258,6 +259,7 @@ parameter_types! { pub static AllowPaidFrom: 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; } pub type TestBarrier = ( @@ -277,7 +279,7 @@ impl Config for TestConfig { type IsTeleporter = TestIsTeleporter; type LocationInverter = LocationInverter; type Barrier = TestBarrier; - type Weigher = FixedWeightBounds; + type Weigher = FixedWeightBounds; type Trader = FixedRateOfFungible; type ResponseHandler = TestResponseHandler; } diff --git a/xcm/xcm-builder/src/tests.rs b/xcm/xcm-builder/src/tests.rs index 25ab06718fcb..22f72041c364 100644 --- a/xcm/xcm-builder/src/tests.rs +++ b/xcm/xcm-builder/src/tests.rs @@ -44,31 +44,21 @@ fn basic_setup_works() { #[test] fn weigher_should_work() { - let mut message = opaque::Xcm::ReserveAssetDeposited { - assets: (Parent, 100).into(), - effects: vec![ - Order::BuyExecution { - fees: (Parent, 1).into(), - weight: 0, - debt: 30, - halt_on_error: true, - instructions: vec![], - }, - Order::DepositAsset { assets: All.into(), max_assets: 1, beneficiary: Here.into() }, - ], - } - .into(); - assert_eq!(::Weigher::shallow(&mut message), Ok(30)); + let mut message = Xcm(vec![ + ReserveAssetDeposited((Parent, 100).into()), + BuyExecution { fees: (Parent, 1).into(), weight_limit: Limited(30) }, + DepositAsset { assets: All.into(), max_assets: 1, beneficiary: Here.into() }, + ]); + assert_eq!(::Weigher::weight(&mut message), Ok(30)); } #[test] fn take_weight_credit_barrier_should_work() { let mut message = - opaque::Xcm::TransferAsset { assets: (Parent, 100).into(), beneficiary: Here.into() }; - + Xcm::<()>(vec![TransferAsset { assets: (Parent, 100).into(), beneficiary: Here.into() }]); let mut weight_credit = 10; let r = TakeWeightCredit::should_execute( - &Parent.into(), + &Some(Parent.into()), true, &mut message, 10, @@ -78,7 +68,7 @@ fn take_weight_credit_barrier_should_work() { assert_eq!(weight_credit, 0); let r = TakeWeightCredit::should_execute( - &Parent.into(), + &Some(Parent.into()), true, &mut message, 10, @@ -91,12 +81,12 @@ fn take_weight_credit_barrier_should_work() { #[test] fn allow_unpaid_should_work() { let mut message = - opaque::Xcm::TransferAsset { assets: (Parent, 100).into(), beneficiary: Here.into() }; + Xcm::<()>(vec![TransferAsset { assets: (Parent, 100).into(), beneficiary: Here.into() }]); AllowUnpaidFrom::set(vec![Parent.into()]); let r = AllowUnpaidExecutionFrom::>::should_execute( - &Parachain(1).into(), + &Some(Parachain(1).into()), true, &mut message, 10, @@ -105,7 +95,7 @@ fn allow_unpaid_should_work() { assert_eq!(r, Err(())); let r = AllowUnpaidExecutionFrom::>::should_execute( - &Parent.into(), + &Some(Parent.into()), true, &mut message, 10, @@ -119,10 +109,10 @@ fn allow_paid_should_work() { AllowPaidFrom::set(vec![Parent.into()]); let mut message = - opaque::Xcm::TransferAsset { assets: (Parent, 100).into(), beneficiary: Here.into() }; + Xcm::<()>(vec![TransferAsset { assets: (Parent, 100).into(), beneficiary: Here.into() }]); let r = AllowTopLevelPaidExecutionFrom::>::should_execute( - &Parachain(1).into(), + &Some(Parachain(1).into()), true, &mut message, 10, @@ -131,22 +121,14 @@ fn allow_paid_should_work() { assert_eq!(r, Err(())); let fees = (Parent, 1).into(); - let mut underpaying_message = opaque::Xcm::ReserveAssetDeposited { - assets: (Parent, 100).into(), - effects: vec![ - Order::BuyExecution { - fees, - weight: 0, - debt: 20, - halt_on_error: true, - instructions: vec![], - }, - Order::DepositAsset { assets: All.into(), max_assets: 1, beneficiary: Here.into() }, - ], - }; + let mut underpaying_message = Xcm::<()>(vec![ + ReserveAssetDeposited((Parent, 100).into()), + BuyExecution { fees, weight_limit: Limited(20) }, + DepositAsset { assets: All.into(), max_assets: 1, beneficiary: Here.into() }, + ]); let r = AllowTopLevelPaidExecutionFrom::>::should_execute( - &Parent.into(), + &Some(Parent.into()), true, &mut underpaying_message, 30, @@ -155,22 +137,14 @@ fn allow_paid_should_work() { assert_eq!(r, Err(())); let fees = (Parent, 1).into(); - let mut paying_message = opaque::Xcm::ReserveAssetDeposited { - assets: (Parent, 100).into(), - effects: vec![ - Order::BuyExecution { - fees, - weight: 0, - debt: 30, - halt_on_error: true, - instructions: vec![], - }, - Order::DepositAsset { assets: All.into(), max_assets: 1, beneficiary: Here.into() }, - ], - }; + let mut paying_message = Xcm::<()>(vec![ + ReserveAssetDeposited((Parent, 100).into()), + BuyExecution { fees, weight_limit: Limited(30) }, + DepositAsset { assets: All.into(), max_assets: 1, beneficiary: Here.into() }, + ]); let r = AllowTopLevelPaidExecutionFrom::>::should_execute( - &Parachain(1).into(), + &Some(Parachain(1).into()), true, &mut paying_message, 30, @@ -179,7 +153,7 @@ fn allow_paid_should_work() { assert_eq!(r, Err(())); let r = AllowTopLevelPaidExecutionFrom::>::should_execute( - &Parent.into(), + &Some(Parent.into()), true, &mut paying_message, 30, @@ -196,23 +170,11 @@ fn paying_reserve_deposit_should_work() { let origin = Parent.into(); let fees = (Parent, 30).into(); - let message = Xcm::::ReserveAssetDeposited { - assets: (Parent, 100).into(), - effects: vec![ - Order::::BuyExecution { - fees, - weight: 0, - debt: 30, - halt_on_error: true, - instructions: vec![], - }, - Order::::DepositAsset { - assets: All.into(), - max_assets: 1, - beneficiary: Here.into(), - }, - ], - }; + let message = Xcm(vec![ + ReserveAssetDeposited((Parent, 100).into()), + BuyExecution { fees, weight_limit: Limited(30) }, + DepositAsset { assets: All.into(), max_assets: 1, beneficiary: Here.into() }, + ]); let weight_limit = 50; let r = XcmExecutor::::execute_xcm(origin, message, weight_limit); assert_eq!(r, Outcome::Complete(30)); @@ -228,10 +190,10 @@ fn transfer_should_work() { // They want to transfer 100 of them to their sibling parachain #2 let r = XcmExecutor::::execute_xcm( Parachain(1).into(), - Xcm::TransferAsset { + Xcm(vec![TransferAsset { assets: (Here, 100).into(), beneficiary: X1(AccountIndex64 { index: 3, network: Any }).into(), - }, + }]), 50, ); assert_eq!(r, Outcome::Complete(10)); @@ -240,6 +202,132 @@ fn transfer_should_work() { assert_eq!(sent_xcm(), vec![]); } +#[test] +fn errors_should_return_unused_weight() { + // we'll let them have message execution for free. + AllowUnpaidFrom::set(vec![Here.into()]); + // We own 1000 of our tokens. + add_asset(3000, (Here, 11)); + let mut message = Xcm(vec![ + // First xfer results in an error on the last message only + TransferAsset { + assets: (Here, 1).into(), + beneficiary: X1(AccountIndex64 { index: 3, network: Any }).into(), + }, + // Second xfer results in error third message and after + TransferAsset { + assets: (Here, 2).into(), + beneficiary: X1(AccountIndex64 { index: 3, network: Any }).into(), + }, + // Third xfer results in error second message and after + TransferAsset { + assets: (Here, 4).into(), + beneficiary: X1(AccountIndex64 { index: 3, network: Any }).into(), + }, + ]); + // Weight limit of 70 is needed. + let limit = ::Weigher::weight(&mut message).unwrap(); + assert_eq!(limit, 30); + + let r = XcmExecutor::::execute_xcm(Here.into(), message.clone(), limit); + assert_eq!(r, Outcome::Complete(30)); + assert_eq!(assets(3), vec![(Here, 7).into()]); + assert_eq!(assets(3000), vec![(Here, 4).into()]); + assert_eq!(sent_xcm(), vec![]); + + let r = XcmExecutor::::execute_xcm(Here.into(), message.clone(), limit); + assert_eq!(r, Outcome::Incomplete(30, XcmError::NotWithdrawable)); + assert_eq!(assets(3), vec![(Here, 10).into()]); + assert_eq!(assets(3000), vec![(Here, 1).into()]); + assert_eq!(sent_xcm(), vec![]); + + let r = XcmExecutor::::execute_xcm(Here.into(), message.clone(), limit); + assert_eq!(r, Outcome::Incomplete(20, XcmError::NotWithdrawable)); + assert_eq!(assets(3), vec![(Here, 11).into()]); + assert_eq!(assets(3000), vec![]); + assert_eq!(sent_xcm(), vec![]); + + let r = XcmExecutor::::execute_xcm(Here.into(), message, limit); + assert_eq!(r, Outcome::Incomplete(10, XcmError::NotWithdrawable)); + assert_eq!(assets(3), vec![(Here, 11).into()]); + assert_eq!(assets(3000), vec![]); + assert_eq!(sent_xcm(), vec![]); +} + +#[test] +fn weight_bounds_should_respect_instructions_limit() { + MaxInstructions::set(3); + let mut message = Xcm(vec![ClearOrigin; 4]); + // 4 instructions are too many. + assert_eq!(::Weigher::weight(&mut message), Err(())); + + let mut message = + Xcm(vec![SetErrorHandler(Xcm(vec![ClearOrigin])), SetAppendix(Xcm(vec![ClearOrigin]))]); + // 4 instructions are too many, even when hidden within 2. + assert_eq!(::Weigher::weight(&mut message), Err(())); + + let mut message = + Xcm(vec![SetErrorHandler(Xcm(vec![SetErrorHandler(Xcm(vec![SetErrorHandler(Xcm( + vec![ClearOrigin], + ))]))]))]); + // 4 instructions are too many, even when it's just one that's 3 levels deep. + assert_eq!(::Weigher::weight(&mut message), Err(())); + + let mut message = + Xcm(vec![SetErrorHandler(Xcm(vec![SetErrorHandler(Xcm(vec![ClearOrigin]))]))]); + // 3 instructions are OK. + assert_eq!(::Weigher::weight(&mut message), Ok(30)); +} + +#[test] +fn code_registers_should_work() { + // we'll let them have message execution for free. + AllowUnpaidFrom::set(vec![Here.into()]); + // We own 1000 of our tokens. + add_asset(3000, (Here, 21)); + let mut message = Xcm(vec![ + // Set our error handler - this will fire only on the second message, when there's an error + SetErrorHandler(Xcm(vec![ + TransferAsset { + assets: (Here, 2).into(), + beneficiary: X1(AccountIndex64 { index: 3, network: Any }).into(), + }, + // It was handled fine. + ClearError, + ])), + // Set the appendix - this will always fire. + SetAppendix(Xcm(vec![TransferAsset { + assets: (Here, 4).into(), + beneficiary: X1(AccountIndex64 { index: 3, network: Any }).into(), + }])), + // First xfer always works ok + TransferAsset { + assets: (Here, 1).into(), + beneficiary: X1(AccountIndex64 { index: 3, network: Any }).into(), + }, + // Second xfer results in error on the second message - our error handler will fire. + TransferAsset { + assets: (Here, 8).into(), + beneficiary: X1(AccountIndex64 { index: 3, network: Any }).into(), + }, + ]); + // Weight limit of 70 is needed. + let limit = ::Weigher::weight(&mut message).unwrap(); + assert_eq!(limit, 70); + + let r = XcmExecutor::::execute_xcm(Here.into(), message.clone(), limit); + assert_eq!(r, Outcome::Complete(50)); // We don't pay the 20 weight for the error handler. + assert_eq!(assets(3), vec![(Here, 13).into()]); + assert_eq!(assets(3000), vec![(Here, 8).into()]); + assert_eq!(sent_xcm(), vec![]); + + let r = XcmExecutor::::execute_xcm(Here.into(), message, limit); + assert_eq!(r, Outcome::Complete(70)); // We pay the full weight here. + assert_eq!(assets(3), vec![(Here, 20).into()]); + assert_eq!(assets(3000), vec![(Here, 1).into()]); + assert_eq!(sent_xcm(), vec![]); +} + #[test] fn reserve_transfer_should_work() { AllowUnpaidFrom::set(vec![X1(Parachain(1)).into()]); @@ -252,15 +340,15 @@ fn reserve_transfer_should_work() { // and let them know to hand it to account #3. let r = XcmExecutor::::execute_xcm( Parachain(1).into(), - Xcm::TransferReserveAsset { + Xcm(vec![TransferReserveAsset { assets: (Here, 100).into(), dest: Parachain(2).into(), - effects: vec![Order::DepositAsset { + xcm: Xcm::<()>(vec![DepositAsset { assets: All.into(), max_assets: 1, beneficiary: three.clone(), - }], - }, + }]), + }]), 50, ); assert_eq!(r, Outcome::Complete(10)); @@ -270,14 +358,11 @@ fn reserve_transfer_should_work() { sent_xcm(), vec![( Parachain(2).into(), - Xcm::ReserveAssetDeposited { - assets: (Parent, 100).into(), - effects: vec![Order::DepositAsset { - assets: All.into(), - max_assets: 1, - beneficiary: three - }], - } + Xcm::<()>(vec![ + ReserveAssetDeposited((Parent, 100).into()), + ClearOrigin, + DepositAsset { assets: All.into(), max_assets: 1, beneficiary: three }, + ]), )] ); } @@ -287,11 +372,11 @@ fn transacting_should_work() { AllowUnpaidFrom::set(vec![Parent.into()]); let origin = Parent.into(); - let message = Xcm::::Transact { + let message = Xcm::(vec![Transact { origin_type: OriginKind::Native, require_weight_at_most: 50, call: TestCall::Any(50, None).encode().into(), - }; + }]); let weight_limit = 60; let r = XcmExecutor::::execute_xcm(origin, message, weight_limit); assert_eq!(r, Outcome::Complete(60)); @@ -302,14 +387,14 @@ fn transacting_should_respect_max_weight_requirement() { AllowUnpaidFrom::set(vec![Parent.into()]); let origin = Parent.into(); - let message = Xcm::::Transact { + let message = Xcm::(vec![Transact { origin_type: OriginKind::Native, require_weight_at_most: 40, call: TestCall::Any(50, None).encode().into(), - }; + }]); let weight_limit = 60; let r = XcmExecutor::::execute_xcm(origin, message, weight_limit); - assert_eq!(r, Outcome::Incomplete(60, XcmError::TooMuchWeightRequired)); + assert_eq!(r, Outcome::Incomplete(50, XcmError::TooMuchWeightRequired)); } #[test] @@ -317,11 +402,11 @@ fn transacting_should_refund_weight() { AllowUnpaidFrom::set(vec![Parent.into()]); let origin = Parent.into(); - let message = Xcm::::Transact { + let message = Xcm::(vec![Transact { origin_type: OriginKind::Native, require_weight_at_most: 50, call: TestCall::Any(50, Some(30)).encode().into(), - }; + }]); let weight_limit = 60; let r = XcmExecutor::::execute_xcm(origin, message, weight_limit); assert_eq!(r, Outcome::Complete(40)); @@ -336,32 +421,22 @@ fn paid_transacting_should_refund_payment_for_unused_weight() { let origin = one.clone(); let fees = (Parent, 100).into(); - let message = Xcm::::WithdrawAsset { - assets: (Parent, 100).into(), // enough for 100 units of weight. - effects: vec![ - Order::::BuyExecution { - fees, - weight: 70, - debt: 30, - halt_on_error: true, - instructions: vec![Xcm::::Transact { - origin_type: OriginKind::Native, - require_weight_at_most: 60, - // call estimated at 70 but only takes 10. - call: TestCall::Any(60, Some(10)).encode().into(), - }], - }, - Order::::DepositAsset { - assets: All.into(), - max_assets: 1, - beneficiary: one.clone(), - }, - ], - }; + let message = Xcm::(vec![ + WithdrawAsset((Parent, 100).into()), // enough for 100 units of weight. + BuyExecution { fees, weight_limit: Limited(100) }, + Transact { + origin_type: OriginKind::Native, + require_weight_at_most: 50, + // call estimated at 50 but only takes 10. + call: TestCall::Any(50, Some(10)).encode().into(), + }, + RefundSurplus, + DepositAsset { assets: All.into(), max_assets: 1, beneficiary: one.clone() }, + ]); let weight_limit = 100; let r = XcmExecutor::::execute_xcm(origin, message, weight_limit); - assert_eq!(r, Outcome::Complete(50)); - assert_eq!(assets(1), vec![(Parent, 50).into()]); + assert_eq!(r, Outcome::Complete(60)); + assert_eq!(assets(1), vec![(Parent, 40).into()]); } #[test] @@ -372,7 +447,11 @@ fn prepaid_result_of_query_should_get_free_execution() { expect_response(query_id, origin.clone()); let the_response = Response::Assets((Parent, 100).into()); - let message = Xcm::::QueryResponse { query_id, response: the_response.clone() }; + let message = Xcm::(vec![QueryResponse { + query_id, + response: the_response.clone(), + max_weight: 10, + }]); let weight_limit = 10; // First time the response gets through since we're expecting it... @@ -382,7 +461,7 @@ fn prepaid_result_of_query_should_get_free_execution() { // Second time it doesn't, since we're not. let r = XcmExecutor::::execute_xcm(origin.clone(), message.clone(), weight_limit); - assert_eq!(r, Outcome::Incomplete(10, XcmError::Barrier)); + assert_eq!(r, Outcome::Error(XcmError::Barrier)); } fn fungible_multi_asset(location: MultiLocation, amount: u128) -> MultiAsset { diff --git a/xcm/xcm-builder/src/weight.rs b/xcm/xcm-builder/src/weight.rs index 515d7dbf22c4..c438ae4edbd4 100644 --- a/xcm/xcm-builder/src/weight.rs +++ b/xcm/xcm-builder/src/weight.rs @@ -21,76 +21,43 @@ use frame_support::{ use parity_scale_codec::Decode; use sp_runtime::traits::{SaturatedConversion, Saturating, Zero}; use sp_std::{convert::TryInto, marker::PhantomData, result::Result}; -use xcm::latest::{AssetId, AssetId::Concrete, Error, MultiAsset, MultiLocation, Order, Xcm}; +use xcm::latest::prelude::*; use xcm_executor::{ traits::{WeightBounds, WeightTrader}, Assets, }; -pub struct FixedWeightBounds(PhantomData<(T, C)>); -impl, C: Decode + GetDispatchInfo> WeightBounds for FixedWeightBounds { - fn shallow(message: &mut Xcm) -> Result { - Ok(match message { - Xcm::Transact { call, .. } => - call.ensure_decoded()?.get_dispatch_info().weight.saturating_add(T::get()), - Xcm::RelayedFrom { ref mut message, .. } => - T::get().saturating_add(Self::shallow(message.as_mut())?), - Xcm::WithdrawAsset { effects, .. } | - Xcm::ReserveAssetDeposited { effects, .. } | - Xcm::ReceiveTeleportedAsset { effects, .. } => { - let mut extra = T::get(); - for order in effects.iter_mut() { - extra.saturating_accrue(Self::shallow_order(order)?); - } - extra - }, - _ => T::get(), - }) +pub struct FixedWeightBounds(PhantomData<(T, C, M)>); +impl, C: Decode + GetDispatchInfo, M: Get> WeightBounds + for FixedWeightBounds +{ + fn weight(message: &mut Xcm) -> Result { + let mut instructions_left = M::get(); + Self::weight_with_limit(message, &mut instructions_left) } - fn deep(message: &mut Xcm) -> Result { - Ok(match message { - Xcm::RelayedFrom { ref mut message, .. } => Self::deep(message.as_mut())?, - Xcm::WithdrawAsset { effects, .. } | - Xcm::ReserveAssetDeposited { effects, .. } | - Xcm::ReceiveTeleportedAsset { effects, .. } => { - let mut extra = 0; - for order in effects.iter_mut() { - extra.saturating_accrue(Self::deep_order(order)?); - } - extra - }, - _ => 0, - }) + fn instr_weight(message: &Instruction) -> Result { + Self::instr_weight_with_limit(message, &mut u32::max_value()) } } -impl, C: Decode + GetDispatchInfo> FixedWeightBounds { - fn shallow_order(order: &mut Order) -> Result { - Ok(match order { - Order::BuyExecution { .. } => { - // On success, execution of this will result in more weight being consumed but - // we don't count it here since this is only the *shallow*, non-negotiable weight - // spend and doesn't count weight placed behind a `BuyExecution` since it will not - // be definitely consumed from any existing weight credit if execution of the message - // is attempted. - T::get() - }, - _ => T::get(), - }) - } - fn deep_order(order: &mut Order) -> Result { - Ok(match order { - Order::BuyExecution { instructions, .. } => { - let mut extra = 0; - for instruction in instructions.iter_mut() { - extra.saturating_accrue( - Self::shallow(instruction)?.saturating_add(Self::deep(instruction)?), - ); - } - extra - }, +impl, C: Decode + GetDispatchInfo, M> FixedWeightBounds { + fn weight_with_limit(message: &Xcm, instrs_limit: &mut u32) -> Result { + let mut r = 0; + *instrs_limit = instrs_limit.checked_sub(message.0.len() as u32).ok_or(())?; + for m in message.0.iter() { + r += Self::instr_weight_with_limit(m, instrs_limit)?; + } + Ok(r) + } + fn instr_weight_with_limit( + message: &Instruction, + instrs_limit: &mut u32, + ) -> Result { + Ok(T::get().saturating_add(match message { + Transact { require_weight_at_most, .. } => *require_weight_at_most, + SetErrorHandler(xcm) | SetAppendix(xcm) => Self::weight_with_limit(xcm, instrs_limit)?, _ => 0, - }) + })) } } @@ -124,10 +91,11 @@ impl, R: TakeRevenue> WeightTrader Self(0, 0, PhantomData) } - fn buy_weight(&mut self, weight: Weight, payment: Assets) -> Result { + fn buy_weight(&mut self, weight: Weight, payment: Assets) -> Result { let (id, units_per_second) = T::get(); let amount = units_per_second * (weight as u128) / (WEIGHT_PER_SECOND as u128); - let unused = payment.checked_sub((id, amount).into()).map_err(|_| Error::TooExpensive)?; + let unused = + payment.checked_sub((id, amount).into()).map_err(|_| XcmError::TooExpensive)?; self.0 = self.0.saturating_add(weight); self.1 = self.1.saturating_add(amount); Ok(unused) @@ -169,13 +137,14 @@ impl, R: TakeRevenue> WeightTrader for FixedRateOfFungib Self(0, 0, PhantomData) } - fn buy_weight(&mut self, weight: Weight, payment: Assets) -> Result { + fn buy_weight(&mut self, weight: Weight, payment: Assets) -> Result { let (id, units_per_second) = T::get(); let amount = units_per_second * (weight as u128) / (WEIGHT_PER_SECOND as u128); if amount == 0 { return Ok(payment) } - let unused = payment.checked_sub((id, amount).into()).map_err(|_| Error::TooExpensive)?; + let unused = + payment.checked_sub((id, amount).into()).map_err(|_| XcmError::TooExpensive)?; self.0 = self.0.saturating_add(weight); self.1 = self.1.saturating_add(amount); Ok(unused) @@ -228,11 +197,11 @@ impl< Self(0, Zero::zero(), PhantomData) } - fn buy_weight(&mut self, weight: Weight, payment: Assets) -> Result { + fn buy_weight(&mut self, weight: Weight, payment: Assets) -> Result { let amount = WeightToFee::calc(&weight); - let u128_amount: u128 = amount.try_into().map_err(|_| Error::Overflow)?; + let u128_amount: u128 = amount.try_into().map_err(|_| XcmError::Overflow)?; let required = (Concrete(AssetId::get()), u128_amount).into(); - let unused = payment.checked_sub(required).map_err(|_| Error::TooExpensive)?; + let unused = payment.checked_sub(required).map_err(|_| XcmError::TooExpensive)?; self.0 = self.0.saturating_add(weight); self.1 = self.1.saturating_add(amount); Ok(unused) diff --git a/xcm/xcm-builder/tests/mock/mod.rs b/xcm/xcm-builder/tests/mock/mod.rs index fa40c9e231fc..ed64ce9470da 100644 --- a/xcm/xcm-builder/tests/mock/mod.rs +++ b/xcm/xcm-builder/tests/mock/mod.rs @@ -47,7 +47,7 @@ pub fn sent_xcm() -> Vec<(MultiLocation, opaque::Xcm)> { } pub struct TestSendXcm; impl SendXcm for TestSendXcm { - fn send_xcm(dest: MultiLocation, msg: opaque::Xcm) -> XcmResult { + fn send_xcm(dest: MultiLocation, msg: opaque::Xcm) -> SendResult { SENT_XCM.with(|q| q.borrow_mut().push((dest, msg))); Ok(()) } @@ -150,6 +150,7 @@ pub type Barrier = ( parameter_types! { pub const KusamaForStatemint: (MultiAssetFilter, MultiLocation) = (MultiAssetFilter::Wild(WildMultiAsset::AllOf { id: Concrete(MultiLocation::here()), fun: WildFungible }), X1(Parachain(1000)).into()); + pub const MaxInstructions: u32 = 100; } pub type TrustedTeleporters = (xcm_builder::Case,); @@ -163,7 +164,7 @@ impl xcm_executor::Config for XcmConfig { type IsTeleporter = TrustedTeleporters; type LocationInverter = LocationInverter; type Barrier = Barrier; - type Weigher = FixedWeightBounds; + type Weigher = FixedWeightBounds; type Trader = FixedRateOfFungible; type ResponseHandler = (); } @@ -181,7 +182,9 @@ impl pallet_xcm::Config for Runtime { type XcmExecutor = XcmExecutor; type XcmTeleportFilter = Everything; type XcmReserveTransferFilter = Everything; - type Weigher = FixedWeightBounds; + type Weigher = FixedWeightBounds; + type Call = Call; + type Origin = Origin; } impl origin::Config for Runtime {} @@ -198,7 +201,7 @@ construct_runtime!( System: frame_system::{Pallet, Call, Storage, Config, Event}, Balances: pallet_balances::{Pallet, Call, Storage, Config, Event}, ParasOrigin: origin::{Pallet, Origin}, - XcmPallet: pallet_xcm::{Pallet, Call, Storage, Event}, + XcmPallet: pallet_xcm::{Pallet, Call, Storage, Event, Origin}, } ); diff --git a/xcm/xcm-builder/tests/scenarios.rs b/xcm/xcm-builder/tests/scenarios.rs index bb3186fd36d0..077a6590d576 100644 --- a/xcm/xcm-builder/tests/scenarios.rs +++ b/xcm/xcm-builder/tests/scenarios.rs @@ -16,7 +16,6 @@ mod mock; -use frame_support::weights::Weight; use mock::{ kusama_like_with_balances, AccountId, Balance, Balances, BaseXcmWeight, XcmConfig, CENTS, }; @@ -31,15 +30,8 @@ pub const INITIAL_BALANCE: u128 = 100_000_000_000; pub const REGISTER_AMOUNT: Balance = 10 * CENTS; // Construct a `BuyExecution` order. -fn buy_execution(debt: Weight) -> Order { - use xcm::latest::prelude::*; - Order::BuyExecution { - fees: (Here, REGISTER_AMOUNT).into(), - weight: 0, - debt, - halt_on_error: false, - instructions: vec![], - } +fn buy_execution() -> Instruction { + BuyExecution { fees: (Here, REGISTER_AMOUNT).into(), weight_limit: Unlimited } } /// Scenario: @@ -56,17 +48,15 @@ fn withdraw_and_deposit_works() { let weight = 3 * BaseXcmWeight::get(); let r = XcmExecutor::::execute_xcm( Parachain(PARA_ID).into(), - Xcm::WithdrawAsset { - assets: vec![(Here, amount).into()].into(), - effects: vec![ - buy_execution(weight), - Order::DepositAsset { - assets: All.into(), - max_assets: 1, - beneficiary: Parachain(other_para_id).into(), - }, - ], - }, + Xcm(vec![ + WithdrawAsset((Here, amount).into()), + buy_execution(), + DepositAsset { + assets: All.into(), + max_assets: 1, + beneficiary: Parachain(other_para_id).into(), + }, + ]), weight, ); assert_eq!(r, Outcome::Complete(weight)); @@ -94,31 +84,31 @@ fn query_holding_works() { let amount = REGISTER_AMOUNT; let query_id = 1234; let weight = 4 * BaseXcmWeight::get(); + let max_response_weight = 1_000_000_000; let r = XcmExecutor::::execute_xcm( Parachain(PARA_ID).into(), - Xcm::WithdrawAsset { - assets: vec![(Here, amount).into()].into(), - effects: vec![ - buy_execution(weight), - Order::DepositAsset { - assets: All.into(), - max_assets: 1, - beneficiary: OnlyChild.into(), // invalid destination - }, - // is not triggered becasue the deposit fails - Order::QueryHolding { - query_id, - dest: Parachain(PARA_ID).into(), - assets: All.into(), - }, - ], - }, + Xcm(vec![ + WithdrawAsset((Here, amount).into()), + buy_execution(), + DepositAsset { + assets: All.into(), + max_assets: 1, + beneficiary: OnlyChild.into(), // invalid destination + }, + // is not triggered becasue the deposit fails + QueryHolding { + query_id, + dest: Parachain(PARA_ID).into(), + assets: All.into(), + max_response_weight, + }, + ]), weight, ); assert_eq!( r, Outcome::Incomplete( - weight, + weight - BaseXcmWeight::get(), XcmError::FailedToTransactAsset("AccountIdConversionFailed") ) ); @@ -129,23 +119,22 @@ fn query_holding_works() { // now do a successful transfer let r = XcmExecutor::::execute_xcm( Parachain(PARA_ID).into(), - Xcm::WithdrawAsset { - assets: vec![(Here, amount).into()].into(), - effects: vec![ - buy_execution(weight), - Order::DepositAsset { - assets: All.into(), - max_assets: 1, - beneficiary: Parachain(other_para_id).into(), - }, - // used to get a notification in case of success - Order::QueryHolding { - query_id, - dest: Parachain(PARA_ID).into(), - assets: All.into(), - }, - ], - }, + Xcm(vec![ + WithdrawAsset((Here, amount).into()), + buy_execution(), + DepositAsset { + assets: All.into(), + max_assets: 1, + beneficiary: Parachain(other_para_id).into(), + }, + // used to get a notification in case of success + QueryHolding { + query_id, + dest: Parachain(PARA_ID).into(), + assets: All.into(), + max_response_weight: 1_000_000_000, + }, + ]), weight, ); assert_eq!(r, Outcome::Complete(weight)); @@ -156,7 +145,11 @@ fn query_holding_works() { mock::sent_xcm(), vec![( Parachain(PARA_ID).into(), - Xcm::QueryResponse { query_id, response: Response::Assets(vec![].into()) } + Xcm(vec![QueryResponse { + query_id, + response: Response::Assets(vec![].into()), + max_weight: 1_000_000_000, + }]), )] ); }); @@ -180,8 +173,8 @@ fn teleport_to_statemine_works() { let other_para_id = 3000; let amount = REGISTER_AMOUNT; let teleport_effects = vec![ - buy_execution(5), // unchecked mock value - Order::DepositAsset { + buy_execution(), // unchecked mock value + DepositAsset { assets: All.into(), max_assets: 1, beneficiary: (1, Parachain(PARA_ID)).into(), @@ -192,17 +185,15 @@ fn teleport_to_statemine_works() { // teleports are allowed to community chains, even in the absence of trust from their side. let r = XcmExecutor::::execute_xcm( Parachain(PARA_ID).into(), - Xcm::WithdrawAsset { - assets: vec![(Here, amount).into()].into(), - effects: vec![ - buy_execution(weight), - Order::InitiateTeleport { - assets: All.into(), - dest: Parachain(other_para_id).into(), - effects: teleport_effects.clone(), - }, - ], - }, + Xcm(vec![ + WithdrawAsset((Here, amount).into()), + buy_execution(), + InitiateTeleport { + assets: All.into(), + dest: Parachain(other_para_id).into(), + xcm: Xcm(teleport_effects.clone()), + }, + ]), weight, ); assert_eq!(r, Outcome::Complete(weight)); @@ -210,27 +201,25 @@ fn teleport_to_statemine_works() { mock::sent_xcm(), vec![( Parachain(other_para_id).into(), - Xcm::ReceiveTeleportedAsset { - assets: vec![(Parent, amount).into()].into(), - effects: teleport_effects.clone(), - } + Xcm(vec![ReceiveTeleportedAsset((Parent, amount).into()), ClearOrigin,] + .into_iter() + .chain(teleport_effects.clone().into_iter()) + .collect()) )] ); // teleports are allowed from statemine to kusama. let r = XcmExecutor::::execute_xcm( Parachain(PARA_ID).into(), - Xcm::WithdrawAsset { - assets: vec![(Here, amount).into()].into(), - effects: vec![ - buy_execution(weight), - Order::InitiateTeleport { - assets: All.into(), - dest: Parachain(statemine_id).into(), - effects: teleport_effects.clone(), - }, - ], - }, + Xcm(vec![ + WithdrawAsset((Here, amount).into()), + buy_execution(), + InitiateTeleport { + assets: All.into(), + dest: Parachain(statemine_id).into(), + xcm: Xcm(teleport_effects.clone()), + }, + ]), weight, ); assert_eq!(r, Outcome::Complete(weight)); @@ -241,17 +230,17 @@ fn teleport_to_statemine_works() { vec![ ( Parachain(other_para_id).into(), - Xcm::ReceiveTeleportedAsset { - assets: vec![(Parent, amount).into()].into(), - effects: teleport_effects.clone(), - } + Xcm(vec![ReceiveTeleportedAsset((Parent, amount).into()), ClearOrigin,] + .into_iter() + .chain(teleport_effects.clone().into_iter()) + .collect()), ), ( Parachain(statemine_id).into(), - Xcm::ReceiveTeleportedAsset { - assets: vec![(Parent, amount).into()].into(), - effects: teleport_effects, - } + Xcm(vec![ReceiveTeleportedAsset((Parent, amount).into()), ClearOrigin,] + .into_iter() + .chain(teleport_effects.clone().into_iter()) + .collect()), ) ] ); @@ -273,8 +262,8 @@ fn reserve_based_transfer_works() { let other_para_id = 3000; let amount = REGISTER_AMOUNT; let transfer_effects = vec![ - buy_execution(5), // unchecked mock value - Order::DepositAsset { + buy_execution(), // unchecked mock value + DepositAsset { assets: All.into(), max_assets: 1, beneficiary: (1, Parachain(PARA_ID)).into(), @@ -283,18 +272,16 @@ fn reserve_based_transfer_works() { let weight = 3 * BaseXcmWeight::get(); let r = XcmExecutor::::execute_xcm( Parachain(PARA_ID).into(), - Xcm::WithdrawAsset { - assets: vec![(Here, amount).into()].into(), - effects: vec![ - buy_execution(weight), - Order::DepositReserveAsset { - assets: All.into(), - max_assets: 1, - dest: Parachain(other_para_id).into(), - effects: transfer_effects.clone(), - }, - ], - }, + Xcm(vec![ + WithdrawAsset((Here, amount).into()), + buy_execution(), + DepositReserveAsset { + assets: All.into(), + max_assets: 1, + dest: Parachain(other_para_id).into(), + xcm: Xcm(transfer_effects.clone()), + }, + ]), weight, ); assert_eq!(r, Outcome::Complete(weight)); @@ -303,10 +290,10 @@ fn reserve_based_transfer_works() { mock::sent_xcm(), vec![( Parachain(other_para_id).into(), - Xcm::ReserveAssetDeposited { - assets: vec![(Parent, amount).into()].into(), - effects: transfer_effects, - } + Xcm(vec![ReserveAssetDeposited((Parent, amount).into()), ClearOrigin,] + .into_iter() + .chain(transfer_effects.into_iter()) + .collect()) )] ); }); diff --git a/xcm/xcm-executor/integration-tests/src/lib.rs b/xcm/xcm-executor/integration-tests/src/lib.rs index 78b03dd82ccd..72aa7b15d07b 100644 --- a/xcm/xcm-executor/integration-tests/src/lib.rs +++ b/xcm/xcm-executor/integration-tests/src/lib.rs @@ -21,46 +21,35 @@ use polkadot_test_client::{ BlockBuilderExt, ClientBlockImportExt, DefaultTestClientBuilderExt, ExecutionStrategy, InitPolkadotBlockBuilder, TestClientBuilder, TestClientBuilderExt, }; +use polkadot_test_runtime::pallet_test_notifier; use polkadot_test_service::construct_extrinsic; use sp_runtime::{generic::BlockId, traits::Block}; use sp_state_machine::InspectState; -use xcm::{latest::prelude::*, VersionedXcm}; -use xcm_executor::MAX_RECURSION_LIMIT; - -// This is the inflection point where the test should either fail or pass. -const MAX_RECURSION_CHECK: u32 = MAX_RECURSION_LIMIT / 2; +use xcm::{latest::prelude::*, VersionedResponse, VersionedXcm}; #[test] -fn execute_within_recursion_limit() { +fn basic_buy_fees_message_executes() { sp_tracing::try_init_simple(); let mut client = TestClientBuilder::new() .set_execution_strategy(ExecutionStrategy::AlwaysWasm) .build(); - let mut msg = WithdrawAsset { assets: (Parent, 100).into(), effects: vec![] }; - for _ in 0..MAX_RECURSION_CHECK { - msg = WithdrawAsset { - assets: (Parent, 100).into(), - effects: vec![Order::BuyExecution { - fees: (Parent, 1).into(), - weight: 0, - debt: 0, - halt_on_error: true, - // nest `msg` into itself on each iteration. - instructions: vec![msg], - }], - }; - } + let msg = Xcm(vec![ + WithdrawAsset((Parent, 100).into()), + BuyExecution { fees: (Parent, 1).into(), weight_limit: Unlimited }, + DepositAsset { assets: Wild(All), max_assets: 1, beneficiary: Parent.into() }, + ]); let mut block_builder = client.init_polkadot_block_builder(); let execute = construct_extrinsic( &client, polkadot_test_runtime::Call::Xcm(pallet_xcm::Call::execute( - Box::new(VersionedXcm::from(msg.clone())), + Box::new(VersionedXcm::from(msg)), 1_000_000_000, )), sp_keyring::Sr25519Keyring::Alice, + 0, ); block_builder.push_polkadot_extrinsic(execute).expect("pushes extrinsic"); @@ -79,42 +68,153 @@ fn execute_within_recursion_limit() { r.event, polkadot_test_runtime::Event::Xcm(pallet_xcm::Event::Attempted(Outcome::Complete( _ - )),), + ))), ))); }); } #[test] -fn exceed_recursion_limit() { +fn query_response_fires() { + use pallet_test_notifier::Event::*; + use pallet_xcm::QueryStatus; + use polkadot_test_runtime::Event::TestNotifier; + sp_tracing::try_init_simple(); let mut client = TestClientBuilder::new() .set_execution_strategy(ExecutionStrategy::AlwaysWasm) .build(); - let mut msg = WithdrawAsset { assets: (Parent, 100).into(), effects: vec![] }; - for _ in 0..(MAX_RECURSION_CHECK + 1) { - msg = WithdrawAsset { - assets: (Parent, 100).into(), - effects: vec![Order::BuyExecution { - fees: (Parent, 1).into(), - weight: 0, - debt: 0, - halt_on_error: true, - // nest `msg` into itself on each iteration. - instructions: vec![msg], - }], - }; - } + let mut block_builder = client.init_polkadot_block_builder(); + + let execute = construct_extrinsic( + &client, + polkadot_test_runtime::Call::TestNotifier(pallet_test_notifier::Call::prepare_new_query()), + sp_keyring::Sr25519Keyring::Alice, + 0, + ); + + block_builder.push_polkadot_extrinsic(execute).expect("pushes extrinsic"); + + let block = block_builder.build().expect("Finalizes the block").block; + let block_hash = block.hash(); + + futures::executor::block_on(client.import(sp_consensus::BlockOrigin::Own, block)) + .expect("imports the block"); + + let mut query_id = None; + client + .state_at(&BlockId::Hash(block_hash)) + .expect("state should exist") + .inspect_state(|| { + for r in polkadot_test_runtime::System::events().iter() { + match r.event { + TestNotifier(QueryPrepared(q)) => query_id = Some(q), + _ => (), + } + } + }); + let query_id = query_id.unwrap(); let mut block_builder = client.init_polkadot_block_builder(); + let response = Response::ExecutionResult(Ok(())); + let max_weight = 1_000_000; + let msg = Xcm(vec![QueryResponse { query_id, response, max_weight }]); + let msg = Box::new(VersionedXcm::from(msg)); + + let execute = construct_extrinsic( + &client, + polkadot_test_runtime::Call::Xcm(pallet_xcm::Call::execute(msg, 1_000_000_000)), + sp_keyring::Sr25519Keyring::Alice, + 1, + ); + + block_builder.push_polkadot_extrinsic(execute).expect("pushes extrinsic"); + + let block = block_builder.build().expect("Finalizes the block").block; + let block_hash = block.hash(); + + futures::executor::block_on(client.import(sp_consensus::BlockOrigin::Own, block)) + .expect("imports the block"); + + client + .state_at(&BlockId::Hash(block_hash)) + .expect("state should exist") + .inspect_state(|| { + assert!(polkadot_test_runtime::System::events().iter().any(|r| matches!( + r.event, + polkadot_test_runtime::Event::Xcm(pallet_xcm::Event::ResponseReady( + q, + Response::ExecutionResult(Ok(())), + )) if q == query_id, + ))); + assert_eq!( + polkadot_test_runtime::Xcm::query(query_id), + Some(QueryStatus::Ready { + response: VersionedResponse::V2(Response::ExecutionResult(Ok(()))), + at: 2u32.into() + }), + ) + }); +} + +#[test] +fn query_response_elicits_handler() { + use pallet_test_notifier::Event::*; + use polkadot_test_runtime::Event::TestNotifier; + + sp_tracing::try_init_simple(); + let mut client = TestClientBuilder::new() + .set_execution_strategy(ExecutionStrategy::AlwaysWasm) + .build(); + + let mut block_builder = client.init_polkadot_block_builder(); + + let execute = construct_extrinsic( + &client, + polkadot_test_runtime::Call::TestNotifier( + pallet_test_notifier::Call::prepare_new_notify_query(), + ), + sp_keyring::Sr25519Keyring::Alice, + 0, + ); + + block_builder.push_polkadot_extrinsic(execute).expect("pushes extrinsic"); + + let block = block_builder.build().expect("Finalizes the block").block; + let block_hash = block.hash(); + + futures::executor::block_on(client.import(sp_consensus::BlockOrigin::Own, block)) + .expect("imports the block"); + + let mut query_id = None; + client + .state_at(&BlockId::Hash(block_hash)) + .expect("state should exist") + .inspect_state(|| { + for r in polkadot_test_runtime::System::events().iter() { + match r.event { + TestNotifier(NotifyQueryPrepared(q)) => query_id = Some(q), + _ => (), + } + } + }); + let query_id = query_id.unwrap(); + + let mut block_builder = client.init_polkadot_block_builder(); + + let response = Response::ExecutionResult(Ok(())); + let max_weight = 1_000_000; + let msg = Xcm(vec![QueryResponse { query_id, response, max_weight }]); + let execute = construct_extrinsic( &client, polkadot_test_runtime::Call::Xcm(pallet_xcm::Call::execute( - Box::new(VersionedXcm::from(msg.clone())), + Box::new(VersionedXcm::from(msg)), 1_000_000_000, )), sp_keyring::Sr25519Keyring::Alice, + 1, ); block_builder.push_polkadot_extrinsic(execute).expect("pushes extrinsic"); @@ -131,9 +231,11 @@ fn exceed_recursion_limit() { .inspect_state(|| { assert!(polkadot_test_runtime::System::events().iter().any(|r| matches!( r.event, - polkadot_test_runtime::Event::Xcm(pallet_xcm::Event::Attempted( - Outcome::Incomplete(_, XcmError::RecursionLimitReached), - )), + TestNotifier(ResponseReceived( + MultiLocation { parents: 0, interior: X1(Junction::AccountId32 { .. }) }, + q, + Response::ExecutionResult(Ok(())), + )) if q == query_id, ))); }); } diff --git a/xcm/xcm-executor/src/config.rs b/xcm/xcm-executor/src/config.rs index 7f4bf08f5fa0..abb241c49274 100644 --- a/xcm/xcm-executor/src/config.rs +++ b/xcm/xcm-executor/src/config.rs @@ -38,10 +38,10 @@ pub trait Config { /// How to get a call origin from a `OriginKind` value. type OriginConverter: ConvertOrigin<::Origin>; - /// Combinations of (Location, Asset) pairs which we unilateral trust as reserves. + /// Combinations of (Location, Asset) pairs which we trust as reserves. type IsReserve: FilterAssetLocation; - /// Combinations of (Location, Asset) pairs which we bilateral trust as teleporters. + /// Combinations of (Location, Asset) pairs which we trust as teleporters. type IsTeleporter: FilterAssetLocation; /// Means of inverting a location. diff --git a/xcm/xcm-executor/src/lib.rs b/xcm/xcm-executor/src/lib.rs index e85e211a18d0..5484121d5ebc 100644 --- a/xcm/xcm-executor/src/lib.rs +++ b/xcm/xcm-executor/src/lib.rs @@ -21,10 +21,12 @@ use frame_support::{ ensure, weights::GetDispatchInfo, }; +use sp_runtime::traits::Saturating; use sp_std::{marker::PhantomData, prelude::*}; use xcm::latest::{ - Error as XcmError, ExecuteXcm, MultiAssets, MultiLocation, Order, Outcome, Response, SendXcm, - Xcm, + Error as XcmError, ExecuteXcm, + Instruction::{self, *}, + MultiAssets, MultiLocation, Outcome, Response, SendXcm, Xcm, }; pub mod traits; @@ -39,7 +41,25 @@ mod config; pub use config::Config; /// The XCM executor. -pub struct XcmExecutor(PhantomData); +pub struct XcmExecutor { + holding: Assets, + origin: Option, + trader: Config::Trader, + /// The most recent error result and instruction index into the fragment in which it occured, + /// if any. + error: Option<(u32, XcmError)>, + /// The surplus weight, defined as the amount by which `max_weight` is + /// an over-estimate of the actual weight consumed. We do it this way to avoid needing the + /// execution engine to keep track of all instructions' weights (it only needs to care about + /// the weight of dynamically determined instructions such as `Transact`). + total_surplus: u64, + total_refunded: u64, + error_handler: Xcm, + error_handler_weight: u64, + appendix: Xcm, + appendix_weight: u64, + _config: PhantomData, +} /// The maximum recursion limit for `execute_xcm` and `execute_effects`. pub const MAX_RECURSION_LIMIT: u32 = 8; @@ -47,7 +67,7 @@ pub const MAX_RECURSION_LIMIT: u32 = 8; impl ExecuteXcm for XcmExecutor { fn execute_xcm_in_credit( origin: MultiLocation, - message: Xcm, + mut message: Xcm, weight_limit: Weight, mut weight_credit: Weight, ) -> Outcome { @@ -59,143 +79,186 @@ impl ExecuteXcm for XcmExecutor { weight_limit, weight_credit, ); - let mut message = Xcm::::from(message); - let shallow_weight = match Config::Weigher::shallow(&mut message) { - Ok(x) => x, - Err(()) => return Outcome::Error(XcmError::WeightNotComputable), - }; - let deep_weight = match Config::Weigher::deep(&mut message) { + let xcm_weight = match Config::Weigher::weight(&mut message) { Ok(x) => x, Err(()) => return Outcome::Error(XcmError::WeightNotComputable), }; - let maximum_weight = match shallow_weight.checked_add(deep_weight) { - Some(x) => x, - None => return Outcome::Error(XcmError::Overflow), - }; - if maximum_weight > weight_limit { - return Outcome::Error(XcmError::WeightLimitReached(maximum_weight)) + if xcm_weight > weight_limit { + return Outcome::Error(XcmError::WeightLimitReached(xcm_weight)) } - let mut trader = Config::Trader::new(); - let result = Self::do_execute_xcm( - origin, + let origin = Some(origin); + + if let Err(_) = Config::Barrier::should_execute( + &origin, true, - message, + &mut message, + xcm_weight, &mut weight_credit, - Some(shallow_weight), - &mut trader, - 0, - ); - drop(trader); - log::trace!(target: "xcm::execute_xcm", "result: {:?}", &result); - match result { - Ok(surplus) => Outcome::Complete(maximum_weight.saturating_sub(surplus)), - // TODO: #2841 #REALWEIGHT We can do better than returning `maximum_weight` here, and we should otherwise - // we'll needlessly be disregarding block execution time. - Err(e) => Outcome::Incomplete(maximum_weight, e), + ) { + return Outcome::Error(XcmError::Barrier) + } + + let mut vm = Self::new(origin); + + while !message.0.is_empty() { + let result = vm.execute(message); + log::trace!(target: "xcm::execute_xcm_in_credit", "result: {:?}", result); + message = if let Err((i, e, w)) = result { + vm.total_surplus.saturating_accrue(w); + vm.error = Some((i, e)); + vm.take_error_handler().or_else(|| vm.take_appendix()) + } else { + vm.drop_error_handler(); + vm.take_appendix() + } + } + + vm.refund_surplus(); + drop(vm.trader); + + // TODO #2841: Do something with holding? (Fail-safe AssetTrap?) + + let weight_used = xcm_weight.saturating_sub(vm.total_surplus); + match vm.error { + None => Outcome::Complete(weight_used), + // TODO: #2841 #REALWEIGHT We should deduct the cost of any instructions following + // the error which didn't end up being executed. + Some((_, e)) => Outcome::Incomplete(weight_used, e), } } } impl XcmExecutor { - fn reanchored(mut assets: Assets, dest: &MultiLocation) -> MultiAssets { - let inv_dest = Config::LocationInverter::invert_location(&dest); - assets.prepend_location(&inv_dest); - assets.into_assets_iter().collect::>().into() + fn new(origin: Option) -> Self { + Self { + holding: Assets::new(), + origin, + trader: Config::Trader::new(), + error: None, + total_surplus: 0, + total_refunded: 0, + error_handler: Xcm(vec![]), + error_handler_weight: 0, + appendix: Xcm(vec![]), + appendix_weight: 0, + _config: PhantomData, + } } - /// Execute the XCM and return the portion of weight of `shallow_weight + deep_weight` that `message` did not use. - /// - /// NOTE: The amount returned must be less than `shallow_weight + deep_weight` of `message`. - fn do_execute_xcm( - origin: MultiLocation, - top_level: bool, - mut message: Xcm, - weight_credit: &mut Weight, - maybe_shallow_weight: Option, - trader: &mut Config::Trader, - num_recursions: u32, - ) -> Result { + /// Execute the XCM program fragment and report back the error and which instruction caused it, + /// or `Ok` if there was no error. + fn execute(&mut self, xcm: Xcm) -> Result<(), (u32, XcmError, u64)> { log::trace!( - target: "xcm::do_execute_xcm", - "origin: {:?}, top_level: {:?}, message: {:?}, weight_credit: {:?}, maybe_shallow_weight: {:?}, recursion: {:?}", - origin, - top_level, - message, - weight_credit, - maybe_shallow_weight, - num_recursions, + target: "xcm::execute", + "origin: {:?}, total_surplus/refunded: {:?}/{:?}, error_handler_weight: {:?}", + self.origin, + self.total_surplus, + self.total_refunded, + self.error_handler_weight, ); - - if num_recursions > MAX_RECURSION_LIMIT { - return Err(XcmError::RecursionLimitReached) + let mut result = Ok(()); + for (i, instr) in xcm.0.into_iter().enumerate() { + match &mut result { + r @ Ok(()) => + if let Err(e) = self.process_instruction(instr) { + *r = Err((i as u32, e, 0)); + }, + Err((_, _, ref mut w)) => + if let Ok(x) = Config::Weigher::instr_weight(&instr) { + w.saturating_accrue(x) + }, + } } + result + } - // This is the weight of everything that cannot be paid for. This basically means all computation - // except any XCM which is behind an Order::BuyExecution. - let shallow_weight = maybe_shallow_weight - .or_else(|| Config::Weigher::shallow(&mut message).ok()) - .ok_or(XcmError::WeightNotComputable)?; + /// Remove the registered error handler and return it. Do not refund its weight. + fn take_error_handler(&mut self) -> Xcm { + let mut r = Xcm::(vec![]); + sp_std::mem::swap(&mut self.error_handler, &mut r); + self.error_handler_weight = 0; + r + } - Config::Barrier::should_execute( - &origin, - top_level, - &message, - shallow_weight, - weight_credit, - ) - .map_err(|()| XcmError::Barrier)?; + /// Drop the registered error handler and refund its weight. + fn drop_error_handler(&mut self) { + self.error_handler = Xcm::(vec![]); + self.total_surplus = self.total_surplus.saturating_add(self.error_handler_weight); + self.error_handler_weight = 0; + } + + /// Remove the registered appendix and return it. + fn take_appendix(&mut self) -> Xcm { + let mut r = Xcm::(vec![]); + sp_std::mem::swap(&mut self.appendix, &mut r); + self.appendix_weight = 0; + r + } - // The surplus weight, defined as the amount by which `shallow_weight` plus all nested - // `shallow_weight` values (ensuring no double-counting and also known as `deep_weight`) is an - // over-estimate of the actual weight consumed. - let mut total_surplus: Weight = 0; + /// Refund any unused weight. + fn refund_surplus(&mut self) { + let current_surplus = self.total_surplus.saturating_sub(self.total_refunded); + if current_surplus > 0 { + self.total_refunded = self.total_refunded.saturating_add(current_surplus); + if let Some(w) = self.trader.refund_weight(current_surplus) { + self.holding.subsume(w); + } + } + } - let maybe_holding_effects = match (origin.clone(), message) { - (origin, Xcm::WithdrawAsset { assets, effects }) => { + /// Process a single XCM instruction, mutating the state of the XCM virtual machine. + fn process_instruction(&mut self, instr: Instruction) -> Result<(), XcmError> { + match instr { + WithdrawAsset(assets) => { // Take `assets` from the origin account (on-chain) and place in holding. - let mut holding = Assets::default(); - for asset in assets.inner() { - let withdrawn = Config::AssetTransactor::withdraw_asset(&asset, &origin)?; - holding.subsume_assets(withdrawn); + let origin = self.origin.as_ref().ok_or(XcmError::BadOrigin)?; + for asset in assets.drain().into_iter() { + Config::AssetTransactor::withdraw_asset(&asset, origin)?; + self.holding.subsume(asset); } - Some((holding, effects)) + Ok(()) }, - (origin, Xcm::ReserveAssetDeposited { assets, effects }) => { + ReserveAssetDeposited(assets) => { // check whether we trust origin to be our reserve location for this asset. - for asset in assets.inner() { - // We only trust the origin to send us assets that they identify as their - // sovereign assets. + let origin = self.origin.as_ref().ok_or(XcmError::BadOrigin)?; + for asset in assets.drain().into_iter() { + // Must ensure that we recognise the asset as being managed by the origin. ensure!( - Config::IsReserve::filter_asset_location(asset, &origin), + Config::IsReserve::filter_asset_location(&asset, origin), XcmError::UntrustedReserveLocation ); + self.holding.subsume(asset); } - Some((assets.into(), effects)) + Ok(()) }, - (origin, Xcm::TransferAsset { assets, beneficiary }) => { + TransferAsset { assets, beneficiary } => { // Take `assets` from the origin account (on-chain) and place into dest account. + let origin = self.origin.as_ref().ok_or(XcmError::BadOrigin)?; for asset in assets.inner() { - Config::AssetTransactor::beam_asset(&asset, &origin, &beneficiary)?; + Config::AssetTransactor::beam_asset(&asset, origin, &beneficiary)?; } - None + Ok(()) }, - (origin, Xcm::TransferReserveAsset { mut assets, dest, effects }) => { + TransferReserveAsset { mut assets, dest, xcm } => { + let origin = self.origin.as_ref().ok_or(XcmError::BadOrigin)?; // Take `assets` from the origin account (on-chain) and place into dest account. let inv_dest = Config::LocationInverter::invert_location(&dest); for asset in assets.inner() { - Config::AssetTransactor::beam_asset(asset, &origin, &dest)?; + Config::AssetTransactor::beam_asset(asset, origin, &dest)?; } assets.reanchor(&inv_dest)?; - Config::XcmSender::send_xcm(dest, Xcm::ReserveAssetDeposited { assets, effects })?; - None + let mut message = vec![ReserveAssetDeposited(assets), ClearOrigin]; + message.extend(xcm.0.into_iter()); + Config::XcmSender::send_xcm(dest, Xcm(message)).map_err(Into::into) }, - (origin, Xcm::ReceiveTeleportedAsset { assets, effects }) => { + ReceiveTeleportedAsset(assets) => { + let origin = self.origin.as_ref().ok_or(XcmError::BadOrigin)?; // check whether we trust origin to teleport this asset to us via config trait. for asset in assets.inner() { // We only trust the origin to send us assets that they identify as their // sovereign assets. ensure!( - Config::IsTeleporter::filter_asset_location(asset, &origin), + Config::IsTeleporter::filter_asset_location(asset, origin), XcmError::UntrustedTeleportLocation ); // We should check that the asset can actually be teleported in (for this to be in error, there @@ -203,13 +266,15 @@ impl XcmExecutor { // don't want to punish a possibly innocent chain/user). Config::AssetTransactor::can_check_in(&origin, asset)?; } - for asset in assets.inner() { - Config::AssetTransactor::check_in(&origin, asset); + for asset in assets.drain().into_iter() { + Config::AssetTransactor::check_in(origin, &asset); + self.holding.subsume(asset); } - Some((Assets::from(assets), effects)) + Ok(()) }, - (origin, Xcm::Transact { origin_type, require_weight_at_most, mut call }) => { + Transact { origin_type, require_weight_at_most, mut call } => { // We assume that the Relay-chain is allowed to use transact on this parachain. + let origin = self.origin.clone().ok_or(XcmError::BadOrigin)?; // TODO: #2841 #TRANSACTFILTER allow the trait to issue filters for the relay-chain let message_call = call.take_decoded().map_err(|_| XcmError::FailedToDecode)?; @@ -227,142 +292,130 @@ impl XcmExecutor { } .unwrap_or(weight); let surplus = weight.saturating_sub(actual_weight); - // Credit any surplus weight that we bought. This should be safe since it's work we - // didn't realise that we didn't have to do. - // It works because we assume that the `Config::Weigher` will always count the `call`'s - // `get_dispatch_info` weight into its `shallow` estimate. - *weight_credit = weight_credit.saturating_add(surplus); - // Do the same for the total surplus, which is reported to the caller and eventually makes its way - // back up the stack to be subtracted from the deep-weight. - total_surplus = total_surplus.saturating_add(surplus); - // Return the overestimated amount so we can adjust our expectations on how much this entire - // execution has taken. - None + // We assume that the `Config::Weigher` will counts the `require_weight_at_most` + // for the estimate of how much weight this instruction will take. Now that we know + // that it's less, we credit it. + // + // We make the adjustment for the total surplus, which is used eventually + // reported back to the caller and this ensures that they account for the total + // weight consumed correctly (potentially allowing them to do more operations in a + // block than they otherwise would). + self.total_surplus = self.total_surplus.saturating_add(surplus); + Ok(()) }, - (origin, Xcm::QueryResponse { query_id, response }) => { - Config::ResponseHandler::on_response(origin, query_id, response); - None + QueryResponse { query_id, response, max_weight } => { + let origin = self.origin.as_ref().ok_or(XcmError::BadOrigin)?; + Config::ResponseHandler::on_response(origin, query_id, response, max_weight); + Ok(()) }, - (origin, Xcm::RelayedFrom { who, message }) => { - let mut origin = origin; - origin.append_with(who).map_err(|_| XcmError::MultiLocationFull)?; - let surplus = Self::do_execute_xcm( - origin, - top_level, - *message, - weight_credit, - None, - trader, - num_recursions + 1, - )?; - total_surplus = total_surplus.saturating_add(surplus); - None + DescendOrigin(who) => self + .origin + .as_mut() + .ok_or(XcmError::BadOrigin)? + .append_with(who) + .map_err(|_| XcmError::MultiLocationFull), + ClearOrigin => { + self.origin = None; + Ok(()) }, - _ => Err(XcmError::UnhandledXcmMessage)?, // Unhandled XCM message. - }; - - if let Some((mut holding, effects)) = maybe_holding_effects { - for effect in effects.into_iter() { - total_surplus += Self::execute_orders( - &origin, - &mut holding, - effect, - trader, - num_recursions + 1, - )?; - } - } - - Ok(total_surplus) - } - - fn execute_orders( - origin: &MultiLocation, - holding: &mut Assets, - order: Order, - trader: &mut Config::Trader, - num_recursions: u32, - ) -> Result { - log::trace!( - target: "xcm::execute_orders", - "origin: {:?}, holding: {:?}, order: {:?}, recursion: {:?}", - origin, - holding, - order, - num_recursions, - ); - - if num_recursions > MAX_RECURSION_LIMIT { - return Err(XcmError::RecursionLimitReached) - } - - let mut total_surplus = 0; - match order { - Order::DepositAsset { assets, max_assets, beneficiary } => { - let deposited = holding.limited_saturating_take(assets, max_assets as usize); + ReportError { query_id, dest, max_response_weight: max_weight } => { + // Report the given result by sending a QueryResponse XCM to a previously given outcome + // destination if one was registered. + let response = Response::ExecutionResult(match self.error { + None => Ok(()), + Some(e) => Err(e), + }); + let message = QueryResponse { query_id, response, max_weight }; + Config::XcmSender::send_xcm(dest, Xcm(vec![message]))?; + Ok(()) + }, + DepositAsset { assets, max_assets, beneficiary } => { + let deposited = self.holding.limited_saturating_take(assets, max_assets as usize); for asset in deposited.into_assets_iter() { Config::AssetTransactor::deposit_asset(&asset, &beneficiary)?; } + Ok(()) }, - Order::DepositReserveAsset { assets, max_assets, dest, effects } => { - let deposited = holding.limited_saturating_take(assets, max_assets as usize); + DepositReserveAsset { assets, max_assets, dest, xcm } => { + let deposited = self.holding.limited_saturating_take(assets, max_assets as usize); for asset in deposited.assets_iter() { Config::AssetTransactor::deposit_asset(&asset, &dest)?; } let assets = Self::reanchored(deposited, &dest); - Config::XcmSender::send_xcm(dest, Xcm::ReserveAssetDeposited { assets, effects })?; + let mut message = vec![ReserveAssetDeposited(assets), ClearOrigin]; + message.extend(xcm.0.into_iter()); + Config::XcmSender::send_xcm(dest, Xcm(message)).map_err(Into::into) }, - Order::InitiateReserveWithdraw { assets, reserve, effects } => { - let assets = Self::reanchored(holding.saturating_take(assets), &reserve); - Config::XcmSender::send_xcm(reserve, Xcm::WithdrawAsset { assets, effects })?; + InitiateReserveWithdraw { assets, reserve, xcm } => { + let assets = Self::reanchored(self.holding.saturating_take(assets), &reserve); + let mut message = vec![WithdrawAsset(assets), ClearOrigin]; + message.extend(xcm.0.into_iter()); + Config::XcmSender::send_xcm(reserve, Xcm(message)).map_err(Into::into) }, - Order::InitiateTeleport { assets, dest, effects } => { + InitiateTeleport { assets, dest, xcm } => { // We must do this first in order to resolve wildcards. - let assets = holding.saturating_take(assets); + let assets = self.holding.saturating_take(assets); for asset in assets.assets_iter() { - Config::AssetTransactor::check_out(&origin, &asset); + Config::AssetTransactor::check_out(&dest, &asset); } let assets = Self::reanchored(assets, &dest); - Config::XcmSender::send_xcm(dest, Xcm::ReceiveTeleportedAsset { assets, effects })?; + let mut message = vec![ReceiveTeleportedAsset(assets), ClearOrigin]; + message.extend(xcm.0.into_iter()); + Config::XcmSender::send_xcm(dest, Xcm(message)).map_err(Into::into) }, - Order::QueryHolding { query_id, dest, assets } => { - let assets = Self::reanchored(holding.min(&assets), &dest); - Config::XcmSender::send_xcm( - dest, - Xcm::QueryResponse { query_id, response: Response::Assets(assets) }, - )?; + QueryHolding { query_id, dest, assets, max_response_weight } => { + let assets = Self::reanchored(self.holding.min(&assets), &dest); + let max_weight = max_response_weight; + let response = Response::Assets(assets); + let instruction = QueryResponse { query_id, response, max_weight }; + Config::XcmSender::send_xcm(dest, Xcm(vec![instruction])).map_err(Into::into) }, - Order::BuyExecution { fees, weight, debt, halt_on_error, instructions } => { - // pay for `weight` using up to `fees` of the holding register. - let purchasing_weight = - Weight::from(weight.checked_add(debt).ok_or(XcmError::Overflow)?); - let max_fee = - holding.try_take(fees.into()).map_err(|_| XcmError::NotHoldingFees)?; - let unspent = trader.buy_weight(purchasing_weight, max_fee)?; - holding.subsume_assets(unspent); - - let mut remaining_weight = weight; - for instruction in instructions.into_iter() { - match Self::do_execute_xcm( - origin.clone(), - false, - instruction, - &mut remaining_weight, - None, - trader, - num_recursions + 1, - ) { - Err(e) if halt_on_error => return Err(e), - Err(_) => {}, - Ok(surplus) => total_surplus += surplus, - } - } - if let Some(w) = trader.refund_weight(remaining_weight) { - holding.subsume(w); + BuyExecution { fees, weight_limit } => { + // There is no need to buy any weight is `weight_limit` is `Unlimited` since it + // would indicate that `AllowTopLevelPaidExecutionFrom` was unused for execution + // and thus there is some other reason why it has been determined that this XCM + // should be executed. + if let Some(weight) = Option::::from(weight_limit) { + // pay for `weight` using up to `fees` of the holding register. + let max_fee = + self.holding.try_take(fees.into()).map_err(|_| XcmError::NotHoldingFees)?; + let unspent = self.trader.buy_weight(weight, max_fee)?; + self.holding.subsume_assets(unspent); } + Ok(()) + }, + RefundSurplus => { + self.refund_surplus(); + Ok(()) }, - _ => return Err(XcmError::UnhandledEffect)?, + SetErrorHandler(mut handler) => { + let handler_weight = Config::Weigher::weight(&mut handler)?; + self.total_surplus = self.total_surplus.saturating_add(self.error_handler_weight); + self.error_handler = handler; + self.error_handler_weight = handler_weight; + Ok(()) + }, + SetAppendix(mut appendix) => { + let appendix_weight = Config::Weigher::weight(&mut appendix)?; + self.total_surplus = self.total_surplus.saturating_add(self.appendix_weight); + self.appendix = appendix; + self.appendix_weight = appendix_weight; + Ok(()) + }, + ClearError => { + self.error = None; + Ok(()) + }, + ExchangeAsset { .. } => Err(XcmError::Unimplemented), + HrmpNewChannelOpenRequest { .. } => Err(XcmError::Unimplemented), + HrmpChannelAccepted { .. } => Err(XcmError::Unimplemented), + HrmpChannelClosing { .. } => Err(XcmError::Unimplemented), } - Ok(total_surplus) + } + + fn reanchored(mut assets: Assets, dest: &MultiLocation) -> MultiAssets { + let inv_dest = Config::LocationInverter::invert_location(&dest); + assets.prepend_location(&inv_dest); + assets.into_assets_iter().collect::>().into() } } diff --git a/xcm/xcm-executor/src/traits/on_response.rs b/xcm/xcm-executor/src/traits/on_response.rs index 8af238e7cef2..158e448161e2 100644 --- a/xcm/xcm-executor/src/traits/on_response.rs +++ b/xcm/xcm-executor/src/traits/on_response.rs @@ -22,13 +22,23 @@ pub trait OnResponse { /// Returns `true` if we are expecting a response from `origin` for query `query_id`. fn expecting_response(origin: &MultiLocation, query_id: u64) -> bool; /// Handler for receiving a `response` from `origin` relating to `query_id`. - fn on_response(origin: MultiLocation, query_id: u64, response: Response) -> Weight; + fn on_response( + origin: &MultiLocation, + query_id: u64, + response: Response, + max_weight: Weight, + ) -> Weight; } impl OnResponse for () { fn expecting_response(_origin: &MultiLocation, _query_id: u64) -> bool { false } - fn on_response(_origin: MultiLocation, _query_id: u64, _response: Response) -> Weight { + fn on_response( + _origin: &MultiLocation, + _query_id: u64, + _response: Response, + _max_weight: Weight, + ) -> Weight { 0 } } diff --git a/xcm/xcm-executor/src/traits/should_execute.rs b/xcm/xcm-executor/src/traits/should_execute.rs index a6f7c0207b1a..53600779fc54 100644 --- a/xcm/xcm-executor/src/traits/should_execute.rs +++ b/xcm/xcm-executor/src/traits/should_execute.rs @@ -26,19 +26,18 @@ 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`. + /// - `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. - /// - `shallow_weight`: The weight of the non-negotiable execution of the message. This does not include any - /// embedded XCMs sat behind mechanisms like `BuyExecution` which would need to answer for their own weight. - /// - `weight_credit`: The pre-established amount of weight that the system has determined this message may utilize - /// in its execution. Typically non-zero only because of prior fee payment, but could in principle be due to other - /// factors. + /// - `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 + /// message may utilize in its execution. Typically non-zero only because of prior fee + /// payment, but could in principle be due to other factors. fn should_execute( - origin: &MultiLocation, + origin: &Option, top_level: bool, - message: &Xcm, - shallow_weight: Weight, + message: &mut Xcm, + max_weight: Weight, weight_credit: &mut Weight, ) -> Result<(), ()>; } @@ -46,25 +45,25 @@ pub trait ShouldExecute { #[impl_trait_for_tuples::impl_for_tuples(30)] impl ShouldExecute for Tuple { fn should_execute( - origin: &MultiLocation, + origin: &Option, top_level: bool, - message: &Xcm, - shallow_weight: Weight, + message: &mut Xcm, + max_weight: Weight, weight_credit: &mut Weight, ) -> Result<(), ()> { for_tuples!( #( - match Tuple::should_execute(origin, top_level, message, shallow_weight, weight_credit) { + match Tuple::should_execute(origin, top_level, message, max_weight, weight_credit) { Ok(()) => return Ok(()), _ => (), } )* ); log::trace!( target: "xcm::should_execute", - "did not pass barrier: origin: {:?}, top_level: {:?}, message: {:?}, shallow_weight: {:?}, weight_credit: {:?}", + "did not pass barrier: origin: {:?}, top_level: {:?}, message: {:?}, max_weight: {:?}, weight_credit: {:?}", origin, top_level, message, - shallow_weight, + max_weight, weight_credit, ); Err(()) diff --git a/xcm/xcm-executor/src/traits/transact_asset.rs b/xcm/xcm-executor/src/traits/transact_asset.rs index 80ee2de660c3..78e2180ed7a4 100644 --- a/xcm/xcm-executor/src/traits/transact_asset.rs +++ b/xcm/xcm-executor/src/traits/transact_asset.rs @@ -58,7 +58,7 @@ pub trait TransactAsset { /// /// When composed as a tuple, all type-items are called. It is up to the implementer that there exists no /// value for `_what` which can cause side-effects for more than one of the type-items. - fn check_out(_origin: &MultiLocation, _what: &MultiAsset) {} + fn check_out(_dest: &MultiLocation, _what: &MultiAsset) {} /// Deposit the `what` asset into the account of `who`. /// @@ -67,8 +67,8 @@ pub trait TransactAsset { Err(XcmError::Unimplemented) } - /// Withdraw the given asset from the consensus system. Return the actual asset(s) withdrawn. In - /// the case of `what` being a wildcard, this may be something more specific. + /// Withdraw the given asset from the consensus system. Return the actual asset(s) withdrawn, + /// which should always be equal to `_what`. /// /// Implementations should return `XcmError::FailedToTransactAsset` if withdraw failed. fn withdraw_asset(_what: &MultiAsset, _who: &MultiLocation) -> Result { diff --git a/xcm/xcm-executor/src/traits/weight.rs b/xcm/xcm-executor/src/traits/weight.rs index 8d962c88e5ec..5d3af4f4c141 100644 --- a/xcm/xcm-executor/src/traits/weight.rs +++ b/xcm/xcm-executor/src/traits/weight.rs @@ -17,37 +17,17 @@ use crate::Assets; use frame_support::weights::Weight; use sp_std::result::Result; -use xcm::latest::{Error, MultiAsset, MultiLocation, Xcm}; +use xcm::latest::prelude::*; /// Determine the weight of an XCM message. pub trait WeightBounds { - /// Return the minimum amount of weight that an attempted execution of this message would definitely + /// Return the maximum amount of weight that an attempted execution of this message could /// consume. - /// - /// This is useful to gauge how many fees should be paid up front to begin execution of the message. - /// It is not useful for determining whether execution should begin lest it result in surpassing weight - /// limits - in that case `deep` is the function to use. - fn shallow(message: &mut Xcm) -> Result; + fn weight(message: &mut Xcm) -> Result; - /// Return the deep amount of weight, over `shallow` that complete, successful and worst-case execution of - /// `message` would incur. - /// - /// This is perhaps overly pessimistic for determining how many fees should be paid for up-front since - /// fee payment (or any other way of offsetting the execution costs such as an voucher-style NFT) may - /// happen in stages throughout execution of the XCM. - /// - /// A reminder: if it is possible that `message` may have alternative means of successful completion - /// (perhaps a conditional path), then the *worst case* weight must be reported. - /// - /// This is guaranteed equal to the eventual sum of all `shallow` XCM messages that get executed through - /// any internal effects. Inner XCM messages may be executed by: - /// - `Order::BuyExecution` - fn deep(message: &mut Xcm) -> Result; - - /// Return the total weight for executing `message`. - fn weight(message: &mut Xcm) -> Result { - Self::shallow(message)?.checked_add(Self::deep(message)?).ok_or(()) - } + /// Return the maximum amount of weight that an attempted execution of this instruction could + /// consume. + fn instr_weight(instruction: &Instruction) -> Result; } /// A means of getting approximate weight consumption for a given destination message executor and a @@ -70,7 +50,7 @@ pub trait WeightTrader: Sized { /// Purchase execution weight credit in return for up to a given `fee`. If less of the fee is required /// then the surplus is returned. If the `fee` cannot be used to pay for the `weight`, then an error is /// returned. - fn buy_weight(&mut self, weight: Weight, payment: Assets) -> Result; + fn buy_weight(&mut self, weight: Weight, payment: Assets) -> Result; /// Attempt a refund of `weight` into some asset. The caller does not guarantee that the weight was /// purchased using `buy_weight`. @@ -87,7 +67,7 @@ impl WeightTrader for Tuple { for_tuples!( ( #( Tuple::new() ),* ) ) } - fn buy_weight(&mut self, weight: Weight, payment: Assets) -> Result { + fn buy_weight(&mut self, weight: Weight, payment: Assets) -> Result { let mut last_error = None; for_tuples!( #( match Tuple.buy_weight(weight, payment.clone()) { @@ -95,7 +75,7 @@ impl WeightTrader for Tuple { Err(e) => { last_error = Some(e) } } )* ); - let last_error = last_error.unwrap_or(Error::TooExpensive); + let last_error = last_error.unwrap_or(XcmError::TooExpensive); log::trace!(target: "xcm::buy_weight", "last_error: {:?}", last_error); Err(last_error) } diff --git a/xcm/xcm-simulator/example/src/lib.rs b/xcm/xcm-simulator/example/src/lib.rs index 964046d6f2f0..e93393e54d7f 100644 --- a/xcm/xcm-simulator/example/src/lib.rs +++ b/xcm/xcm-simulator/example/src/lib.rs @@ -105,19 +105,13 @@ mod tests { use super::*; use codec::Encode; - use frame_support::{assert_ok, weights::Weight}; + use frame_support::assert_ok; use xcm::latest::prelude::*; use xcm_simulator::TestExt; // Helper function for forming buy execution message - fn buy_execution(fees: impl Into, debt: Weight) -> Order { - Order::BuyExecution { - fees: fees.into(), - weight: 0, - debt, - halt_on_error: false, - instructions: vec![], - } + fn buy_execution(fees: impl Into) -> Instruction { + BuyExecution { fees: fees.into(), weight_limit: Unlimited } } #[test] @@ -131,11 +125,11 @@ mod tests { assert_ok!(RelayChainPalletXcm::send_xcm( Here, Parachain(1).into(), - Transact { + Xcm(vec![Transact { origin_type: OriginKind::SovereignAccount, require_weight_at_most: INITIAL_BALANCE as u64, call: remark.encode().into(), - }, + }]), )); }); @@ -158,11 +152,11 @@ mod tests { assert_ok!(ParachainPalletXcm::send_xcm( Here, Parent.into(), - Transact { + Xcm(vec![Transact { origin_type: OriginKind::SovereignAccount, require_weight_at_most: INITIAL_BALANCE as u64, call: remark.encode().into(), - }, + }]), )); }); @@ -185,11 +179,11 @@ mod tests { assert_ok!(ParachainPalletXcm::send_xcm( Here, MultiLocation::new(1, X1(Parachain(2))), - Transact { + Xcm(vec![Transact { origin_type: OriginKind::SovereignAccount, require_weight_at_most: INITIAL_BALANCE as u64, call: remark.encode().into(), - }, + }]), )); }); @@ -206,7 +200,6 @@ mod tests { MockNet::reset(); let withdraw_amount = 123; - let max_weight_for_execution = 3; Relay::execute_with(|| { assert_ok!(RelayChainPalletXcm::reserve_transfer_assets( @@ -215,7 +208,6 @@ mod tests { Box::new(X1(AccountId32 { network: Any, id: ALICE.into() }).into().into()), Box::new((Here, withdraw_amount).into()), 0, - max_weight_for_execution, )); assert_eq!( parachain::Balances::free_balance(¶_account_id(1)), @@ -241,20 +233,17 @@ mod tests { MockNet::reset(); let send_amount = 10; - let weight_for_execution = 3 * relay_chain::BaseXcmWeight::get(); ParaA::execute_with(|| { - let message = WithdrawAsset { - assets: (Here, send_amount).into(), - effects: vec![ - buy_execution((Here, send_amount), weight_for_execution), - Order::DepositAsset { - assets: All.into(), - max_assets: 1, - beneficiary: Parachain(2).into(), - }, - ], - }; + let message = Xcm(vec![ + WithdrawAsset((Here, send_amount).into()), + buy_execution((Here, send_amount)), + DepositAsset { + assets: All.into(), + max_assets: 1, + beneficiary: Parachain(2).into(), + }, + ]); // Send withdraw and deposit assert_ok!(ParachainPalletXcm::send_xcm(Here, Parent.into(), message.clone())); }); @@ -278,27 +267,25 @@ mod tests { MockNet::reset(); let send_amount = 10; - let weight_for_execution = 3 * relay_chain::BaseXcmWeight::get(); let query_id_set = 1234; // Send a message which fully succeeds on the relay chain ParaA::execute_with(|| { - let message = WithdrawAsset { - assets: (Here, send_amount).into(), - effects: vec![ - buy_execution((Here, send_amount), weight_for_execution), - Order::DepositAsset { - assets: All.into(), - max_assets: 1, - beneficiary: Parachain(2).into(), - }, - Order::QueryHolding { - query_id: query_id_set, - dest: Parachain(1).into(), - assets: All.into(), - }, - ], - }; + let message = Xcm(vec![ + WithdrawAsset((Here, send_amount).into()), + buy_execution((Here, send_amount)), + DepositAsset { + assets: All.into(), + max_assets: 1, + beneficiary: Parachain(2).into(), + }, + QueryHolding { + query_id: query_id_set, + dest: Parachain(1).into(), + assets: All.into(), + max_response_weight: 1_000_000_000, + }, + ]); // Send withdraw and deposit with query holding assert_ok!(ParachainPalletXcm::send_xcm(Here, Parent.into(), message.clone(),)); }); @@ -318,10 +305,11 @@ mod tests { ParaA::execute_with(|| { assert_eq!( parachain::MsgQueue::received_dmp(), - vec![QueryResponse { + vec![Xcm(vec![QueryResponse { query_id: query_id_set, - response: Response::Assets(MultiAssets::new()) - }] + response: Response::Assets(MultiAssets::new()), + max_weight: 1_000_000_000, + }])], ); }); } diff --git a/xcm/xcm-simulator/example/src/parachain.rs b/xcm/xcm-simulator/example/src/parachain.rs index 4b08b45d4dc4..8f714a30cb52 100644 --- a/xcm/xcm-simulator/example/src/parachain.rs +++ b/xcm/xcm-simulator/example/src/parachain.rs @@ -121,6 +121,7 @@ pub type XcmOriginToCallOrigin = ( parameter_types! { pub const UnitWeightCost: Weight = 1; pub KsmPerSecond: (AssetId, u128) = (Concrete(Parent.into()), 1); + pub const MaxInstructions: u32 = 100; } pub type LocalAssetTransactor = @@ -139,7 +140,7 @@ impl Config for XcmConfig { type IsTeleporter = (); type LocationInverter = LocationInverter; type Barrier = Barrier; - type Weigher = FixedWeightBounds; + type Weigher = FixedWeightBounds; type Trader = FixedRateOfFungible; type ResponseHandler = (); } @@ -298,8 +299,10 @@ impl pallet_xcm::Config for Runtime { type XcmExecutor = XcmExecutor; type XcmTeleportFilter = Nothing; type XcmReserveTransferFilter = Everything; - type Weigher = FixedWeightBounds; + type Weigher = FixedWeightBounds; type LocationInverter = LocationInverter; + type Origin = Origin; + type Call = Call; } 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 a5816447e6f7..fa2218786d2c 100644 --- a/xcm/xcm-simulator/example/src/relay_chain.rs +++ b/xcm/xcm-simulator/example/src/relay_chain.rs @@ -114,6 +114,7 @@ type LocalOriginConverter = ( parameter_types! { pub const BaseXcmWeight: Weight = 1_000; pub KsmPerSecond: (AssetId, u128) = (Concrete(KsmLocation::get()), 1); + pub const MaxInstructions: u32 = 100; } pub type XcmRouter = super::RelayChainXcmRouter; @@ -129,7 +130,7 @@ impl Config for XcmConfig { type IsTeleporter = (); type LocationInverter = LocationInverter; type Barrier = Barrier; - type Weigher = FixedWeightBounds; + type Weigher = FixedWeightBounds; type Trader = FixedRateOfFungible; type ResponseHandler = (); } @@ -146,8 +147,10 @@ impl pallet_xcm::Config for Runtime { type XcmExecutor = XcmExecutor; type XcmTeleportFilter = Everything; type XcmReserveTransferFilter = Everything; - type Weigher = FixedWeightBounds; + type Weigher = FixedWeightBounds; type LocationInverter = LocationInverter; + type Origin = Origin; + type Call = Call; } parameter_types! { @@ -175,6 +178,6 @@ construct_runtime!( Balances: pallet_balances::{Pallet, Call, Storage, Config, Event}, ParasOrigin: origin::{Pallet, Origin}, ParasUmp: ump::{Pallet, Call, Storage, Event}, - XcmPallet: pallet_xcm::{Pallet, Call, Storage, Event}, + XcmPallet: pallet_xcm::{Pallet, Call, Storage, Event, Origin}, } ); diff --git a/xcm/xcm-simulator/src/lib.rs b/xcm/xcm-simulator/src/lib.rs index 25cd15cc9e49..f932fc50deb8 100644 --- a/xcm/xcm-simulator/src/lib.rs +++ b/xcm/xcm-simulator/src/lib.rs @@ -260,7 +260,7 @@ macro_rules! decl_test_network { }, )* _ => { - return Err($crate::XcmError::CannotReachDestination(destination, message)); + return Err($crate::XcmError::Unroutable); } } } @@ -285,7 +285,7 @@ macro_rules! decl_test_network { ); }, )* - _ => return Err($crate::XcmError::SendFailed("Only sends to children parachain.")), + _ => return Err($crate::XcmError::Transport("Only sends to children parachain.")), } } @@ -296,7 +296,7 @@ macro_rules! decl_test_network { pub struct ParachainXcmRouter($crate::PhantomData); impl> $crate::SendXcm for ParachainXcmRouter { - fn send_xcm(destination: $crate::MultiLocation, message: $crate::Xcm<()>) -> $crate::XcmResult { + fn send_xcm(destination: $crate::MultiLocation, message: $crate::Xcm<()>) -> $crate::SendResult { use $crate::{UmpSink, XcmpMessageHandlerT}; match destination.interior() { @@ -312,7 +312,7 @@ macro_rules! decl_test_network { Ok(()) }, )* - _ => Err($crate::XcmError::CannotReachDestination(destination, message)), + _ => Err($crate::SendError::CannotReachDestination(destination, message)), } } } @@ -320,7 +320,7 @@ macro_rules! decl_test_network { /// XCM router for relay chain. pub struct RelayChainXcmRouter; impl $crate::SendXcm for RelayChainXcmRouter { - fn send_xcm(destination: $crate::MultiLocation, message: $crate::Xcm<()>) -> $crate::XcmResult { + fn send_xcm(destination: $crate::MultiLocation, message: $crate::Xcm<()>) -> $crate::SendResult { use $crate::DmpMessageHandlerT; match destination.interior() { @@ -331,7 +331,7 @@ macro_rules! decl_test_network { Ok(()) }, )* - _ => Err($crate::XcmError::SendFailed("Only sends to children parachain.")), + _ => Err($crate::SendError::Unroutable), } } }