diff --git a/polkadot/xcm/src/lib.rs b/polkadot/xcm/src/lib.rs index 678a825b66..a49806e16b 100644 --- a/polkadot/xcm/src/lib.rs +++ b/polkadot/xcm/src/lib.rs @@ -23,6 +23,7 @@ #![no_std] extern crate alloc; +use core::ops::ControlFlow; use derivative::Derivative; use parity_scale_codec::{Decode, Encode, Error as CodecError, Input, MaxEncodedLen}; use scale_info::TypeInfo; @@ -47,6 +48,89 @@ pub const MAX_XCM_DECODE_DEPTH: u32 = 8; /// A version of XCM. pub type Version = u32; +/// Creates an instruction matcher from an XCM. Since XCM versions differ, we need to make a trait +/// here to unify the interfaces among them. +pub trait CreateMatcher { + /// The concrete matcher type. + type Matcher; + + /// Method that creates and returns the matcher type from `Self`. + fn matcher(self) -> Self::Matcher; +} + +/// API that allows to pattern-match against anything that is contained within an XCM. +/// +/// The intended usage of the matcher API is to enable the ability to chain successive methods of +/// this trait together, along with the ? operator for the purpose of facilitating the writing, +/// maintenance and auditability of XCM barriers. +/// +/// Example: +/// ```rust +/// use xcm::{ +/// v3::{Instruction, Matcher}, +/// CreateMatcher, MatchXcm, +/// }; +/// +/// let mut msg = [Instruction::<()>::ClearOrigin]; +/// let res = msg +/// .matcher() +/// .assert_remaining_insts(1)? +/// .match_next_inst(|inst| match inst { +/// Instruction::<()>::ClearOrigin => Ok(()), +/// _ => Err(()), +/// }); +/// assert!(res.is_ok()); +/// +/// Ok::<(), ()>(()) +/// ``` +pub trait MatchXcm { + /// The concrete instruction type. Necessary to specify as it changes between XCM versions. + type Inst; + /// The `MultiLocation` type. Necessary to specify as it changes between XCM versions. + type Loc; + /// The error type to throw when errors happen during matching. + type Error; + + /// Returns success if the number of instructions that still have not been iterated over + /// equals `n`, otherwise returns an error. + fn assert_remaining_insts(self, n: usize) -> Result + where + Self: Sized; + + /// Accepts a closure `f` that contains an argument signifying the next instruction to be + /// iterated over. The closure can then be used to check whether the instruction matches a + /// given condition, and can also be used to mutate the fields of an instruction. + /// + /// The closure `f` returns success when the instruction passes the condition, otherwise it + /// returns an error, which will ultimately be returned by this function. + fn match_next_inst(self, f: F) -> Result + where + Self: Sized, + F: FnMut(&mut Self::Inst) -> Result<(), Self::Error>; + + /// Attempts to continuously iterate through the instructions while applying `f` to each of + /// them, until either the last instruction or `cond` returns false. + /// + /// If `f` returns an error, then iteration halts and the function returns that error. + /// Otherwise, `f` returns a `ControlFlow` which signifies whether the iteration breaks or + /// continues. + fn match_next_inst_while(self, cond: C, f: F) -> Result + where + Self: Sized, + C: Fn(&Self::Inst) -> bool, + F: FnMut(&mut Self::Inst) -> Result, Self::Error>; + + /// Iterate instructions forward until `cond` returns false. When there are no more instructions + /// to be read, an error is returned. + fn skip_inst_while(self, cond: C) -> Result + where + Self: Sized, + C: Fn(&Self::Inst) -> bool, + { + Self::match_next_inst_while(self, cond, |_| Ok(ControlFlow::Continue(()))) + } +} + #[derive(Clone, Eq, PartialEq, Debug)] pub enum Unsupported {} impl Encode for Unsupported {} diff --git a/polkadot/xcm/src/v3/matcher.rs b/polkadot/xcm/src/v3/matcher.rs new file mode 100644 index 0000000000..de390338e3 --- /dev/null +++ b/polkadot/xcm/src/v3/matcher.rs @@ -0,0 +1,109 @@ +// Copyright 2023 Parity Technologies (UK) Ltd. +// This file is part of Polkadot. + +// 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 Polkadot. If not, see . + +//! XCM matcher API, used primarily for writing barrier conditions. + +use super::{Instruction, MultiLocation}; +use crate::{CreateMatcher, MatchXcm}; +use core::ops::ControlFlow; + +impl<'a, Call> CreateMatcher for &'a mut [Instruction] { + type Matcher = Matcher<'a, Call>; + + fn matcher(self) -> Self::Matcher { + let total_inst = self.len(); + + Matcher { xcm: self, current_idx: 0, total_inst } + } +} + +/// Struct created from calling `fn matcher()` on a mutable slice of `Instruction`s. +/// +/// Implements `MatchXcm` to allow an iterator-like API to match against each `Instruction` +/// contained within the slice, which facilitates the building of XCM barriers. +pub struct Matcher<'a, Call> { + pub(crate) xcm: &'a mut [Instruction], + pub(crate) current_idx: usize, + pub(crate) total_inst: usize, +} + +impl<'a, Call> MatchXcm for Matcher<'a, Call> { + type Error = (); + type Inst = Instruction; + type Loc = MultiLocation; + + fn assert_remaining_insts(self, n: usize) -> Result + where + Self: Sized, + { + if self.total_inst - self.current_idx != n { + return Err(()) + } + + Ok(self) + } + + fn match_next_inst(mut self, mut f: F) -> Result + where + Self: Sized, + F: FnMut(&mut Self::Inst) -> Result<(), Self::Error>, + { + if self.current_idx < self.total_inst { + f(&mut self.xcm[self.current_idx])?; + self.current_idx += 1; + Ok(self) + } else { + Err(()) + } + } + + fn match_next_inst_while(mut self, cond: C, mut f: F) -> Result + where + Self: Sized, + C: Fn(&Self::Inst) -> bool, + F: FnMut(&mut Self::Inst) -> Result, Self::Error>, + { + if self.current_idx >= self.total_inst { + return Err(()) + } + + while self.current_idx < self.total_inst && cond(&self.xcm[self.current_idx]) { + if let ControlFlow::Break(()) = f(&mut self.xcm[self.current_idx])? { + break + } + self.current_idx += 1; + } + + Ok(self) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::v3::prelude::*; + use alloc::{vec, vec::Vec}; + + #[test] + fn match_next_inst_while_works() { + let mut xcm: Vec> = vec![ClearOrigin]; + + let _ = xcm + .matcher() + .match_next_inst_while(|_| true, |_| Ok(ControlFlow::Continue(()))) + .unwrap(); + } +} diff --git a/polkadot/xcm/src/v3/mod.rs b/polkadot/xcm/src/v3/mod.rs index 0fed6defb9..45a960f28f 100644 --- a/polkadot/xcm/src/v3/mod.rs +++ b/polkadot/xcm/src/v3/mod.rs @@ -1,5 +1,5 @@ // Copyright 2020 Parity Technologies (UK) Ltd. -// This file is part of Cumulus. +// This file is part of Polkadot. // Substrate is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -12,7 +12,7 @@ // 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 . +// along with Polkadot. If not, see . //! Version 3 of the Cross-Consensus Message format data structures. @@ -34,12 +34,14 @@ use scale_info::TypeInfo; mod junction; pub(crate) mod junctions; +mod matcher; mod multiasset; mod multilocation; mod traits; pub use junction::{BodyId, BodyPart, Junction, NetworkId}; pub use junctions::Junctions; +pub use matcher::Matcher; pub use multiasset::{ AssetId, AssetInstance, Fungibility, MultiAsset, MultiAssetFilter, MultiAssets, WildFungibility, WildMultiAsset, diff --git a/polkadot/xcm/src/v3/traits.rs b/polkadot/xcm/src/v3/traits.rs index 6ccf01ad06..74ca60cd66 100644 --- a/polkadot/xcm/src/v3/traits.rs +++ b/polkadot/xcm/src/v3/traits.rs @@ -1,5 +1,5 @@ // Copyright 2020 Parity Technologies (UK) Ltd. -// This file is part of Cumulus. +// This file is part of Polkadot. // Substrate is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -12,7 +12,7 @@ // 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 . +// along with Polkadot. If not, see . //! Cross-Consensus Message format data structures. diff --git a/polkadot/xcm/xcm-builder/src/barriers.rs b/polkadot/xcm/xcm-builder/src/barriers.rs index 3ac275b63e..02de36caeb 100644 --- a/polkadot/xcm/xcm-builder/src/barriers.rs +++ b/polkadot/xcm/xcm-builder/src/barriers.rs @@ -21,13 +21,16 @@ use frame_support::{ traits::{Contains, Get}, }; use polkadot_parachain::primitives::IsSystem; -use sp_std::{marker::PhantomData, result::Result}; -use xcm::latest::{ - Instruction::{self, *}, - InteriorMultiLocation, Junction, Junctions, - Junctions::X1, - MultiLocation, Weight, - WeightLimit::*, +use sp_std::{cell::Cell, marker::PhantomData, ops::ControlFlow, result::Result}; +use xcm::{ + latest::{ + Instruction::{self, *}, + InteriorMultiLocation, Junction, Junctions, + Junctions::X1, + MultiLocation, Weight, + WeightLimit::*, + }, + CreateMatcher, MatchXcm, }; use xcm_executor::traits::{OnResponse, ShouldExecute}; @@ -77,32 +80,31 @@ impl> ShouldExecute for AllowTopLevelPaidExecutionFro // We will read up to 5 instructions. This allows up to 3 `ClearOrigin` instructions. We // allow for more than one since anything beyond the first is a no-op and it's conceivable // that composition of operations might result in more than one being appended. - let mut iter = instructions.iter_mut().take(5); - let i = iter.next().ok_or(())?; - match i { - ReceiveTeleportedAsset(..) | - WithdrawAsset(..) | - ReserveAssetDeposited(..) | - ClaimAsset { .. } => (), - _ => 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.all_gte(max_weight) => - { - *weight = max_weight; - Ok(()) - }, - BuyExecution { ref mut weight_limit, .. } if weight_limit == &Unlimited => { - *weight_limit = Limited(max_weight); - Ok(()) - }, - _ => Err(()), - } + let end = instructions.len().min(5); + instructions[..end] + .matcher() + .match_next_inst(|inst| match inst { + ReceiveTeleportedAsset(..) | + WithdrawAsset(..) | + ReserveAssetDeposited(..) | + ClaimAsset { .. } => Ok(()), + _ => Err(()), + })? + .skip_inst_while(|inst| matches!(inst, ClearOrigin))? + .match_next_inst(|inst| match inst { + BuyExecution { weight_limit: Limited(ref mut weight), .. } + if weight.all_gte(max_weight) => + { + *weight = max_weight; + Ok(()) + }, + BuyExecution { ref mut weight_limit, .. } if weight_limit == &Unlimited => { + *weight_limit = Limited(max_weight); + Ok(()) + }, + _ => Err(()), + })?; + Ok(()) } } @@ -172,29 +174,34 @@ impl< origin, instructions, max_weight, weight_credit, ); let mut actual_origin = *origin; - let mut skipped = 0; + let skipped = Cell::new(0usize); // NOTE: We do not check the validity of `UniversalOrigin` here, meaning that a malicious // origin could place a `UniversalOrigin` in order to spoof some location which gets free // execution. This technical could get it past the barrier condition, but the execution // would instantly fail since the first instruction would cause an error with the // invalid UniversalOrigin. - while skipped < MaxPrefixes::get() as usize { - match instructions.get(skipped) { - Some(UniversalOrigin(new_global)) => { - // Note the origin is *relative to local consensus*! So we need to escape local - // consensus with the `parents` before diving in into the `universal_location`. - actual_origin = X1(*new_global).relative_to(&LocalUniversal::get()); - }, - Some(DescendOrigin(j)) => { - actual_origin.append_with(*j).map_err(|_| ())?; - }, - _ => break, - } - skipped += 1; - } + instructions.matcher().match_next_inst_while( + |_| skipped.get() < MaxPrefixes::get() as usize, + |inst| { + match inst { + UniversalOrigin(new_global) => { + // Note the origin is *relative to local consensus*! So we need to escape + // local consensus with the `parents` before diving in into the + // `universal_location`. + actual_origin = X1(*new_global).relative_to(&LocalUniversal::get()); + }, + DescendOrigin(j) => { + let Ok(_) = actual_origin.append_with(*j) else { return Err(()) }; + }, + _ => return Ok(ControlFlow::Break(())), + }; + skipped.set(skipped.get() + 1); + Ok(ControlFlow::Continue(())) + }, + )?; InnerBarrier::should_execute( &actual_origin, - &mut instructions[skipped..], + &mut instructions[skipped.get()..], max_weight, weight_credit, ) @@ -241,12 +248,12 @@ impl> ShouldExecute for AllowExplicitUnpaidExecutionF origin, instructions, max_weight, _weight_credit, ); ensure!(T::contains(origin), ()); - match instructions.first() { - Some(UnpaidExecution { weight_limit: Limited(m), .. }) if m.all_gte(max_weight) => - Ok(()), - Some(UnpaidExecution { weight_limit: Unlimited, .. }) => Ok(()), + instructions.matcher().match_next_inst(|inst| match inst { + UnpaidExecution { weight_limit: Limited(m), .. } if m.all_gte(max_weight) => Ok(()), + UnpaidExecution { weight_limit: Unlimited, .. } => Ok(()), _ => Err(()), - } + })?; + Ok(()) } } @@ -276,13 +283,16 @@ impl ShouldExecute for AllowKnownQueryResponses - Ok(()), - _ => Err(()), - } + instructions + .matcher() + .assert_remaining_insts(1)? + .match_next_inst(|inst| match inst { + QueryResponse { query_id, querier, .. } + if ResponseHandler::expecting_response(origin, *query_id, querier.as_ref()) => + Ok(()), + _ => Err(()), + })?; + Ok(()) } } @@ -302,9 +312,13 @@ impl> ShouldExecute for AllowSubscriptionsFrom { origin, instructions, _max_weight, _weight_credit, ); ensure!(T::contains(origin), ()); - match instructions { - &mut [SubscribeVersion { .. } | UnsubscribeVersion] => Ok(()), - _ => Err(()), - } + instructions + .matcher() + .assert_remaining_insts(1)? + .match_next_inst(|inst| match inst { + SubscribeVersion { .. } | UnsubscribeVersion => Ok(()), + _ => Err(()), + })?; + Ok(()) } }