Use Message Queue as DMP and XCMP dispatch queue (#1246)

(imported from https://github.com/paritytech/cumulus/pull/2157)

## Changes

This MR refactores the XCMP, Parachains System and DMP pallets to use
the [MessageQueue](https://github.com/paritytech/substrate/pull/12485)
for delayed execution of incoming messages. The DMP pallet is entirely
replaced by the MQ and thereby removed. This allows for PoV-bounded
execution and resolves a number of issues that stem from the current
work-around.

All System Parachains adopt this change.  
The most important changes are in `primitives/core/src/lib.rs`,
`parachains/common/src/process_xcm_message.rs`,
`pallets/parachain-system/src/lib.rs`, `pallets/xcmp-queue/src/lib.rs`
and the runtime configs.

### DMP Queue Pallet

The pallet got removed and its logic refactored into parachain-system.
Overweight message management can be done directly through the MQ
pallet.

Final undeployment migrations are provided by
`cumulus_pallet_dmp_queue::UndeployDmpQueue` and `DeleteDmpQueue` that
can be configured with an aux config trait like:

```rust
parameter_types! {
	pub const DmpQueuePalletName: &'static str = \"DmpQueue\" < CHANGE ME;
	pub const RelayOrigin: AggregateMessageOrigin = AggregateMessageOrigin::Parent;
}

impl cumulus_pallet_dmp_queue::MigrationConfig for Runtime {
	type PalletName = DmpQueuePalletName;
	type DmpHandler = frame_support::traits::EnqueueWithOrigin<MessageQueue, RelayOrigin>;
	type DbWeight = <Runtime as frame_system::Config>::DbWeight;
}

// And adding them to your Migrations tuple:
pub type Migrations = (
	...
	cumulus_pallet_dmp_queue::UndeployDmpQueue<Runtime>,
	cumulus_pallet_dmp_queue::DeleteDmpQueue<Runtime>,
);
```

### XCMP Queue pallet

Removed all dispatch queue functionality. Incoming XCMP messages are now
either: Immediately handled if they are Signals, enqueued into the MQ
pallet otherwise.

New config items for the XCMP queue pallet:
```rust
/// The actual queue implementation that retains the messages for later processing.
type XcmpQueue: EnqueueMessage<ParaId>;

/// How a XCM over HRMP from a sibling parachain should be processed.
type XcmpProcessor: ProcessMessage<Origin = ParaId>;

/// The maximal number of suspended XCMP channels at the same time.
#[pallet::constant]
type MaxInboundSuspended: Get<u32>;
```

How to configure those:

```rust
// Use the MessageQueue pallet to store messages for later processing. The `TransformOrigin` is needed since
// the MQ pallet itself operators on `AggregateMessageOrigin` but we want to enqueue `ParaId`s.
type XcmpQueue = TransformOrigin<MessageQueue, AggregateMessageOrigin, ParaId, ParaIdToSibling>;

// Process XCMP messages from siblings. This is type-safe to only accept `ParaId`s. They will be dispatched
// with origin `Junction::Sibling(…)`.
type XcmpProcessor = ProcessFromSibling<
	ProcessXcmMessage<
		AggregateMessageOrigin,
		xcm_executor::XcmExecutor<xcm_config::XcmConfig>,
		RuntimeCall,
	>,
>;

// Not really important what to choose here. Just something larger than the maximal number of channels.
type MaxInboundSuspended = sp_core::ConstU32<1_000>;
```

The `InboundXcmpStatus` storage item was replaced by
`InboundXcmpSuspended` since it now only tracks inbound queue suspension
and no message indices anymore.

Now only sends the most recent channel `Signals`, as all prio ones are
out-dated anyway.

### Parachain System pallet

For `DMP` messages instead of forwarding them to the `DMP` pallet, it
now pushes them to the configured `DmpQueue`. The message processing
which was triggered in `set_validation_data` is now being done by the MQ
pallet `on_initialize`.

XCMP messages are still handed off to the `XcmpMessageHandler`
(XCMP-Queue pallet) - no change here.

New config items for the parachain system pallet:
```rust
/// Queues inbound downward messages for delayed processing. 
///
/// Analogous to the `XcmpQueue` of the XCMP queue pallet.
type DmpQueue: EnqueueMessage<AggregateMessageOrigin>;
``` 

How to configure:
```rust
/// Use the MQ pallet to store DMP messages for delayed processing.
type DmpQueue = MessageQueue;
``` 

## Message Flow

The flow of messages on the parachain side. Messages come in from the
left via the `Validation Data` and finally end up at the `Xcm Executor`
on the right.

![Untitled
(1)](https://github.com/paritytech/cumulus/assets/10380170/6cf8b377-88c9-4aed-96df-baace266e04d)

## Further changes

- Bumped the default suspension, drop and resume thresholds in
`QueueConfigData::default()`.
- `XcmpQueue::{suspend_xcm_execution, resume_xcm_execution}` errors when
they would be a noop.
- Properly validate the `QueueConfigData` before setting it.
- Marked weight files as auto-generated so they wont auto-expand in the
MR files view.
- Move the `hypothetical` asserts to `frame_support` under the name
`experimental_hypothetically`

Questions:
- [ ] What about the ugly `#[cfg(feature = \"runtime-benchmarks\")]` in
the runtimes? Not sure how to best fix. Just having them like this makes
tests fail that rely on the real message processor when the feature is
enabled.
- [ ] Need a good weight for `MessageQueueServiceWeight`. The scheduler
already takes 80% so I put it to 10% but that is quite low.

TODO:
- [x] Remove c&p code after
https://github.com/paritytech/polkadot/pull/6271
- [x] Use `HandleMessage` once it is public in Substrate
- [x] fix `runtime-benchmarks` feature
https://github.com/paritytech/polkadot/pull/6966
- [x] Benchmarks
- [x] Tests
- [ ] Migrate `InboundXcmpStatus` to `InboundXcmpSuspended`
- [x] Possibly cleanup Migrations (DMP+XCMP)
- [x] optional: create `TransformProcessMessageOrigin` in Substrate and
replace `ProcessFromSibling`
- [ ] Rerun weights on ref HW

---------

Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>
Co-authored-by: Liam Aharon <liam.aharon@hotmail.com>
Co-authored-by: joe petrowski <25483142+joepetrowski@users.noreply.github.com>
Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com>
Co-authored-by: command-bot <>
This commit is contained in:
Oliver Tale-Yazdi
2023-11-02 15:31:38 +01:00
committed by GitHub
parent 7df0417bcd
commit e1c033ebe1
277 changed files with 11604 additions and 4733 deletions
+539 -180
View File
@@ -13,247 +13,325 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use super::*;
use super::{
mock::{mk_page, v2_xcm, v3_xcm, EnqueuedMessages, HRMP_PARA_ID},
*,
};
use XcmpMessageFormat::*;
use codec::Input;
use cumulus_primitives_core::{ParaId, XcmpMessageHandler};
use frame_support::{assert_noop, assert_ok};
use mock::{new_test_ext, ParachainSystem, RuntimeCall, RuntimeOrigin, Test, XcmpQueue};
use sp_runtime::traits::BadOrigin;
use frame_support::{
assert_err, assert_noop, assert_ok, assert_storage_noop, hypothetically,
traits::{Footprint, Hooks},
StorageNoopGuard,
};
use mock::{new_test_ext, ParachainSystem, RuntimeOrigin as Origin, Test, XcmpQueue};
use sp_runtime::traits::{BadOrigin, Zero};
use std::iter::{once, repeat};
#[test]
fn one_message_does_not_panic() {
fn empty_concatenated_works() {
new_test_ext().execute_with(|| {
let message_format = XcmpMessageFormat::ConcatenatedVersionedXcm.encode();
let messages = vec![(Default::default(), 1u32, message_format.as_slice())];
let data = ConcatenatedVersionedXcm.encode();
// This shouldn't cause a panic
XcmpQueue::handle_xcmp_messages(messages.into_iter(), Weight::MAX);
XcmpQueue::handle_xcmp_messages(once((1000.into(), 1, data.as_slice())), Weight::MAX);
})
}
#[test]
#[should_panic = "Invalid incoming blob message data"]
fn xcm_enqueueing_basic_works() {
new_test_ext().execute_with(|| {
let xcm = VersionedXcm::<Test>::from(Xcm::<Test>(vec![ClearOrigin])).encode();
let data = [ConcatenatedVersionedXcm.encode(), xcm.clone()].concat();
XcmpQueue::handle_xcmp_messages(once((1000.into(), 1, data.as_slice())), Weight::MAX);
assert_eq!(EnqueuedMessages::get(), vec![(1000.into(), xcm)]);
})
}
#[test]
fn xcm_enqueueing_many_works() {
new_test_ext().execute_with(|| {
let mut encoded_xcms = vec![];
for i in 0..10 {
let xcm = VersionedXcm::<Test>::from(Xcm::<Test>(vec![ClearOrigin; i as usize]));
encoded_xcms.push(xcm.encode());
}
let mut data = ConcatenatedVersionedXcm.encode();
data.extend(encoded_xcms.iter().flatten());
XcmpQueue::handle_xcmp_messages(once((1000.into(), 1, data.as_slice())), Weight::MAX);
assert_eq!(
EnqueuedMessages::get(),
encoded_xcms.into_iter().map(|xcm| (1000.into(), xcm)).collect::<Vec<_>>(),
);
})
}
#[test]
fn xcm_enqueueing_multiple_times_works() {
new_test_ext().execute_with(|| {
let mut encoded_xcms = vec![];
for _ in 0..10 {
let xcm = VersionedXcm::<Test>::from(Xcm::<Test>(vec![ClearOrigin]));
encoded_xcms.push(xcm.encode());
}
let mut data = ConcatenatedVersionedXcm.encode();
data.extend(encoded_xcms.iter().flatten());
for i in 0..10 {
XcmpQueue::handle_xcmp_messages(once((1000.into(), 1, data.as_slice())), Weight::MAX);
assert_eq!((i + 1) * 10, EnqueuedMessages::get().len());
}
assert_eq!(
EnqueuedMessages::get(),
encoded_xcms
.into_iter()
.map(|xcm| (1000.into(), xcm))
.cycle()
.take(100)
.collect::<Vec<_>>(),
);
})
}
#[test]
#[cfg_attr(debug_assertions, should_panic = "Defensive failure")]
fn xcm_enqueueing_starts_dropping_on_overflow() {
new_test_ext().execute_with(|| {
let xcm = VersionedXcm::<Test>::from(Xcm::<Test>(vec![ClearOrigin]));
let data = (ConcatenatedVersionedXcm, xcm).encode();
// Its possible to enqueue 256 messages at most:
let limit = 256;
XcmpQueue::handle_xcmp_messages(
repeat((1000.into(), 1, data.as_slice())).take(limit * 2),
Weight::MAX,
);
assert_eq!(EnqueuedMessages::get().len(), limit);
// The drop threshold for pages is 48, the others numbers dont really matter:
assert_eq!(
<Test as Config>::XcmpQueue::footprint(1000.into()),
QueueFootprint { storage: Footprint { count: 256, size: 768 }, pages: 48 }
);
})
}
/// First enqueue 10 good, 1 bad and then 10 good XCMs.
///
/// It should only process the first 10 good though.
#[test]
#[cfg(not(debug_assertions))]
fn xcm_enqueueing_broken_xcm_works() {
new_test_ext().execute_with(|| {
let mut encoded_xcms = vec![];
for _ in 0..10 {
let xcm = VersionedXcm::<Test>::from(Xcm::<Test>(vec![ClearOrigin]));
encoded_xcms.push(xcm.encode());
}
let mut good = ConcatenatedVersionedXcm.encode();
good.extend(encoded_xcms.iter().flatten());
let mut bad = ConcatenatedVersionedXcm.encode();
bad.extend(vec![0u8].into_iter());
// Of we enqueue them in multiple pages, then its fine.
XcmpQueue::handle_xcmp_messages(once((1000.into(), 1, good.as_slice())), Weight::MAX);
XcmpQueue::handle_xcmp_messages(once((1000.into(), 1, bad.as_slice())), Weight::MAX);
XcmpQueue::handle_xcmp_messages(once((1000.into(), 1, good.as_slice())), Weight::MAX);
assert_eq!(20, EnqueuedMessages::get().len());
assert_eq!(
EnqueuedMessages::get(),
encoded_xcms
.clone()
.into_iter()
.map(|xcm| (1000.into(), xcm))
.cycle()
.take(20)
.collect::<Vec<_>>(),
);
EnqueuedMessages::set(&vec![]);
// But if we do it all in one page, then it only uses the first 10:
XcmpQueue::handle_xcmp_messages(
vec![(1000.into(), 1, good.as_slice()), (1000.into(), 1, bad.as_slice())].into_iter(),
Weight::MAX,
);
assert_eq!(10, EnqueuedMessages::get().len());
assert_eq!(
EnqueuedMessages::get(),
encoded_xcms
.into_iter()
.map(|xcm| (1000.into(), xcm))
.cycle()
.take(10)
.collect::<Vec<_>>(),
);
})
}
/// Message blobs are not supported and panic in debug mode.
#[test]
#[should_panic = "Blob messages are unhandled"]
#[cfg(debug_assertions)]
fn bad_message_is_handled() {
fn bad_blob_message_panics() {
new_test_ext().execute_with(|| {
let bad_data = vec![
1, 1, 3, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 64, 239, 139, 0,
0, 0, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 0, 0, 37, 0,
0, 0, 0, 0, 0, 0, 16, 0, 127, 147,
];
InboundXcmpMessages::<Test>::insert(ParaId::from(1000), 1, bad_data);
let format = XcmpMessageFormat::ConcatenatedEncodedBlob;
// This should exit with an error.
XcmpQueue::process_xcmp_message(
1000.into(),
(1, format),
&mut 0,
Weight::from_parts(10_000_000_000, 0),
Weight::from_parts(10_000_000_000, 0),
);
let data = [ConcatenatedEncodedBlob.encode(), vec![1].encode()].concat();
XcmpQueue::handle_xcmp_messages(once((1000.into(), 1, data.as_slice())), Weight::MAX);
});
}
/// Tests that a blob message is handled. Currently this isn't implemented and panics when debug
/// assertions are enabled. When this feature is enabled, this test should be rewritten properly.
/// Message blobs do not panic in release mode but are just a No-OP.
#[test]
#[should_panic = "Blob messages not handled."]
#[cfg(not(debug_assertions))]
fn bad_blob_message_no_panic() {
new_test_ext().execute_with(|| {
let data = [ConcatenatedEncodedBlob.encode(), vec![1].encode()].concat();
frame_support::assert_storage_noop!(XcmpQueue::handle_xcmp_messages(
once((1000.into(), 1, data.as_slice())),
Weight::MAX,
));
});
}
/// Invalid concatenated XCMs panic in debug mode.
#[test]
#[should_panic = "HRMP inbound decode stream broke; page will be dropped."]
#[cfg(debug_assertions)]
fn handle_blob_message() {
fn handle_invalid_data_panics() {
new_test_ext().execute_with(|| {
let bad_data = vec![
1, 1, 1, 1, 3, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 64, 239,
139, 0, 0, 0, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 0, 0,
37, 0, 0, 0, 0, 0, 0, 0, 16, 0, 127, 147,
];
InboundXcmpMessages::<Test>::insert(ParaId::from(1000), 1, bad_data);
let format = XcmpMessageFormat::ConcatenatedEncodedBlob;
XcmpQueue::process_xcmp_message(
1000.into(),
(1, format),
&mut 0,
Weight::from_parts(10_000_000_000, 0),
Weight::from_parts(10_000_000_000, 0),
);
let data = [ConcatenatedVersionedXcm.encode(), Xcm::<Test>(vec![]).encode()].concat();
XcmpQueue::handle_xcmp_messages(once((1000.into(), 1, data.as_slice())), Weight::MAX);
});
}
/// Invalid concatenated XCMs do not panic in release mode but are just a No-OP.
#[test]
#[should_panic = "Invalid incoming XCMP message data"]
#[cfg(debug_assertions)]
fn handle_invalid_data() {
#[cfg(not(debug_assertions))]
fn handle_invalid_data_no_panic() {
new_test_ext().execute_with(|| {
let data = Xcm::<Test>(vec![]).encode();
InboundXcmpMessages::<Test>::insert(ParaId::from(1000), 1, data);
let format = XcmpMessageFormat::ConcatenatedVersionedXcm;
XcmpQueue::process_xcmp_message(
1000.into(),
(1, format),
&mut 0,
Weight::from_parts(10_000_000_000, 0),
Weight::from_parts(10_000_000_000, 0),
);
});
}
let data = [ConcatenatedVersionedXcm.encode(), Xcm::<Test>(vec![]).encode()].concat();
#[test]
fn service_overweight_unknown() {
new_test_ext().execute_with(|| {
assert_noop!(
XcmpQueue::service_overweight(RuntimeOrigin::root(), 0, Weight::from_parts(1000, 1000)),
Error::<Test>::BadOverweightIndex,
);
});
}
#[test]
fn service_overweight_bad_xcm_format() {
new_test_ext().execute_with(|| {
let bad_xcm = vec![255];
Overweight::<Test>::insert(0, (ParaId::from(1000), 0, bad_xcm));
assert_noop!(
XcmpQueue::service_overweight(RuntimeOrigin::root(), 0, Weight::from_parts(1000, 1000)),
Error::<Test>::BadXcm
);
frame_support::assert_storage_noop!(XcmpQueue::handle_xcmp_messages(
once((1000.into(), 1, data.as_slice())),
Weight::MAX,
));
});
}
#[test]
fn suspend_xcm_execution_works() {
new_test_ext().execute_with(|| {
assert!(!XcmpQueue::is_paused(&2000.into()));
QueueSuspended::<Test>::put(true);
assert!(XcmpQueue::is_paused(&2000.into()));
// System parachains can bypass suspension:
assert!(!XcmpQueue::is_paused(&999.into()));
});
}
let xcm =
VersionedXcm::from(Xcm::<RuntimeCall>(vec![Instruction::<RuntimeCall>::ClearOrigin]))
.encode();
let mut message_format = XcmpMessageFormat::ConcatenatedVersionedXcm.encode();
message_format.extend(xcm.clone());
let messages = vec![(ParaId::from(999), 1u32, message_format.as_slice())];
#[test]
fn suspend_and_resume_xcm_execution_work() {
new_test_ext().execute_with(|| {
assert_noop!(XcmpQueue::suspend_xcm_execution(Origin::signed(1)), BadOrigin);
assert_ok!(XcmpQueue::suspend_xcm_execution(Origin::root()));
assert_noop!(
XcmpQueue::suspend_xcm_execution(Origin::root()),
Error::<Test>::AlreadySuspended
);
assert!(QueueSuspended::<Test>::get());
// This should have executed the incoming XCM, because it came from a system parachain
XcmpQueue::handle_xcmp_messages(messages.into_iter(), Weight::MAX);
assert_noop!(XcmpQueue::resume_xcm_execution(Origin::signed(1)), BadOrigin);
assert_ok!(XcmpQueue::resume_xcm_execution(Origin::root()));
assert_noop!(
XcmpQueue::resume_xcm_execution(Origin::root()),
Error::<Test>::AlreadyResumed
);
assert!(!QueueSuspended::<Test>::get());
});
}
let queued_xcm = InboundXcmpMessages::<Test>::get(ParaId::from(999), 1u32);
assert!(queued_xcm.is_empty());
#[test]
#[cfg(not(debug_assertions))]
fn xcm_enqueueing_backpressure_works() {
let para: ParaId = 1000.into();
new_test_ext().execute_with(|| {
let xcm = VersionedXcm::<Test>::from(Xcm::<Test>(vec![ClearOrigin]));
let data = (ConcatenatedVersionedXcm, xcm).encode();
let messages = vec![(ParaId::from(2000), 1u32, message_format.as_slice())];
XcmpQueue::handle_xcmp_messages(repeat((para, 1, data.as_slice())).take(170), Weight::MAX);
// This shouldn't have executed the incoming XCM
XcmpQueue::handle_xcmp_messages(messages.into_iter(), Weight::MAX);
assert_eq!(EnqueuedMessages::get().len(), 170,);
// Not yet suspended:
assert!(InboundXcmpSuspended::<Test>::get().is_empty());
// Enqueueing one more will suspend it:
let xcm = VersionedXcm::<Test>::from(Xcm::<Test>(vec![ClearOrigin])).encode();
let small = [ConcatenatedVersionedXcm.encode(), xcm].concat();
let queued_xcm = InboundXcmpMessages::<Test>::get(ParaId::from(2000), 1u32);
assert_eq!(queued_xcm, xcm);
XcmpQueue::handle_xcmp_messages(once((para, 1, small.as_slice())), Weight::MAX);
// Suspended:
assert_eq!(InboundXcmpSuspended::<Test>::get().iter().collect::<Vec<_>>(), vec![&para]);
// Now enqueueing many more will only work until the drop threshold:
XcmpQueue::handle_xcmp_messages(repeat((para, 1, data.as_slice())).take(100), Weight::MAX);
assert_eq!(mock::EnqueuedMessages::get().len(), 256);
crate::mock::EnqueueToLocalStorage::<Pallet<Test>>::sweep_queue(para);
XcmpQueue::handle_xcmp_messages(once((para, 1, small.as_slice())), Weight::MAX);
// Got resumed:
assert!(InboundXcmpSuspended::<Test>::get().is_empty());
// Still resumed:
XcmpQueue::handle_xcmp_messages(once((para, 1, small.as_slice())), Weight::MAX);
assert!(InboundXcmpSuspended::<Test>::get().is_empty());
});
}
#[test]
fn update_suspend_threshold_works() {
new_test_ext().execute_with(|| {
let data: QueueConfigData = <QueueConfig<Test>>::get();
assert_eq!(data.suspend_threshold, 2);
assert_ok!(XcmpQueue::update_suspend_threshold(RuntimeOrigin::root(), 3));
assert_noop!(XcmpQueue::update_suspend_threshold(RuntimeOrigin::signed(2), 5), BadOrigin);
let data: QueueConfigData = <QueueConfig<Test>>::get();
assert_eq!(<QueueConfig<Test>>::get().suspend_threshold, 32);
assert_noop!(XcmpQueue::update_suspend_threshold(Origin::signed(2), 49), BadOrigin);
assert_eq!(data.suspend_threshold, 3);
assert_ok!(XcmpQueue::update_suspend_threshold(Origin::root(), 33));
assert_eq!(<QueueConfig<Test>>::get().suspend_threshold, 33);
});
}
#[test]
fn update_drop_threshold_works() {
new_test_ext().execute_with(|| {
let data: QueueConfigData = <QueueConfig<Test>>::get();
assert_eq!(data.drop_threshold, 5);
assert_ok!(XcmpQueue::update_drop_threshold(RuntimeOrigin::root(), 6));
assert_noop!(XcmpQueue::update_drop_threshold(RuntimeOrigin::signed(2), 7), BadOrigin);
let data: QueueConfigData = <QueueConfig<Test>>::get();
assert_eq!(<QueueConfig<Test>>::get().drop_threshold, 48);
assert_ok!(XcmpQueue::update_drop_threshold(Origin::root(), 4000));
assert_noop!(XcmpQueue::update_drop_threshold(Origin::signed(2), 7), BadOrigin);
assert_eq!(data.drop_threshold, 6);
assert_eq!(<QueueConfig<Test>>::get().drop_threshold, 4000);
});
}
#[test]
fn update_resume_threshold_works() {
new_test_ext().execute_with(|| {
let data: QueueConfigData = <QueueConfig<Test>>::get();
assert_eq!(data.resume_threshold, 1);
assert_ok!(XcmpQueue::update_resume_threshold(RuntimeOrigin::root(), 2));
assert_noop!(XcmpQueue::update_resume_threshold(RuntimeOrigin::signed(7), 3), BadOrigin);
let data: QueueConfigData = <QueueConfig<Test>>::get();
assert_eq!(data.resume_threshold, 2);
});
}
#[test]
fn update_threshold_weight_works() {
new_test_ext().execute_with(|| {
let data: QueueConfigData = <QueueConfig<Test>>::get();
assert_eq!(data.threshold_weight, Weight::from_parts(100_000, 0));
assert_ok!(XcmpQueue::update_threshold_weight(
RuntimeOrigin::root(),
Weight::from_parts(10_000, 0)
));
assert_eq!(<QueueConfig<Test>>::get().resume_threshold, 8);
assert_noop!(
XcmpQueue::update_threshold_weight(
RuntimeOrigin::signed(5),
Weight::from_parts(10_000_000, 0),
),
BadOrigin
XcmpQueue::update_resume_threshold(Origin::root(), 0),
Error::<Test>::BadQueueConfig
);
let data: QueueConfigData = <QueueConfig<Test>>::get();
assert_eq!(data.threshold_weight, Weight::from_parts(10_000, 0));
});
}
#[test]
fn update_weight_restrict_decay_works() {
new_test_ext().execute_with(|| {
let data: QueueConfigData = <QueueConfig<Test>>::get();
assert_eq!(data.weight_restrict_decay, Weight::from_parts(2, 0));
assert_ok!(XcmpQueue::update_weight_restrict_decay(
RuntimeOrigin::root(),
Weight::from_parts(5, 0)
));
assert_noop!(
XcmpQueue::update_weight_restrict_decay(
RuntimeOrigin::signed(6),
Weight::from_parts(4, 0),
),
BadOrigin
XcmpQueue::update_resume_threshold(Origin::root(), 33),
Error::<Test>::BadQueueConfig
);
let data: QueueConfigData = <QueueConfig<Test>>::get();
assert_ok!(XcmpQueue::update_resume_threshold(Origin::root(), 16));
assert_noop!(XcmpQueue::update_resume_threshold(Origin::signed(7), 3), BadOrigin);
assert_eq!(data.weight_restrict_decay, Weight::from_parts(5, 0));
});
}
#[test]
fn update_xcmp_max_individual_weight() {
new_test_ext().execute_with(|| {
let data: QueueConfigData = <QueueConfig<Test>>::get();
assert_eq!(
data.xcmp_max_individual_weight,
Weight::from_parts(20u64 * WEIGHT_REF_TIME_PER_MILLIS, DEFAULT_POV_SIZE),
);
assert_ok!(XcmpQueue::update_xcmp_max_individual_weight(
RuntimeOrigin::root(),
Weight::from_parts(30u64 * WEIGHT_REF_TIME_PER_MILLIS, 0)
));
assert_noop!(
XcmpQueue::update_xcmp_max_individual_weight(
RuntimeOrigin::signed(3),
Weight::from_parts(10u64 * WEIGHT_REF_TIME_PER_MILLIS, 0)
),
BadOrigin
);
let data: QueueConfigData = <QueueConfig<Test>>::get();
assert_eq!(
data.xcmp_max_individual_weight,
Weight::from_parts(30u64 * WEIGHT_REF_TIME_PER_MILLIS, 0)
);
assert_eq!(<QueueConfig<Test>>::get().resume_threshold, 16);
});
}
@@ -343,6 +421,287 @@ fn xcmp_queue_consumes_dest_and_msg_on_ok_validate() {
});
}
#[test]
fn xcmp_queue_validate_nested_xcm_works() {
let dest = (Parent, X1(Parachain(5555)));
// Message that is not too deeply nested:
let mut good = Xcm(vec![ClearOrigin]);
for _ in 0..MAX_XCM_DECODE_DEPTH - 1 {
good = Xcm(vec![SetAppendix(good)]);
}
new_test_ext().execute_with(|| {
// Check that the good message is validated:
assert_ok!(<XcmpQueue as SendXcm>::validate(
&mut Some(dest.into()),
&mut Some(good.clone())
));
// Nesting the message one more time should reject it:
let bad = Xcm(vec![SetAppendix(good)]);
assert_eq!(
Err(SendError::ExceedsMaxMessageSize),
<XcmpQueue as SendXcm>::validate(&mut Some(dest.into()), &mut Some(bad))
);
});
}
#[test]
fn send_xcm_nested_works() {
let dest = (Parent, X1(Parachain(HRMP_PARA_ID)));
// Message that is not too deeply nested:
let mut good = Xcm(vec![ClearOrigin]);
for _ in 0..MAX_XCM_DECODE_DEPTH - 1 {
good = Xcm(vec![SetAppendix(good)]);
}
// Check that the good message is sent:
new_test_ext().execute_with(|| {
assert_ok!(send_xcm::<XcmpQueue>(dest.into(), good.clone()));
assert_eq!(
XcmpQueue::take_outbound_messages(usize::MAX),
vec![(
HRMP_PARA_ID.into(),
(XcmpMessageFormat::ConcatenatedVersionedXcm, VersionedXcm::V3(good.clone()))
.encode(),
)]
);
});
// Nesting the message one more time should not send it:
let bad = Xcm(vec![SetAppendix(good)]);
new_test_ext().execute_with(|| {
assert_err!(send_xcm::<XcmpQueue>(dest.into(), bad), SendError::ExceedsMaxMessageSize);
assert!(XcmpQueue::take_outbound_messages(usize::MAX).is_empty());
});
}
#[test]
fn hrmp_signals_are_prioritized() {
let message = Xcm(vec![Trap(5)]);
let sibling_para_id = ParaId::from(12345);
let dest = (Parent, X1(Parachain(sibling_para_id.into())));
let mut dest_wrapper = Some(dest.into());
let mut msg_wrapper = Some(message.clone());
new_test_ext().execute_with(|| {
frame_system::Pallet::<Test>::set_block_number(1);
<XcmpQueue as SendXcm>::validate(&mut dest_wrapper, &mut msg_wrapper).unwrap();
// check wrapper were consumed
assert_eq!(None, dest_wrapper.take());
assert_eq!(None, msg_wrapper.take());
ParachainSystem::open_custom_outbound_hrmp_channel_for_benchmarks_or_tests(
sibling_para_id,
cumulus_primitives_core::AbridgedHrmpChannel {
max_capacity: 128,
max_total_size: 1 << 16,
max_message_size: 128,
msg_count: 0,
total_size: 0,
mqc_head: None,
},
);
let taken = XcmpQueue::take_outbound_messages(130);
assert_eq!(taken, vec![]);
// Enqueue some messages
let num_events = frame_system::Pallet::<Test>::events().len();
for _ in 0..256 {
assert_ok!(send_xcm::<XcmpQueue>(dest.into(), message.clone()));
}
assert_eq!(num_events + 256, frame_system::Pallet::<Test>::events().len());
// Without a signal we get the messages in order:
let mut expected_msg = XcmpMessageFormat::ConcatenatedVersionedXcm.encode();
for _ in 0..31 {
expected_msg.extend(VersionedXcm::V3(message.clone()).encode());
}
hypothetically!({
let taken = XcmpQueue::take_outbound_messages(usize::MAX);
assert_eq!(taken, vec![(sibling_para_id.into(), expected_msg,)]);
});
// But a signal gets prioritized instead of the messages:
XcmpQueue::send_signal(sibling_para_id.into(), ChannelSignal::Suspend);
let taken = XcmpQueue::take_outbound_messages(130);
assert_eq!(
taken,
vec![(
sibling_para_id.into(),
(XcmpMessageFormat::Signals, ChannelSignal::Suspend).encode()
)]
);
});
}
#[test]
fn maybe_double_encoded_versioned_xcm_works() {
// pre conditions
assert_eq!(VersionedXcm::<()>::V2(Default::default()).encode(), &[2, 0]);
assert_eq!(VersionedXcm::<()>::V3(Default::default()).encode(), &[3, 0]);
}
// Now also testing a page instead of just concat messages.
#[test]
fn maybe_double_encoded_versioned_xcm_decode_page_works() {
let page = mk_page();
// Now try to decode the page.
let input = &mut &page[..];
for i in 0..100 {
match (i % 2, VersionedXcm::<()>::decode(input)) {
(0, Ok(xcm)) => {
assert_eq!(xcm, v2_xcm());
},
(1, Ok(xcm)) => {
assert_eq!(xcm, v3_xcm());
},
unexpected => unreachable!("{:?}", unexpected),
}
}
assert_eq!(input.remaining_len(), Ok(Some(0)), "All data consumed");
}
/// Check that `take_first_concatenated_xcm` correctly splits a page into its XCMs.
#[test]
fn take_first_concatenated_xcm_works() {
let page = mk_page();
let input = &mut &page[..];
for i in 0..100 {
let xcm = XcmpQueue::take_first_concatenated_xcm(input, &mut WeightMeter::new()).unwrap();
match (i % 2, xcm) {
(0, data) | (2, data) => {
assert_eq!(data, v2_xcm().encode());
},
(1, data) | (3, data) => {
assert_eq!(data, v3_xcm().encode());
},
unexpected => unreachable!("{:?}", unexpected),
}
}
assert_eq!(input.remaining_len(), Ok(Some(0)), "All data consumed");
}
/// A message that is not too deeply nested will be accepted by `take_first_concatenated_xcm`.
#[test]
fn take_first_concatenated_xcm_good_recursion_depth_works() {
let mut good = Xcm::<()>(vec![ClearOrigin]);
for _ in 0..MAX_XCM_DECODE_DEPTH - 1 {
good = Xcm(vec![SetAppendix(good)]);
}
let good = VersionedXcm::V3(good);
let page = good.encode();
assert_ok!(XcmpQueue::take_first_concatenated_xcm(&mut &page[..], &mut WeightMeter::new()));
}
/// A message that is too deeply nested will be rejected by `take_first_concatenated_xcm`.
#[test]
fn take_first_concatenated_xcm_good_bad_depth_errors() {
let mut bad = Xcm::<()>(vec![ClearOrigin]);
for _ in 0..MAX_XCM_DECODE_DEPTH {
bad = Xcm(vec![SetAppendix(bad)]);
}
let bad = VersionedXcm::V3(bad);
let page = bad.encode();
assert_err!(
XcmpQueue::take_first_concatenated_xcm(&mut &page[..], &mut WeightMeter::new()),
()
);
}
#[test]
fn lazy_migration_works() {
use crate::migration::v3::*;
new_test_ext().execute_with(|| {
EnqueuedMessages::set(vec![]);
let _g = StorageNoopGuard::default(); // No storage is leaked.
let mut channels = vec![];
for i in 0..20 {
let para = ParaId::from(i);
let mut message_metadata = vec![];
for block in 0..30 {
message_metadata.push((block, XcmpMessageFormat::ConcatenatedVersionedXcm));
InboundXcmpMessages::<Test>::insert(para, block, vec![(i + block) as u8]);
}
channels.push(InboundChannelDetails {
sender: para,
state: InboundState::Ok,
message_metadata,
});
}
InboundXcmpStatus::<Test>::set(Some(channels));
for para in 0..20u32 {
assert_eq!(InboundXcmpStatus::<Test>::get().unwrap().len() as u32, 20 - para);
assert_eq!(InboundXcmpMessages::<Test>::iter_prefix(ParaId::from(para)).count(), 30);
for block in 0..30 {
XcmpQueue::on_idle(0u32.into(), Weight::MAX);
assert_eq!(
EnqueuedMessages::get(),
vec![(para.into(), vec![(29 - block + para) as u8])]
);
EnqueuedMessages::set(vec![]);
}
// One more to jump to the next channel:
XcmpQueue::on_idle(0u32.into(), Weight::MAX);
assert_eq!(InboundXcmpStatus::<Test>::get().unwrap().len() as u32, 20 - para - 1);
assert_eq!(InboundXcmpMessages::<Test>::iter_prefix(ParaId::from(para)).count(), 0);
}
// One more for the cleanup:
XcmpQueue::on_idle(0u32.into(), Weight::MAX);
assert!(!InboundXcmpStatus::<Test>::exists());
assert_eq!(InboundXcmpMessages::<Test>::iter().count(), 0);
EnqueuedMessages::set(vec![]);
});
}
#[test]
fn lazy_migration_noop_when_out_of_weight() {
use crate::migration::v3::*;
assert!(!XcmpQueue::on_idle_weight().is_zero(), "pre condition");
new_test_ext().execute_with(|| {
let _g = StorageNoopGuard::default(); // No storage is leaked.
let block = 5;
let para = ParaId::from(4);
let message_metadata = vec![(block, XcmpMessageFormat::ConcatenatedVersionedXcm)];
InboundXcmpMessages::<Test>::insert(para, block, vec![123u8]);
InboundXcmpStatus::<Test>::set(Some(vec![InboundChannelDetails {
sender: para,
state: InboundState::Ok,
message_metadata,
}]));
// Hypothetically, it would do something with enough weight limit:
hypothetically!({
XcmpQueue::on_idle(0u32.into(), Weight::MAX);
assert_eq!(EnqueuedMessages::get(), vec![(para, vec![123u8])]);
});
// But does not, since the limit is zero:
assert_storage_noop!({ XcmpQueue::on_idle(0u32.into(), Weight::zero()) });
InboundXcmpMessages::<Test>::remove(para, block);
InboundXcmpStatus::<Test>::kill();
});
}
#[test]
fn xcmp_queue_send_xcm_works() {
new_test_ext().execute_with(|| {