referrerpolicy=no-referrer-when-downgrade

staging_xcm_builder/
barriers.rs

1// Copyright (C) Parity Technologies (UK) Ltd.
2// This file is part of Polkadot.
3
4// Polkadot is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8
9// Polkadot is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13
14// You should have received a copy of the GNU General Public License
15// along with Polkadot.  If not, see <http://www.gnu.org/licenses/>.
16
17//! Various implementations for `ShouldExecute`.
18
19use crate::{CreateMatcher, MatchXcm};
20use core::{cell::Cell, marker::PhantomData, ops::ControlFlow, result::Result};
21use frame_support::{
22	ensure,
23	traits::{Contains, ContainsPair, Get, Nothing, ProcessMessageError},
24};
25use polkadot_parachain_primitives::primitives::IsSystem;
26use xcm::prelude::*;
27use xcm_executor::traits::{CheckSuspension, DenyExecution, OnResponse, Properties, ShouldExecute};
28
29/// Execution barrier that just takes `max_weight` from `properties.weight_credit`.
30///
31/// Useful to allow XCM execution by local chain users via extrinsics.
32/// E.g. `pallet_xcm::reserve_asset_transfer` to transfer a reserve asset
33/// out of the local chain to another one.
34pub struct TakeWeightCredit;
35impl ShouldExecute for TakeWeightCredit {
36	fn should_execute<RuntimeCall>(
37		origin: &Location,
38		instructions: &mut [Instruction<RuntimeCall>],
39		max_weight: Weight,
40		properties: &mut Properties,
41	) -> Result<(), ProcessMessageError> {
42		tracing::trace!(
43			target: "xcm::barriers",
44			?origin,
45			?instructions,
46			?max_weight,
47			?properties,
48			"TakeWeightCredit"
49		);
50		properties.weight_credit = properties
51			.weight_credit
52			.checked_sub(&max_weight)
53			.ok_or(ProcessMessageError::Overweight(max_weight))?;
54		Ok(())
55	}
56}
57
58const MAX_ASSETS_FOR_BUY_EXECUTION: usize = 2;
59
60/// Allows execution from `origin` if it is contained in `T` (i.e. `T::Contains(origin)`) taking
61/// payments into account.
62///
63/// Only allows for `WithdrawAsset`, `ReceiveTeleportedAsset`, `ReserveAssetDeposited` and
64/// `ClaimAsset` XCMs because they are the only ones that place assets in the Holding Register to
65/// pay for execution.
66pub struct AllowTopLevelPaidExecutionFrom<T>(PhantomData<T>);
67impl<T: Contains<Location>> ShouldExecute for AllowTopLevelPaidExecutionFrom<T> {
68	fn should_execute<RuntimeCall>(
69		origin: &Location,
70		instructions: &mut [Instruction<RuntimeCall>],
71		max_weight: Weight,
72		properties: &mut Properties,
73	) -> Result<(), ProcessMessageError> {
74		tracing::trace!(
75			target: "xcm::barriers",
76			?origin,
77			?instructions,
78			?max_weight,
79			?properties,
80			"AllowTopLevelPaidExecutionFrom",
81		);
82
83		ensure!(T::contains(origin), ProcessMessageError::Unsupported);
84		// We will read up to 5 instructions. This allows up to 3 `ClearOrigin` instructions. We
85		// allow for more than one since anything beyond the first is a no-op and it's conceivable
86		// that composition of operations might result in more than one being appended.
87		let end = instructions.len().min(5);
88		instructions[..end]
89			.matcher()
90			.match_next_inst(|inst| match inst {
91				WithdrawAsset(ref assets) |
92				ReceiveTeleportedAsset(ref assets) |
93				ReserveAssetDeposited(ref assets) |
94				ClaimAsset { ref assets, .. } => {
95					if assets.len() <= MAX_ASSETS_FOR_BUY_EXECUTION {
96						Ok(())
97					} else {
98						Err(ProcessMessageError::BadFormat)
99					}
100				},
101				_ => Err(ProcessMessageError::BadFormat),
102			})?
103			.skip_inst_while(|inst| {
104				matches!(inst, ClearOrigin | AliasOrigin(..)) ||
105					matches!(inst, DescendOrigin(child) if child != &Here) ||
106					matches!(inst, SetHints { .. })
107			})?
108			.match_next_inst(|inst| match inst {
109				BuyExecution { weight_limit: Limited(ref mut weight), .. }
110					if weight.all_gte(max_weight) =>
111				{
112					*weight = max_weight;
113					Ok(())
114				},
115				BuyExecution { ref mut weight_limit, .. } if weight_limit == &Unlimited => {
116					*weight_limit = Limited(max_weight);
117					Ok(())
118				},
119				PayFees { .. } => Ok(()),
120				_ => Err(ProcessMessageError::Overweight(max_weight)),
121			})?;
122		Ok(())
123	}
124}
125
126/// A derivative barrier, which scans the first `MaxPrefixes` instructions for origin-alterers and
127/// then evaluates `should_execute` of the `InnerBarrier` based on the remaining instructions and
128/// the newly computed origin.
129///
130/// This effectively allows for the possibility of distinguishing an origin which is acting as a
131/// router for its derivative locations (or as a bridge for a remote location) and an origin which
132/// is actually trying to send a message for itself. In the former case, the message will be
133/// prefixed with origin-mutating instructions.
134///
135/// Any barriers which should be interpreted based on the computed origin rather than the original
136/// message origin should be subject to this. This is the case for most barriers since the
137/// effective origin is generally more important than the routing origin. Any other barriers, and
138/// especially those which should be interpreted only the routing origin should not be subject to
139/// this.
140///
141/// E.g.
142/// ```nocompile
143/// type MyBarrier = (
144/// 	TakeWeightCredit,
145/// 	AllowTopLevelPaidExecutionFrom<DirectCustomerLocations>,
146/// 	WithComputedOrigin<(
147/// 		AllowTopLevelPaidExecutionFrom<DerivativeCustomerLocations>,
148/// 		AllowUnpaidExecutionFrom<ParentLocation>,
149/// 		AllowSubscriptionsFrom<AllowedSubscribers>,
150/// 		AllowKnownQueryResponses<TheResponseHandler>,
151/// 	)>,
152/// );
153/// ```
154///
155/// In the above example, `AllowUnpaidExecutionFrom` appears once underneath
156/// `WithComputedOrigin`. This is in order to distinguish between messages which are notionally
157/// from a derivative location of `ParentLocation` but that just happened to be sent via
158/// `ParentLocation` rather than messages that were sent by the parent.
159///
160/// Similarly `AllowTopLevelPaidExecutionFrom` appears twice: once inside of `WithComputedOrigin`
161/// where we provide the list of origins which are derivative origins, and then secondly outside
162/// of `WithComputedOrigin` where we provide the list of locations which are direct origins. It's
163/// reasonable for these lists to be merged into one and that used both inside and out.
164///
165/// Finally, we see `AllowSubscriptionsFrom` and `AllowKnownQueryResponses` are both inside of
166/// `WithComputedOrigin`. This means that if a message begins with origin-mutating instructions,
167/// then it must be the finally computed origin which we accept subscriptions or expect a query
168/// response from. For example, even if an origin appeared in the `AllowedSubscribers` list, we
169/// would ignore this rule if it began with origin mutators and they changed the origin to something
170/// which was not on the list.
171pub struct WithComputedOrigin<InnerBarrier, LocalUniversal, MaxPrefixes>(
172	PhantomData<(InnerBarrier, LocalUniversal, MaxPrefixes)>,
173);
174impl<InnerBarrier: ShouldExecute, LocalUniversal: Get<InteriorLocation>, MaxPrefixes: Get<u32>>
175	ShouldExecute for WithComputedOrigin<InnerBarrier, LocalUniversal, MaxPrefixes>
176{
177	fn should_execute<Call>(
178		origin: &Location,
179		instructions: &mut [Instruction<Call>],
180		max_weight: Weight,
181		properties: &mut Properties,
182	) -> Result<(), ProcessMessageError> {
183		tracing::trace!(
184			target: "xcm::barriers",
185			?origin,
186			?instructions,
187			?max_weight,
188			?properties,
189			"WithComputedOrigin"
190		);
191		let mut actual_origin = origin.clone();
192		let skipped = Cell::new(0usize);
193		// NOTE: We do not check the validity of `UniversalOrigin` here, meaning that a malicious
194		// origin could place a `UniversalOrigin` in order to spoof some location which gets free
195		// execution. This technical could get it past the barrier condition, but the execution
196		// would instantly fail since the first instruction would cause an error with the
197		// invalid UniversalOrigin.
198		instructions.matcher().match_next_inst_while(
199			|_| skipped.get() < MaxPrefixes::get() as usize,
200			|inst| {
201				match inst {
202					UniversalOrigin(new_global) => {
203						// Note the origin is *relative to local consensus*! So we need to escape
204						// local consensus with the `parents` before diving in into the
205						// `universal_location`.
206						actual_origin =
207							Junctions::from([*new_global]).relative_to(&LocalUniversal::get());
208					},
209					DescendOrigin(j) => {
210						let Ok(_) = actual_origin.append_with(j.clone()) else {
211							return Err(ProcessMessageError::Unsupported);
212						};
213					},
214					_ => return Ok(ControlFlow::Break(())),
215				};
216				skipped.set(skipped.get() + 1);
217				Ok(ControlFlow::Continue(()))
218			},
219		)?;
220		InnerBarrier::should_execute(
221			&actual_origin,
222			&mut instructions[skipped.get()..],
223			max_weight,
224			properties,
225		)
226	}
227}
228
229/// Sets the message ID to `t` using a `SetTopic(t)` in the last position if present.
230///
231/// Note that the message ID does not necessarily have to be unique; it is the
232/// sender's responsibility to ensure uniqueness.
233///
234/// Requires some inner barrier to pass on the rest of the message.
235pub struct TrailingSetTopicAsId<InnerBarrier>(PhantomData<InnerBarrier>);
236impl<InnerBarrier: ShouldExecute> ShouldExecute for TrailingSetTopicAsId<InnerBarrier> {
237	fn should_execute<Call>(
238		origin: &Location,
239		instructions: &mut [Instruction<Call>],
240		max_weight: Weight,
241		properties: &mut Properties,
242	) -> Result<(), ProcessMessageError> {
243		tracing::trace!(
244			target: "xcm::barriers",
245			?origin,
246			?instructions,
247			?max_weight,
248			?properties,
249			"TrailingSetTopicAsId"
250		);
251		let until = if let Some(SetTopic(t)) = instructions.last() {
252			properties.message_id = Some(*t);
253			instructions.len() - 1
254		} else {
255			instructions.len()
256		};
257		InnerBarrier::should_execute(&origin, &mut instructions[..until], max_weight, properties)
258	}
259}
260
261/// Barrier condition that allows for a `SuspensionChecker` that controls whether or not the XCM
262/// executor will be suspended from executing the given XCM.
263pub struct RespectSuspension<Inner, SuspensionChecker>(PhantomData<(Inner, SuspensionChecker)>);
264impl<Inner, SuspensionChecker> ShouldExecute for RespectSuspension<Inner, SuspensionChecker>
265where
266	Inner: ShouldExecute,
267	SuspensionChecker: CheckSuspension,
268{
269	fn should_execute<Call>(
270		origin: &Location,
271		instructions: &mut [Instruction<Call>],
272		max_weight: Weight,
273		properties: &mut Properties,
274	) -> Result<(), ProcessMessageError> {
275		if SuspensionChecker::is_suspended(origin, instructions, max_weight, properties) {
276			Err(ProcessMessageError::Yield)
277		} else {
278			Inner::should_execute(origin, instructions, max_weight, properties)
279		}
280	}
281}
282
283/// Allows execution from any origin that is contained in `T` (i.e. `T::Contains(origin)`).
284///
285/// Use only for executions from completely trusted origins, from which no permissionless messages
286/// can be sent.
287pub struct AllowUnpaidExecutionFrom<T>(PhantomData<T>);
288impl<T: Contains<Location>> ShouldExecute for AllowUnpaidExecutionFrom<T> {
289	fn should_execute<RuntimeCall>(
290		origin: &Location,
291		instructions: &mut [Instruction<RuntimeCall>],
292		max_weight: Weight,
293		properties: &mut Properties,
294	) -> Result<(), ProcessMessageError> {
295		tracing::trace!(
296			target: "xcm::barriers",
297			?origin, ?instructions, ?max_weight, ?properties,
298			"AllowUnpaidExecutionFrom"
299		);
300		ensure!(T::contains(origin), ProcessMessageError::Unsupported);
301		Ok(())
302	}
303}
304
305/// Allows execution from any origin that is contained in `T` (i.e. `T::Contains(origin)`) if the
306/// message explicitly includes the `UnpaidExecution` instruction.
307///
308/// Use only for executions from trusted origin groups.
309///
310/// Allows for the message to receive teleports or reserve asset transfers and altering
311/// the origin before indicating `UnpaidExecution`.
312///
313/// Origin altering instructions are executed so the barrier can more accurately reject messages
314/// whose effective origin at the time of calling `UnpaidExecution` is not allowed.
315/// This means `T` will be checked against the actual origin _after_ being modified by prior
316/// instructions.
317///
318/// In order to allow messages to use `AliasOrigin` before `UnpaidExecution`, the `Aliasers` type
319/// should be set to a *cheap, computation-only* subset of `xcm_executor::Config::Aliasers`.
320/// It must never include filters that read storage, such as `pallet_xcm::AuthorizedAliasers`:
321/// barriers run before any payment is taken, so an expensive check here is a free-of-charge load
322/// on the chain that any incoming message can trigger.
323///
324/// Aliases that are only allowed by the excluded (expensive) filters simply won't get unpaid
325/// execution through this barrier; they can still buy execution via the paid barrier.
326///
327/// With the default (`Nothing`), all messages with an `AliasOrigin` instruction will be rejected.
328pub struct AllowExplicitUnpaidExecutionFrom<T, Aliasers = Nothing>(PhantomData<(T, Aliasers)>);
329impl<T: Contains<Location>, Aliasers: ContainsPair<Location, Location>> ShouldExecute
330	for AllowExplicitUnpaidExecutionFrom<T, Aliasers>
331{
332	fn should_execute<Call>(
333		origin: &Location,
334		instructions: &mut [Instruction<Call>],
335		max_weight: Weight,
336		properties: &mut Properties,
337	) -> Result<(), ProcessMessageError> {
338		tracing::trace!(
339			target: "xcm::barriers",
340			?origin, ?instructions, ?max_weight, ?properties,
341			"AllowExplicitUnpaidExecutionFrom",
342		);
343		// We will read up to 5 instructions before `UnpaidExecution`.
344		// This allows up to 3 asset transfer instructions, thus covering all possible transfer
345		// types, followed by a potential origin altering instruction, and a potential `SetHints`.
346		let mut actual_origin = origin.clone();
347		let processed = Cell::new(0usize);
348		let instructions_to_process = 5;
349		instructions
350			.matcher()
351			// We skip set hints and all types of asset transfer instructions.
352			.match_next_inst_while(
353				|inst| {
354					processed.get() < instructions_to_process &&
355						matches!(
356							inst,
357							ReceiveTeleportedAsset(_) |
358								ReserveAssetDeposited(_) | WithdrawAsset(_) |
359								SetHints { .. }
360						)
361				},
362				|_| {
363					processed.set(processed.get() + 1);
364					Ok(ControlFlow::Continue(()))
365				},
366			)?
367			// Then we go through all origin altering instructions and we
368			// alter the original origin.
369			.match_next_inst_while(
370				|_| processed.get() < instructions_to_process,
371				|inst| {
372					match inst {
373						ClearOrigin => {
374							// We don't support the `ClearOrigin` instruction since we always need
375							// to know the origin to know if it's allowed unpaid execution.
376							return Err(ProcessMessageError::Unsupported);
377						},
378						AliasOrigin(target) => {
379							if Aliasers::contains(&actual_origin, &target) {
380								actual_origin = target.clone();
381							} else {
382								return Err(ProcessMessageError::Unsupported);
383							}
384						},
385						DescendOrigin(child) if child != &Here => {
386							let Ok(_) = actual_origin.append_with(child.clone()) else {
387								return Err(ProcessMessageError::Unsupported);
388							};
389						},
390						_ => return Ok(ControlFlow::Break(())),
391					};
392					processed.set(processed.get() + 1);
393					Ok(ControlFlow::Continue(()))
394				},
395			)?
396			// We finally match on the required `UnpaidExecution` instruction.
397			.match_next_inst(|inst| match inst {
398				UnpaidExecution { weight_limit: Limited(m), .. } if m.all_gte(max_weight) => Ok(()),
399				UnpaidExecution { weight_limit: Unlimited, .. } => Ok(()),
400				_ => Err(ProcessMessageError::Overweight(max_weight)),
401			})?;
402
403		// After processing all the instructions, `actual_origin` was modified and we
404		// check if it's allowed to have unpaid execution.
405		ensure!(T::contains(&actual_origin), ProcessMessageError::Unsupported);
406
407		Ok(())
408	}
409}
410
411/// Allows a message only if it is from a system-level child parachain.
412pub struct IsChildSystemParachain<ParaId>(PhantomData<ParaId>);
413impl<ParaId: IsSystem + From<u32>> Contains<Location> for IsChildSystemParachain<ParaId> {
414	fn contains(l: &Location) -> bool {
415		matches!(
416			l.interior().as_slice(),
417			[Junction::Parachain(id)]
418				if ParaId::from(*id).is_system() && l.parent_count() == 0,
419		)
420	}
421}
422
423/// Matches if the given location is a system-level sibling parachain.
424pub struct IsSiblingSystemParachain<ParaId, SelfParaId>(PhantomData<(ParaId, SelfParaId)>);
425impl<ParaId: IsSystem + From<u32> + Eq, SelfParaId: Get<ParaId>> Contains<Location>
426	for IsSiblingSystemParachain<ParaId, SelfParaId>
427{
428	fn contains(l: &Location) -> bool {
429		matches!(
430			l.unpack(),
431			(1, [Junction::Parachain(id)])
432				if SelfParaId::get() != ParaId::from(*id) && ParaId::from(*id).is_system(),
433		)
434	}
435}
436
437/// Matches if the given location contains only the specified amount of parents and no interior
438/// junctions.
439pub struct IsParentsOnly<Count>(PhantomData<Count>);
440impl<Count: Get<u8>> Contains<Location> for IsParentsOnly<Count> {
441	fn contains(t: &Location) -> bool {
442		t.contains_parents_only(Count::get())
443	}
444}
445
446/// Allows only messages if the generic `ResponseHandler` expects them via `expecting_response`.
447pub struct AllowKnownQueryResponses<ResponseHandler>(PhantomData<ResponseHandler>);
448impl<ResponseHandler: OnResponse> ShouldExecute for AllowKnownQueryResponses<ResponseHandler> {
449	fn should_execute<RuntimeCall>(
450		origin: &Location,
451		instructions: &mut [Instruction<RuntimeCall>],
452		max_weight: Weight,
453		properties: &mut Properties,
454	) -> Result<(), ProcessMessageError> {
455		tracing::trace!(
456			target: "xcm::barriers",
457			?origin, ?instructions, ?max_weight, ?properties,
458			"AllowKnownQueryResponses"
459		);
460		instructions
461			.matcher()
462			.assert_remaining_insts(1)?
463			.match_next_inst(|inst| match inst {
464				QueryResponse { query_id, querier, .. }
465					if ResponseHandler::expecting_response(origin, *query_id, querier.as_ref()) =>
466				{
467					Ok(())
468				},
469				_ => Err(ProcessMessageError::BadFormat),
470			})?;
471		Ok(())
472	}
473}
474
475/// Allows execution from `origin` if it is just a straight `SubscribeVersion` or
476/// `UnsubscribeVersion` instruction.
477pub struct AllowSubscriptionsFrom<T>(PhantomData<T>);
478impl<T: Contains<Location>> ShouldExecute for AllowSubscriptionsFrom<T> {
479	fn should_execute<RuntimeCall>(
480		origin: &Location,
481		instructions: &mut [Instruction<RuntimeCall>],
482		max_weight: Weight,
483		properties: &mut Properties,
484	) -> Result<(), ProcessMessageError> {
485		tracing::trace!(
486			target: "xcm::barriers",
487			?origin, ?instructions, ?max_weight, ?properties,
488			"AllowSubscriptionsFrom",
489		);
490		ensure!(T::contains(origin), ProcessMessageError::Unsupported);
491		instructions
492			.matcher()
493			.assert_remaining_insts(1)?
494			.match_next_inst(|inst| match inst {
495				SubscribeVersion { .. } | UnsubscribeVersion => Ok(()),
496				_ => Err(ProcessMessageError::BadFormat),
497			})?;
498		Ok(())
499	}
500}
501
502/// Allows execution for the Relay Chain origin (represented as `Location::parent()`) if it is just
503/// a straight `HrmpNewChannelOpenRequest`, `HrmpChannelAccepted`, or `HrmpChannelClosing`
504/// instruction.
505///
506/// Note: This barrier fulfills safety recommendations for the mentioned instructions - see their
507/// documentation.
508pub struct AllowHrmpNotificationsFromRelayChain;
509impl ShouldExecute for AllowHrmpNotificationsFromRelayChain {
510	fn should_execute<RuntimeCall>(
511		origin: &Location,
512		instructions: &mut [Instruction<RuntimeCall>],
513		max_weight: Weight,
514		properties: &mut Properties,
515	) -> Result<(), ProcessMessageError> {
516		tracing::trace!(
517			target: "xcm::barriers",
518			?origin, ?instructions, ?max_weight, ?properties,
519			"AllowHrmpNotificationsFromRelayChain"
520		);
521		// accept only the Relay Chain
522		ensure!(matches!(origin.unpack(), (1, [])), ProcessMessageError::Unsupported);
523		// accept only HRMP notifications and nothing else
524		instructions
525			.matcher()
526			.assert_remaining_insts(1)?
527			.match_next_inst(|inst| match inst {
528				HrmpNewChannelOpenRequest { .. } |
529				HrmpChannelAccepted { .. } |
530				HrmpChannelClosing { .. } => Ok(()),
531				_ => Err(ProcessMessageError::BadFormat),
532			})?;
533		Ok(())
534	}
535}
536
537/// Deny executing the XCM if it matches any of the Deny filter regardless of anything else.
538/// If it passes the Deny, and matches one of the Allow cases then it is let through.
539pub struct DenyThenTry<Deny, Allow>(PhantomData<Deny>, PhantomData<Allow>)
540where
541	Deny: DenyExecution,
542	Allow: ShouldExecute;
543
544impl<Deny, Allow> ShouldExecute for DenyThenTry<Deny, Allow>
545where
546	Deny: DenyExecution,
547	Allow: ShouldExecute,
548{
549	fn should_execute<RuntimeCall>(
550		origin: &Location,
551		message: &mut [Instruction<RuntimeCall>],
552		max_weight: Weight,
553		properties: &mut Properties,
554	) -> Result<(), ProcessMessageError> {
555		Deny::deny_execution(origin, message, max_weight, properties)?;
556		Allow::should_execute(origin, message, max_weight, properties)
557	}
558}
559
560// See issue <https://github.com/paritytech/polkadot/issues/5233>
561pub struct DenyReserveTransferToRelayChain;
562impl DenyExecution for DenyReserveTransferToRelayChain {
563	fn deny_execution<RuntimeCall>(
564		origin: &Location,
565		message: &mut [Instruction<RuntimeCall>],
566		_max_weight: Weight,
567		_properties: &mut Properties,
568	) -> Result<(), ProcessMessageError> {
569		message.matcher().match_next_inst_while(
570			|_| true,
571			|inst| match inst {
572				InitiateReserveWithdraw {
573					reserve: Location { parents: 1, interior: Here },
574					..
575				} |
576				DepositReserveAsset { dest: Location { parents: 1, interior: Here }, .. } |
577				TransferReserveAsset { dest: Location { parents: 1, interior: Here }, .. } => {
578					Err(ProcessMessageError::Unsupported) // Deny
579				},
580
581				// An unexpected reserve transfer has arrived from the Relay Chain. Generally,
582				// `IsReserve` should not allow this, but we just log it here.
583				ReserveAssetDeposited { .. }
584					if matches!(origin, Location { parents: 1, interior: Here }) =>
585				{
586					tracing::debug!(
587						target: "xcm::barriers",
588						"Unexpected ReserveAssetDeposited from the Relay Chain",
589					);
590					Ok(ControlFlow::Continue(()))
591				},
592
593				_ => Ok(ControlFlow::Continue(())),
594			},
595		)?;
596		Ok(())
597	}
598}
599
600environmental::environmental!(recursion_count: u8);
601
602/// Denies execution if the XCM contains instructions not meant to run on this chain,
603/// first checking at the top-level and then **recursively**.
604///
605/// This barrier only applies to **locally executed** XCM instructions (`SetAppendix`,
606/// `SetErrorHandler`, and `ExecuteWithOrigin`). Remote parts of the XCM are expected to be
607/// validated by the receiving chain's barrier.
608///
609/// Note: Ensures that restricted instructions do not execute on the local chain, enforcing stricter
610/// execution policies while allowing remote chains to enforce their own rules.
611pub struct DenyRecursively<Inner>(PhantomData<Inner>);
612
613impl<Inner: DenyExecution> DenyRecursively<Inner> {
614	/// Recursively applies the deny filter to a nested XCM.
615	///
616	/// Ensures that restricted instructions are blocked at any depth within the XCM.
617	/// Uses a **recursion counter** to prevent stack overflows from deep nesting.
618	fn deny_recursively<RuntimeCall>(
619		origin: &Location,
620		xcm: &mut Xcm<RuntimeCall>,
621		max_weight: Weight,
622		properties: &mut Properties,
623	) -> Result<ControlFlow<()>, ProcessMessageError> {
624		// Initialise recursion counter for this execution context.
625		recursion_count::using_once(&mut 1, || {
626			// Prevent stack overflow by enforcing a recursion depth limit.
627			recursion_count::with(|count| {
628				if *count > xcm::RECURSION_LIMIT {
629					tracing::debug!(
630                    	target: "xcm::barriers",
631                    	"Recursion limit exceeded (count: {count}), origin: {:?}, xcm: {:?}, max_weight: {:?}, properties: {:?}",
632                    	origin, xcm, max_weight, properties
633                	);
634					return None;
635				}
636				*count = count.saturating_add(1);
637				Some(())
638			}).flatten().ok_or(ProcessMessageError::StackLimitReached)?;
639
640			// Ensure the counter is decremented even if an early return occurs.
641			sp_core::defer! {
642				recursion_count::with(|count| {
643					*count = count.saturating_sub(1);
644				});
645			}
646
647			// Recursively check the nested XCM instructions.
648			Self::deny_execution(origin, xcm.inner_mut(), max_weight, properties)
649		})?;
650
651		Ok(ControlFlow::Continue(()))
652	}
653}
654
655impl<Inner: DenyExecution> DenyExecution for DenyRecursively<Inner> {
656	/// Denies execution of restricted local nested XCM instructions.
657	///
658	/// This checks for `SetAppendix`, `SetErrorHandler`, and `ExecuteWithOrigin` instruction
659	/// applying the deny filter **recursively** to any nested XCMs found.
660	fn deny_execution<RuntimeCall>(
661		origin: &Location,
662		instructions: &mut [Instruction<RuntimeCall>],
663		max_weight: Weight,
664		properties: &mut Properties,
665	) -> Result<(), ProcessMessageError> {
666		// First, check if the top-level message should be denied.
667		Inner::deny_execution(origin, instructions, max_weight, properties).inspect_err(|e| {
668			tracing::debug!(
669				target: "xcm::barriers",
670				"DenyRecursively::Inner denied execution, origin: {:?}, instructions: {:?}, max_weight: {:?}, properties: {:?}, error: {:?}",
671				origin, instructions, max_weight, properties, e
672			);
673		})?;
674
675		// If the top-level check passes, check nested instructions recursively.
676		instructions.matcher().match_next_inst_while(
677			|_| true,
678			|inst| match inst {
679				SetAppendix(nested_xcm) |
680				SetErrorHandler(nested_xcm) |
681				ExecuteWithOrigin { xcm: nested_xcm, .. } => Self::deny_recursively::<RuntimeCall>(
682					origin, nested_xcm, max_weight, properties,
683				),
684				_ => Ok(ControlFlow::Continue(())),
685			},
686		)?;
687
688		// Permit everything else
689		Ok(())
690	}
691}