Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

XCM: Deny barrier checks for nested XCMs with specific instructions to be executed on the local chain #7200

Draft
wants to merge 13 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion polkadot/xcm/xcm-builder/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ workspace = true

[dependencies]
codec = { features = ["derive"], workspace = true }
environmental = { workspace = true }
frame-support = { workspace = true }
frame-system = { workspace = true }
impl-trait-for-tuples = { workspace = true }
Expand All @@ -21,6 +22,7 @@ pallet-asset-conversion = { workspace = true }
pallet-transaction-payment = { workspace = true }
scale-info = { features = ["derive"], workspace = true }
sp-arithmetic = { workspace = true }
sp-core = { workspace = true }
sp-io = { workspace = true }
sp-runtime = { workspace = true }
sp-weights = { workspace = true }
Expand All @@ -39,7 +41,6 @@ polkadot-primitives = { workspace = true, default-features = true }
polkadot-runtime-parachains = { workspace = true, default-features = true }
polkadot-test-runtime = { workspace = true }
primitive-types = { features = ["codec", "num-traits", "scale-info"], workspace = true }
sp-core = { workspace = true, default-features = true }

[features]
default = ["std"]
Expand All @@ -62,6 +63,7 @@ runtime-benchmarks = [
]
std = [
"codec/std",
"environmental/std",
"frame-support/std",
"frame-system/std",
"log/std",
Expand All @@ -71,6 +73,7 @@ std = [
"primitive-types/std",
"scale-info/std",
"sp-arithmetic/std",
"sp-core/std",
"sp-io/std",
"sp-runtime/std",
"sp-weights/std",
Expand Down
103 changes: 87 additions & 16 deletions polkadot/xcm/xcm-builder/src/barriers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,21 +82,22 @@ impl<T: Contains<Location>> ShouldExecute for AllowTopLevelPaidExecutionFrom<T>
instructions[..end]
.matcher()
.match_next_inst(|inst| match inst {
WithdrawAsset(ref assets) |
ReceiveTeleportedAsset(ref assets) |
ReserveAssetDeposited(ref assets) |
ClaimAsset { ref assets, .. } =>
WithdrawAsset(ref assets)
| ReceiveTeleportedAsset(ref assets)
| ReserveAssetDeposited(ref assets)
| ClaimAsset { ref assets, .. } => {
if assets.len() <= MAX_ASSETS_FOR_BUY_EXECUTION {
Ok(())
} else {
Err(ProcessMessageError::BadFormat)
},
}
},
_ => Err(ProcessMessageError::BadFormat),
})?
.skip_inst_while(|inst| {
matches!(inst, ClearOrigin | AliasOrigin(..)) ||
matches!(inst, DescendOrigin(child) if child != &Here) ||
matches!(inst, SetHints { .. })
matches!(inst, ClearOrigin | AliasOrigin(..))
|| matches!(inst, DescendOrigin(child) if child != &Here)
|| matches!(inst, SetHints { .. })
})?
.match_next_inst(|inst| match inst {
BuyExecution { weight_limit: Limited(ref mut weight), .. }
Expand Down Expand Up @@ -198,7 +199,7 @@ impl<InnerBarrier: ShouldExecute, LocalUniversal: Get<InteriorLocation>, MaxPref
},
DescendOrigin(j) => {
let Ok(_) = actual_origin.append_with(j.clone()) else {
return Err(ProcessMessageError::Unsupported)
return Err(ProcessMessageError::Unsupported);
};
},
_ => return Ok(ControlFlow::Break(())),
Expand Down Expand Up @@ -371,7 +372,9 @@ impl<ResponseHandler: OnResponse> ShouldExecute for AllowKnownQueryResponses<Res
.match_next_inst(|inst| match inst {
QueryResponse { query_id, querier, .. }
if ResponseHandler::expecting_response(origin, *query_id, querier.as_ref()) =>
Ok(()),
{
Ok(())
},
_ => Err(ProcessMessageError::BadFormat),
})?;
Ok(())
Expand Down Expand Up @@ -431,9 +434,9 @@ impl ShouldExecute for AllowHrmpNotificationsFromRelayChain {
.matcher()
.assert_remaining_insts(1)?
.match_next_inst(|inst| match inst {
HrmpNewChannelOpenRequest { .. } |
HrmpChannelAccepted { .. } |
HrmpChannelClosing { .. } => Ok(()),
HrmpNewChannelOpenRequest { .. }
| HrmpChannelAccepted { .. }
| HrmpChannelClosing { .. } => Ok(()),
_ => Err(ProcessMessageError::BadFormat),
})?;
Ok(())
Expand Down Expand Up @@ -478,9 +481,9 @@ impl DenyExecution for DenyReserveTransferToRelayChain {
InitiateReserveWithdraw {
reserve: Location { parents: 1, interior: Here },
..
} |
DepositReserveAsset { dest: Location { parents: 1, interior: Here }, .. } |
TransferReserveAsset { dest: Location { parents: 1, interior: Here }, .. } => {
}
| DepositReserveAsset { dest: Location { parents: 1, interior: Here }, .. }
| TransferReserveAsset { dest: Location { parents: 1, interior: Here }, .. } => {
Err(ProcessMessageError::Unsupported) // Deny
},

Expand All @@ -502,3 +505,71 @@ impl DenyExecution for DenyReserveTransferToRelayChain {
Ok(())
}
}

environmental::environmental!(recursion_count: u8);

// TBD:
/// Applies the `Inner` filter to the nested XCM for the `SetAppendix`, `SetErrorHandler`, and `ExecuteWithOrigin` instructions.
///
/// Note: The nested XCM is checked recursively!
pub struct DenyInstructionsWithXcm<Inner>(PhantomData<Inner>);
Copy link

@raymondkfcheung raymondkfcheung Jan 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding the naming, maybe we can use DenyNestedXcmInstructions, which may be better to state clearly with nested XCMs.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think DenyNestedXcmInstructions is a better name, but we still need to ensure it's explicitly used only for the following instructions: SetAppendix, SetErrorHandler, and ExecuteWithOrigin—which are meant to be executed on the local chain.

This is important because there are other instructions with nested XCM, such as DepositReserveAsset { xcm: Xcm<()>, ... } and InitiateReserveWithdraw { xcm: Xcm<()>, ... }, where the inner xcm is executed on a remote chain.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point! If we want to check all nested XCM instructions recursively (including those executed remotely), we could rename it to DenyNestedXcmInstructions and adjust the logic accordingly.

However, since this implementation currently applies only to instructions executed on the local chain (SetAppendix, SetErrorHandler, and ExecuteWithOrigin), we could instead introduce two separate types for clarity:

  • DenyNestedLocalInstructions: Covers only local execution cases (current behavior).
  • DenyNestedRemoteInstructions: Specifically targets instructions like DepositReserveAsset and InitiateReserveWithdraw, ensuring remote execution filtering.

Would this separation make sense, or do you think a more unified approach is preferable?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DenyNestedRemoteInstructions: Specifically targets instructions like DepositReserveAsset and InitiateReserveWithdraw, ensuring remote execution filtering.

Generally, a local chain should not assume rules about other chains' rules/barriers.

The design is that each chain only enforces its own local rules.

It is the job of the offchain component (wallet/ui/app) building the XCM to validate (e.g. through XCM dry-run APIs) that the XCM they build will pass barriers on all involved chains.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've defined a new NestedXcmType when performing the deny execution. This categorises nested XCM calls as either Local or Remote, ensuring we handle them separately.

Right now, both types go through deny_recursively, but I can implement two different denial strategies (DenyNestedLocalInstructions and DenyNestedRemoteInstructions) if needed. Let me know if you see any difference in behavior, or if you'd prefer keeping a single DenyNestedXcmInstructions that applies to both.

Or shall I use origin: &Location to determine whether it's Local or Remote? If it's Remote, then I could deny all location-based instructions.

impl<Inner: ShouldExecute> ShouldExecute for DenyInstructionsWithXcm<Inner> {
fn should_execute<RuntimeCall>(
origin: &Location,
message: &mut [Instruction<RuntimeCall>],
max_weight: Weight,
properties: &mut Properties,
) -> Result<(), ProcessMessageError> {
message.matcher().match_next_inst_while(
|_| true,
|inst| match inst {
SetAppendix(nested_xcm) |
SetErrorHandler(nested_xcm) |
ExecuteWithOrigin { xcm: nested_xcm, .. } => {
// check xcm instructions with `Inner` filter
let _ = Inner::should_execute(origin, nested_xcm.inner_mut(), max_weight, properties)
.inspect_err(|e| {
log::warn!(
target: "xcm::barriers",
"`DenyInstructionsWithXcm`'s `Inner::should_execute` did not pass for origin: {:?} and nested_xcm: {:?} with error: {:?}",
origin,
nested_xcm,
e
);
})?;

// Initialize the recursion count only the first time we hit this code in our
// potential recursive execution.
let _ = recursion_count::using_once(&mut 1, || {
recursion_count::with(|count| {
if *count > xcm_executor::RECURSION_LIMIT {
return Err(ProcessMessageError::StackLimitReached)
}
*count = count.saturating_add(1);
Ok(())
})
// This should always return `Some`, but let's play it safe.
.unwrap_or(Ok(()))?;

// Ensure that we always decrement the counter whenever we finish processing
// the `DenyInstructionsWithXcm`.
sp_core::defer! {
recursion_count::with(|count| {
*count = count.saturating_sub(1);
});
}

// check recursively with `DenyInstructionsWithXcm`
Self::should_execute(origin, nested_xcm.inner_mut(), max_weight, properties)
})?;

Ok(ControlFlow::Continue(()))
},
_ => Ok(ControlFlow::Continue(())),
},
)?;

// Permit everything else
Ok(())
}
}
4 changes: 2 additions & 2 deletions polkadot/xcm/xcm-builder/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ mod barriers;
pub use barriers::{
AllowExplicitUnpaidExecutionFrom, AllowHrmpNotificationsFromRelayChain,
AllowKnownQueryResponses, AllowSubscriptionsFrom, AllowTopLevelPaidExecutionFrom,
AllowUnpaidExecutionFrom, DenyReserveTransferToRelayChain, DenyThenTry, IsChildSystemParachain,
IsParentsOnly, IsSiblingSystemParachain, RespectSuspension, TakeWeightCredit,
AllowUnpaidExecutionFrom, DenyReserveTransferToRelayChain, DenyInstructionsWithXcm, DenyThenTry,
IsChildSystemParachain, IsParentsOnly, IsSiblingSystemParachain, RespectSuspension, TakeWeightCredit,
TrailingSetTopicAsId, WithComputedOrigin,
};

Expand Down
Loading
Loading