Introduce XCM matcher for writing barriers (#6756)

* Introduce XCM matcher for writing barriers

* Fix compilation

* cargo fmt

* Add more doc comments

* Add mod doc comment

* More doc comments

* Add tests and fix logic

* Remove redundant syntax

* Add more doc comments

* Add more doc comments

* Add more doc comments
This commit is contained in:
Keith Yeung
2023-03-03 21:37:56 -08:00
committed by GitHub
parent 7d9a0b7cc3
commit d29951e2f8
5 changed files with 278 additions and 69 deletions
+84
View File
@@ -23,6 +23,7 @@
#![no_std] #![no_std]
extern crate alloc; extern crate alloc;
use core::ops::ControlFlow;
use derivative::Derivative; use derivative::Derivative;
use parity_scale_codec::{Decode, Encode, Error as CodecError, Input, MaxEncodedLen}; use parity_scale_codec::{Decode, Encode, Error as CodecError, Input, MaxEncodedLen};
use scale_info::TypeInfo; use scale_info::TypeInfo;
@@ -47,6 +48,89 @@ pub const MAX_XCM_DECODE_DEPTH: u32 = 8;
/// A version of XCM. /// A version of XCM.
pub type Version = u32; 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<Self, Self::Error>
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<F>(self, f: F) -> Result<Self, Self::Error>
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<C, F>(self, cond: C, f: F) -> Result<Self, Self::Error>
where
Self: Sized,
C: Fn(&Self::Inst) -> bool,
F: FnMut(&mut Self::Inst) -> Result<ControlFlow<()>, 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<C>(self, cond: C) -> Result<Self, Self::Error>
where
Self: Sized,
C: Fn(&Self::Inst) -> bool,
{
Self::match_next_inst_while(self, cond, |_| Ok(ControlFlow::Continue(())))
}
}
#[derive(Clone, Eq, PartialEq, Debug)] #[derive(Clone, Eq, PartialEq, Debug)]
pub enum Unsupported {} pub enum Unsupported {}
impl Encode for Unsupported {} impl Encode for Unsupported {}
+109
View File
@@ -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 <http://www.gnu.org/licenses/>.
//! 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<Call>] {
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<Call>],
pub(crate) current_idx: usize,
pub(crate) total_inst: usize,
}
impl<'a, Call> MatchXcm for Matcher<'a, Call> {
type Error = ();
type Inst = Instruction<Call>;
type Loc = MultiLocation;
fn assert_remaining_insts(self, n: usize) -> Result<Self, Self::Error>
where
Self: Sized,
{
if self.total_inst - self.current_idx != n {
return Err(())
}
Ok(self)
}
fn match_next_inst<F>(mut self, mut f: F) -> Result<Self, Self::Error>
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<C, F>(mut self, cond: C, mut f: F) -> Result<Self, Self::Error>
where
Self: Sized,
C: Fn(&Self::Inst) -> bool,
F: FnMut(&mut Self::Inst) -> Result<ControlFlow<()>, 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<Instruction<()>> = vec![ClearOrigin];
let _ = xcm
.matcher()
.match_next_inst_while(|_| true, |_| Ok(ControlFlow::Continue(())))
.unwrap();
}
}
+4 -2
View File
@@ -1,5 +1,5 @@
// Copyright 2020 Parity Technologies (UK) Ltd. // 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 // Substrate is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by // it under the terms of the GNU General Public License as published by
@@ -12,7 +12,7 @@
// GNU General Public License for more details. // GNU General Public License for more details.
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Cumulus. If not, see <http://www.gnu.org/licenses/>. // along with Polkadot. If not, see <http://www.gnu.org/licenses/>.
//! Version 3 of the Cross-Consensus Message format data structures. //! Version 3 of the Cross-Consensus Message format data structures.
@@ -34,12 +34,14 @@ use scale_info::TypeInfo;
mod junction; mod junction;
pub(crate) mod junctions; pub(crate) mod junctions;
mod matcher;
mod multiasset; mod multiasset;
mod multilocation; mod multilocation;
mod traits; mod traits;
pub use junction::{BodyId, BodyPart, Junction, NetworkId}; pub use junction::{BodyId, BodyPart, Junction, NetworkId};
pub use junctions::Junctions; pub use junctions::Junctions;
pub use matcher::Matcher;
pub use multiasset::{ pub use multiasset::{
AssetId, AssetInstance, Fungibility, MultiAsset, MultiAssetFilter, MultiAssets, AssetId, AssetInstance, Fungibility, MultiAsset, MultiAssetFilter, MultiAssets,
WildFungibility, WildMultiAsset, WildFungibility, WildMultiAsset,
+2 -2
View File
@@ -1,5 +1,5 @@
// Copyright 2020 Parity Technologies (UK) Ltd. // 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 // Substrate is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by // it under the terms of the GNU General Public License as published by
@@ -12,7 +12,7 @@
// GNU General Public License for more details. // GNU General Public License for more details.
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Cumulus. If not, see <http://www.gnu.org/licenses/>. // along with Polkadot. If not, see <http://www.gnu.org/licenses/>.
//! Cross-Consensus Message format data structures. //! Cross-Consensus Message format data structures.
+79 -65
View File
@@ -21,13 +21,16 @@ use frame_support::{
traits::{Contains, Get}, traits::{Contains, Get},
}; };
use polkadot_parachain::primitives::IsSystem; use polkadot_parachain::primitives::IsSystem;
use sp_std::{marker::PhantomData, result::Result}; use sp_std::{cell::Cell, marker::PhantomData, ops::ControlFlow, result::Result};
use xcm::latest::{ use xcm::{
Instruction::{self, *}, latest::{
InteriorMultiLocation, Junction, Junctions, Instruction::{self, *},
Junctions::X1, InteriorMultiLocation, Junction, Junctions,
MultiLocation, Weight, Junctions::X1,
WeightLimit::*, MultiLocation, Weight,
WeightLimit::*,
},
CreateMatcher, MatchXcm,
}; };
use xcm_executor::traits::{OnResponse, ShouldExecute}; use xcm_executor::traits::{OnResponse, ShouldExecute};
@@ -77,32 +80,31 @@ impl<T: Contains<MultiLocation>> ShouldExecute for AllowTopLevelPaidExecutionFro
// We will read up to 5 instructions. This allows up to 3 `ClearOrigin` instructions. We // 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 // 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. // that composition of operations might result in more than one being appended.
let mut iter = instructions.iter_mut().take(5); let end = instructions.len().min(5);
let i = iter.next().ok_or(())?; instructions[..end]
match i { .matcher()
ReceiveTeleportedAsset(..) | .match_next_inst(|inst| match inst {
WithdrawAsset(..) | ReceiveTeleportedAsset(..) |
ReserveAssetDeposited(..) | WithdrawAsset(..) |
ClaimAsset { .. } => (), ReserveAssetDeposited(..) |
_ => return Err(()), ClaimAsset { .. } => Ok(()),
} _ => Err(()),
let mut i = iter.next().ok_or(())?; })?
while let ClearOrigin = i { .skip_inst_while(|inst| matches!(inst, ClearOrigin))?
i = iter.next().ok_or(())?; .match_next_inst(|inst| match inst {
} BuyExecution { weight_limit: Limited(ref mut weight), .. }
match i { if weight.all_gte(max_weight) =>
BuyExecution { weight_limit: Limited(ref mut weight), .. } {
if weight.all_gte(max_weight) => *weight = max_weight;
{ Ok(())
*weight = max_weight; },
Ok(()) BuyExecution { ref mut weight_limit, .. } if weight_limit == &Unlimited => {
}, *weight_limit = Limited(max_weight);
BuyExecution { ref mut weight_limit, .. } if weight_limit == &Unlimited => { Ok(())
*weight_limit = Limited(max_weight); },
Ok(()) _ => Err(()),
}, })?;
_ => Err(()), Ok(())
}
} }
} }
@@ -172,29 +174,34 @@ impl<
origin, instructions, max_weight, weight_credit, origin, instructions, max_weight, weight_credit,
); );
let mut actual_origin = *origin; 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 // 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 // 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 // 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 // would instantly fail since the first instruction would cause an error with the
// invalid UniversalOrigin. // invalid UniversalOrigin.
while skipped < MaxPrefixes::get() as usize { instructions.matcher().match_next_inst_while(
match instructions.get(skipped) { |_| skipped.get() < MaxPrefixes::get() as usize,
Some(UniversalOrigin(new_global)) => { |inst| {
// Note the origin is *relative to local consensus*! So we need to escape local match inst {
// consensus with the `parents` before diving in into the `universal_location`. UniversalOrigin(new_global) => {
actual_origin = X1(*new_global).relative_to(&LocalUniversal::get()); // Note the origin is *relative to local consensus*! So we need to escape
}, // local consensus with the `parents` before diving in into the
Some(DescendOrigin(j)) => { // `universal_location`.
actual_origin.append_with(*j).map_err(|_| ())?; actual_origin = X1(*new_global).relative_to(&LocalUniversal::get());
}, },
_ => break, DescendOrigin(j) => {
} let Ok(_) = actual_origin.append_with(*j) else { return Err(()) };
skipped += 1; },
} _ => return Ok(ControlFlow::Break(())),
};
skipped.set(skipped.get() + 1);
Ok(ControlFlow::Continue(()))
},
)?;
InnerBarrier::should_execute( InnerBarrier::should_execute(
&actual_origin, &actual_origin,
&mut instructions[skipped..], &mut instructions[skipped.get()..],
max_weight, max_weight,
weight_credit, weight_credit,
) )
@@ -241,12 +248,12 @@ impl<T: Contains<MultiLocation>> ShouldExecute for AllowExplicitUnpaidExecutionF
origin, instructions, max_weight, _weight_credit, origin, instructions, max_weight, _weight_credit,
); );
ensure!(T::contains(origin), ()); ensure!(T::contains(origin), ());
match instructions.first() { instructions.matcher().match_next_inst(|inst| match inst {
Some(UnpaidExecution { weight_limit: Limited(m), .. }) if m.all_gte(max_weight) => UnpaidExecution { weight_limit: Limited(m), .. } if m.all_gte(max_weight) => Ok(()),
Ok(()), UnpaidExecution { weight_limit: Unlimited, .. } => Ok(()),
Some(UnpaidExecution { weight_limit: Unlimited, .. }) => Ok(()),
_ => Err(()), _ => Err(()),
} })?;
Ok(())
} }
} }
@@ -276,13 +283,16 @@ impl<ResponseHandler: OnResponse> ShouldExecute for AllowKnownQueryResponses<Res
"AllowKnownQueryResponses origin: {:?}, instructions: {:?}, max_weight: {:?}, weight_credit: {:?}", "AllowKnownQueryResponses origin: {:?}, instructions: {:?}, max_weight: {:?}, weight_credit: {:?}",
origin, instructions, _max_weight, _weight_credit, origin, instructions, _max_weight, _weight_credit,
); );
ensure!(instructions.len() == 1, ()); instructions
match instructions.first() { .matcher()
Some(QueryResponse { query_id, querier, .. }) .assert_remaining_insts(1)?
if ResponseHandler::expecting_response(origin, *query_id, querier.as_ref()) => .match_next_inst(|inst| match inst {
Ok(()), QueryResponse { query_id, querier, .. }
_ => Err(()), if ResponseHandler::expecting_response(origin, *query_id, querier.as_ref()) =>
} Ok(()),
_ => Err(()),
})?;
Ok(())
} }
} }
@@ -302,9 +312,13 @@ impl<T: Contains<MultiLocation>> ShouldExecute for AllowSubscriptionsFrom<T> {
origin, instructions, _max_weight, _weight_credit, origin, instructions, _max_weight, _weight_credit,
); );
ensure!(T::contains(origin), ()); ensure!(T::contains(origin), ());
match instructions { instructions
&mut [SubscribeVersion { .. } | UnsubscribeVersion] => Ok(()), .matcher()
_ => Err(()), .assert_remaining_insts(1)?
} .match_next_inst(|inst| match inst {
SubscribeVersion { .. } | UnsubscribeVersion => Ok(()),
_ => Err(()),
})?;
Ok(())
} }
} }