mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-05-30 10:31:03 +00:00
Upward Message Passing implementation (#1885)
* UMP: Update the impl guide * UMP: Incorporate XCM related changes into the guide * UMP: Data structures and configuration * UMP: Initial plumbing * UMP: Data layout * UMP: Acceptance criteria & enactment * UMP: Fix dispatcher bug and add the test for it * UMP: Constrain the maximum size of an UMP message This commit addresses the UMP part of https://github.com/paritytech/polkadot/issues/1869 * Fix failing test due to misconfiguration * Make the type of RelayDispatchQueueSize be more apparent in the guide * Revert renaming `max_upward_queue_capacity` to `max_upward_queue_count` * convert spaces to tabs Co-authored-by: Bernhard Schuster <bernhard@ahoi.io> * Update runtime/parachains/src/router/ump.rs Co-authored-by: Bernhard Schuster <bernhard@ahoi.io> Co-authored-by: Bernhard Schuster <bernhard@ahoi.io>
This commit is contained in:
@@ -501,7 +501,7 @@ fn validate_candidate_exhaustive<B: ValidationBackend, S: SpawnNamed + 'static>(
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use polkadot_node_subsystem_test_helpers as test_helpers;
|
use polkadot_node_subsystem_test_helpers as test_helpers;
|
||||||
use polkadot_primitives::v1::{HeadData, BlockData};
|
use polkadot_primitives::v1::{HeadData, BlockData, UpwardMessage};
|
||||||
use sp_core::testing::TaskExecutor;
|
use sp_core::testing::TaskExecutor;
|
||||||
use futures::executor;
|
use futures::executor;
|
||||||
use assert_matches::assert_matches;
|
use assert_matches::assert_matches;
|
||||||
@@ -847,7 +847,7 @@ mod tests {
|
|||||||
|
|
||||||
assert_matches!(v, ValidationResult::Valid(outputs, used_validation_data) => {
|
assert_matches!(v, ValidationResult::Valid(outputs, used_validation_data) => {
|
||||||
assert_eq!(outputs.head_data, HeadData(vec![1, 1, 1]));
|
assert_eq!(outputs.head_data, HeadData(vec![1, 1, 1]));
|
||||||
assert_eq!(outputs.upward_messages, Vec::new());
|
assert_eq!(outputs.upward_messages, Vec::<UpwardMessage>::new());
|
||||||
assert_eq!(outputs.new_validation_code, Some(vec![2, 2, 2].into()));
|
assert_eq!(outputs.new_validation_code, Some(vec![2, 2, 2].into()));
|
||||||
assert_eq!(used_validation_data, validation_data);
|
assert_eq!(used_validation_data, validation_data);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -186,44 +186,8 @@ impl<T: Encode + Decode + Default> AccountIdConversion<T> for Id {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Which origin a parachain's message to the relay chain should be dispatched from.
|
|
||||||
#[derive(Clone, PartialEq, Eq, Encode, Decode)]
|
|
||||||
#[cfg_attr(feature = "std", derive(Debug, Hash))]
|
|
||||||
#[repr(u8)]
|
|
||||||
pub enum ParachainDispatchOrigin {
|
|
||||||
/// As a simple `Origin::Signed`, using `ParaId::account_id` as its value. This is good when
|
|
||||||
/// interacting with standard modules such as `balances`.
|
|
||||||
Signed,
|
|
||||||
/// As the special `Origin::Parachain(ParaId)`. This is good when interacting with parachain-
|
|
||||||
/// aware modules which need to succinctly verify that the origin is a parachain.
|
|
||||||
Parachain,
|
|
||||||
/// As the simple, superuser `Origin::Root`. This can only be done on specially permissioned
|
|
||||||
/// parachains.
|
|
||||||
Root,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl sp_std::convert::TryFrom<u8> for ParachainDispatchOrigin {
|
|
||||||
type Error = ();
|
|
||||||
fn try_from(x: u8) -> core::result::Result<ParachainDispatchOrigin, ()> {
|
|
||||||
const SIGNED: u8 = ParachainDispatchOrigin::Signed as u8;
|
|
||||||
const PARACHAIN: u8 = ParachainDispatchOrigin::Parachain as u8;
|
|
||||||
Ok(match x {
|
|
||||||
SIGNED => ParachainDispatchOrigin::Signed,
|
|
||||||
PARACHAIN => ParachainDispatchOrigin::Parachain,
|
|
||||||
_ => return Err(()),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A message from a parachain to its Relay Chain.
|
/// A message from a parachain to its Relay Chain.
|
||||||
#[derive(Clone, PartialEq, Eq, Encode, Decode)]
|
pub type UpwardMessage = Vec<u8>;
|
||||||
#[cfg_attr(feature = "std", derive(Debug, Hash))]
|
|
||||||
pub struct UpwardMessage {
|
|
||||||
/// The origin for the message to be sent from.
|
|
||||||
pub origin: ParachainDispatchOrigin,
|
|
||||||
/// The message data.
|
|
||||||
pub data: Vec<u8>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Validation parameters for evaluating the parachain validity function.
|
/// Validation parameters for evaluating the parachain validity function.
|
||||||
// TODO: balance downloads (https://github.com/paritytech/polkadot/issues/220)
|
// TODO: balance downloads (https://github.com/paritytech/polkadot/issues/220)
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ pub use polkadot_core_primitives::*;
|
|||||||
pub use parity_scale_codec::Compact;
|
pub use parity_scale_codec::Compact;
|
||||||
|
|
||||||
pub use polkadot_parachain::primitives::{
|
pub use polkadot_parachain::primitives::{
|
||||||
Id, ParachainDispatchOrigin, LOWEST_USER_ID, UpwardMessage, HeadData, BlockData,
|
Id, LOWEST_USER_ID, UpwardMessage, HeadData, BlockData,
|
||||||
ValidationCode,
|
ValidationCode,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -36,8 +36,7 @@ pub use polkadot_core_primitives::v1::{
|
|||||||
|
|
||||||
// Export some polkadot-parachain primitives
|
// Export some polkadot-parachain primitives
|
||||||
pub use polkadot_parachain::primitives::{
|
pub use polkadot_parachain::primitives::{
|
||||||
Id, ParachainDispatchOrigin, LOWEST_USER_ID, UpwardMessage, HeadData, BlockData,
|
Id, LOWEST_USER_ID, UpwardMessage, HeadData, BlockData, ValidationCode,
|
||||||
ValidationCode,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Export some basic parachain primitives from v0.
|
// Export some basic parachain primitives from v0.
|
||||||
|
|||||||
@@ -26,20 +26,28 @@ The downward message queue doesn't have a cap on its size and it is up to the re
|
|||||||
that prevent spamming in place.
|
that prevent spamming in place.
|
||||||
|
|
||||||
Upward Message Passing (UMP) is a mechanism responsible for delivering messages in the opposite direction:
|
Upward Message Passing (UMP) is a mechanism responsible for delivering messages in the opposite direction:
|
||||||
from a parachain up to the relay chain. Upward messages can serve different purposes and can be of different
|
from a parachain up to the relay chain. Upward messages are essentially byte blobs. However, they are interpreted
|
||||||
kinds.
|
by the relay-chain according to the XCM standard.
|
||||||
|
|
||||||
One kind of message is `Dispatchable`. They could be thought of similarly to extrinsics sent to a relay chain: they also
|
The XCM standard is a common vocabulary of messages. The XCM standard doesn't require a particular interpretation of
|
||||||
invoke exposed runtime entrypoints, they consume weight and require fees. The difference is that they originate from
|
a message. However, the parachains host (e.g. Polkadot) guarantees certain semantics for those.
|
||||||
a parachain. Each parachain has a queue of dispatchables to be executed. There can be only so many dispatchables at a time.
|
|
||||||
|
Moreover, while most XCM messages are handled by the on-chain XCM interpreter, some of the messages are special
|
||||||
|
cased. Specifically, those messages can be checked during the acceptance criteria and thus invalid
|
||||||
|
messages would lead to rejecting the candidate itself.
|
||||||
|
|
||||||
|
One kind of such a message is `Xcm::Transact`. This upward message can be seen as a way for a parachain
|
||||||
|
to execute arbitrary entrypoints on the relay-chain. `Xcm::Transact` messages resemble regular extrinsics with the exception that they
|
||||||
|
originate from a parachain.
|
||||||
|
|
||||||
|
The payload of `Xcm::Transact` messages is referred as to `Dispatchable`. When a candidate with such a message is enacted
|
||||||
|
the dispatchables are put into a queue corresponding to the parachain. There can be only so many dispatchables in that queue at once.
|
||||||
The weight that processing of the dispatchables can consume is limited by a preconfigured value. Therefore, it is possible
|
The weight that processing of the dispatchables can consume is limited by a preconfigured value. Therefore, it is possible
|
||||||
that some dispatchables will be left for later blocks. To make the dispatching more fair, the queues are processed turn-by-turn
|
that some dispatchables will be left for later blocks. To make the dispatching more fair, the queues are processed turn-by-turn
|
||||||
in a round robin fashion.
|
in a round robin fashion.
|
||||||
|
|
||||||
Upward messages are also used by a parachain to request opening and closing HRMP channels (HRMP will be described below).
|
The second category of special cased XCM messages are for horizontal messaging channel management,
|
||||||
|
namely messages meant to request opening and closing HRMP channels (HRMP will be described below).
|
||||||
Other kinds of upward messages can be introduced in the future as well. Potential candidates are
|
|
||||||
new validation code signalling, or other requests to the relay chain.
|
|
||||||
|
|
||||||
## Horizontal Message Passing
|
## Horizontal Message Passing
|
||||||
|
|
||||||
|
|||||||
@@ -22,5 +22,5 @@ Included: Option<()>,
|
|||||||
1. Invoke `Scheduler::schedule(freed)`
|
1. Invoke `Scheduler::schedule(freed)`
|
||||||
1. Invoke the `Inclusion::process_candidates` routine with the parameters `(backed_candidates, Scheduler::scheduled(), Scheduler::group_validators)`.
|
1. Invoke the `Inclusion::process_candidates` routine with the parameters `(backed_candidates, Scheduler::scheduled(), Scheduler::group_validators)`.
|
||||||
1. Call `Scheduler::occupied` using the return value of the `Inclusion::process_candidates` call above, first sorting the list of assigned core indices.
|
1. Call `Scheduler::occupied` using the return value of the `Inclusion::process_candidates` call above, first sorting the list of assigned core indices.
|
||||||
1. Call the `Router::process_pending_upward_dispatchables` routine to execute all messages in upward dispatch queues.
|
1. Call the `Router::process_pending_upward_messages` routine to execute all messages in upward dispatch queues.
|
||||||
1. If all of the above succeeds, set `Included` to `Some(())`.
|
1. If all of the above succeeds, set `Included` to `Some(())`.
|
||||||
|
|||||||
@@ -15,20 +15,36 @@ OutgoingParas: Vec<ParaId>;
|
|||||||
### Upward Message Passing (UMP)
|
### Upward Message Passing (UMP)
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
/// Dispatchable objects ready to be dispatched onto the relay chain. The messages are processed in FIFO order.
|
/// The messages waiting to be handled by the relay-chain originating from a certain parachain.
|
||||||
RelayDispatchQueues: map ParaId => Vec<(ParachainDispatchOrigin, RawDispatchable)>;
|
///
|
||||||
|
/// Note that some upward messages might have been already processed by the inclusion logic. E.g.
|
||||||
|
/// channel management messages.
|
||||||
|
///
|
||||||
|
/// The messages are processed in FIFO order.
|
||||||
|
RelayDispatchQueues: map ParaId => Vec<UpwardMessage>;
|
||||||
/// Size of the dispatch queues. Caches sizes of the queues in `RelayDispatchQueue`.
|
/// Size of the dispatch queues. Caches sizes of the queues in `RelayDispatchQueue`.
|
||||||
|
///
|
||||||
/// First item in the tuple is the count of messages and second
|
/// First item in the tuple is the count of messages and second
|
||||||
/// is the total length (in bytes) of the message payloads.
|
/// is the total length (in bytes) of the message payloads.
|
||||||
///
|
///
|
||||||
/// Note that this is an auxilary mapping: it's possible to tell the byte size and the number of
|
/// Note that this is an auxilary mapping: it's possible to tell the byte size and the number of
|
||||||
/// messages only looking at `RelayDispatchQueues`. This mapping is separate to avoid the cost of
|
/// messages only looking at `RelayDispatchQueues`. This mapping is separate to avoid the cost of
|
||||||
/// loading the whole message queue if only the total size and count are required.
|
/// loading the whole message queue if only the total size and count are required.
|
||||||
RelayDispatchQueueSize: map ParaId => (u32, u32);
|
///
|
||||||
|
/// Invariant:
|
||||||
|
/// - The set of keys should exactly match the set of keys of `RelayDispatchQueues`.
|
||||||
|
RelayDispatchQueueSize: map ParaId => (u32, u32); // (num_messages, total_bytes)
|
||||||
/// The ordered list of `ParaId`s that have a `RelayDispatchQueue` entry.
|
/// The ordered list of `ParaId`s that have a `RelayDispatchQueue` entry.
|
||||||
|
///
|
||||||
|
/// Invariant:
|
||||||
|
/// - The set of items from this vector should be exactly the set of the keys in
|
||||||
|
/// `RelayDispatchQueues` and `RelayDispatchQueueSize`.
|
||||||
NeedsDispatch: Vec<ParaId>;
|
NeedsDispatch: Vec<ParaId>;
|
||||||
/// This is the para that will get dispatched first during the next upward dispatchable queue
|
/// This is the para that gets dispatched first during the next upward dispatchable queue
|
||||||
/// execution round.
|
/// execution round.
|
||||||
|
///
|
||||||
|
/// Invariant:
|
||||||
|
/// - If `Some(para)`, then `para` must be present in `NeedsDispatch`.
|
||||||
NextDispatchRoundStartWith: Option<ParaId>;
|
NextDispatchRoundStartWith: Option<ParaId>;
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -156,36 +172,9 @@ No initialization routine runs for this module.
|
|||||||
Candidate Acceptance Function:
|
Candidate Acceptance Function:
|
||||||
|
|
||||||
* `check_upward_messages(P: ParaId, Vec<UpwardMessage>`):
|
* `check_upward_messages(P: ParaId, Vec<UpwardMessage>`):
|
||||||
1. Checks that there are at most `config.max_upward_message_num_per_candidate` messages.
|
1. Checks that there are at most `config.max_upward_message_num_per_candidate` messages.
|
||||||
1. Checks each upward message `M` individually depending on its kind:
|
1. Checks that no message exceeds `config.max_upward_message_size`.
|
||||||
1. If the message kind is `Dispatchable`:
|
1. Verify that `RelayDispatchQueueSize` for `P` has enough capacity for the messages
|
||||||
1. Verify that `RelayDispatchQueueSize` for `P` has enough capacity for the message (NOTE that should include all processed
|
|
||||||
upward messages of the `Dispatchable` kind up to this point!)
|
|
||||||
1. If the message kind is `HrmpInitOpenChannel(recipient, max_places, max_message_size)`:
|
|
||||||
1. Check that the `P` is not `recipient`.
|
|
||||||
1. Check that `max_places` is less or equal to `config.hrmp_channel_max_places`.
|
|
||||||
1. Check that `max_message_size` is less or equal to `config.hrmp_channel_max_message_size`.
|
|
||||||
1. Check that `recipient` is a valid para.
|
|
||||||
1. Check that there is no existing channel for `(P, recipient)` in `HrmpChannels`.
|
|
||||||
1. Check that there is no existing open channel request (`P`, `recipient`) in `HrmpOpenChannelRequests`.
|
|
||||||
1. Check that the sum of the number of already opened HRMP channels by the `P` (the size
|
|
||||||
of the set found `HrmpEgressChannelsIndex` for `P`) and the number of open requests by the
|
|
||||||
`P` (the value from `HrmpOpenChannelRequestCount` for `P`) doesn't exceed the limit of
|
|
||||||
channels (`config.hrmp_max_parachain_outbound_channels` or `config.hrmp_max_parathread_outbound_channels`) minus 1.
|
|
||||||
1. Check that `P`'s balance is more or equal to `config.hrmp_sender_deposit`
|
|
||||||
1. If the message kind is `HrmpAcceptOpenChannel(sender)`:
|
|
||||||
1. Check that there is an existing request between (`sender`, `P`) in `HrmpOpenChannelRequests`
|
|
||||||
1. Check that it is not confirmed.
|
|
||||||
1. Check that `P`'s balance is more or equal to `config.hrmp_recipient_deposit`.
|
|
||||||
1. Check that the sum of the number of inbound HRMP channels opened to `P` (the size of the set
|
|
||||||
found in `HrmpIngressChannelsIndex` for `P`) and the number of accepted open requests by the `P`
|
|
||||||
(the value from `HrmpAcceptedChannelRequestCount` for `P`) doesn't exceed the limit of channels
|
|
||||||
(`config.hrmp_max_parachain_inbound_channels` or `config.hrmp_max_parathread_inbound_channels`)
|
|
||||||
minus 1.
|
|
||||||
1. If the message kind is `HrmpCloseChannel(ch)`:
|
|
||||||
1. Check that `P` is either `ch.sender` or `ch.recipient`
|
|
||||||
1. Check that `HrmpChannels` for `ch` exists.
|
|
||||||
1. Check that `ch` is not in the `HrmpCloseChannelRequests` set.
|
|
||||||
* `check_processed_downward_messages(P: ParaId, processed_downward_messages)`:
|
* `check_processed_downward_messages(P: ParaId, processed_downward_messages)`:
|
||||||
1. Checks that `DownwardMessageQueues` for `P` is at least `processed_downward_messages` long.
|
1. Checks that `DownwardMessageQueues` for `P` is at least `processed_downward_messages` long.
|
||||||
1. Checks that `processed_downward_messages` is at least 1 if `DownwardMessageQueues` for `P` is not empty.
|
1. Checks that `processed_downward_messages` is at least 1 if `DownwardMessageQueues` for `P` is not empty.
|
||||||
@@ -222,58 +211,85 @@ Candidate Enactment:
|
|||||||
* `prune_dmq(P: ParaId, processed_downward_messages)`:
|
* `prune_dmq(P: ParaId, processed_downward_messages)`:
|
||||||
1. Remove the first `processed_downward_messages` from the `DownwardMessageQueues` of `P`.
|
1. Remove the first `processed_downward_messages` from the `DownwardMessageQueues` of `P`.
|
||||||
* `enact_upward_messages(P: ParaId, Vec<UpwardMessage>)`:
|
* `enact_upward_messages(P: ParaId, Vec<UpwardMessage>)`:
|
||||||
1. Process all upward messages in order depending on their kinds:
|
1. Process each upward message `M` in order:
|
||||||
1. If the message kind is `Dispatchable`:
|
|
||||||
1. Append the message to `RelayDispatchQueues` for `P`
|
1. Append the message to `RelayDispatchQueues` for `P`
|
||||||
1. Increment the size and the count in `RelayDispatchQueueSize` for `P`.
|
1. Increment the size and the count in `RelayDispatchQueueSize` for `P`.
|
||||||
1. Ensure that `P` is present in `NeedsDispatch`.
|
1. Ensure that `P` is present in `NeedsDispatch`.
|
||||||
1. If the message kind is `HrmpInitOpenChannel(recipient, max_places, max_message_size)`:
|
|
||||||
1. Increase `HrmpOpenChannelRequestCount` by 1 for `P`.
|
|
||||||
1. Append `(P, recipient)` to `HrmpOpenChannelRequestsList`.
|
|
||||||
1. Add a new entry to `HrmpOpenChannelRequests` for `(sender, recipient)`
|
|
||||||
1. Set `sender_deposit` to `config.hrmp_sender_deposit`
|
|
||||||
1. Set `limit_used_places` to `max_places`
|
|
||||||
1. Set `limit_message_size` to `max_message_size`
|
|
||||||
1. Set `limit_used_bytes` to `config.hrmp_channel_max_size`
|
|
||||||
1. Reserve the deposit for the `P` according to `config.hrmp_sender_deposit`
|
|
||||||
1. If the message kind is `HrmpAcceptOpenChannel(sender)`:
|
|
||||||
1. Reserve the deposit for the `P` according to `config.hrmp_recipient_deposit`
|
|
||||||
1. For the request in `HrmpOpenChannelRequests` identified by `(sender, P)`, set `confirmed` flag to `true`.
|
|
||||||
1. Increase `HrmpAcceptedChannelRequestCount` by 1 for `P`.
|
|
||||||
1. If the message kind is `HrmpCloseChannel(ch)`:
|
|
||||||
1. If not already there, insert a new entry `Some(())` to `HrmpCloseChannelRequests` for `ch`
|
|
||||||
and append `ch` to `HrmpCloseChannelRequestsList`.
|
|
||||||
|
|
||||||
The following routine is intended to be called in the same time when `Paras::schedule_para_cleanup` is called.
|
The following routine is intended to be called in the same time when `Paras::schedule_para_cleanup` is called.
|
||||||
|
|
||||||
`schedule_para_cleanup(ParaId)`:
|
`schedule_para_cleanup(ParaId)`:
|
||||||
1. Add the para into the `OutgoingParas` vector maintaining the sorted order.
|
1. Add the para into the `OutgoingParas` vector maintaining the sorted order.
|
||||||
|
|
||||||
The following routine is meant to execute pending entries in upward dispatchable queues. This function doesn't fail, even if
|
The following routine is meant to execute pending entries in upward message queues. This function doesn't fail, even if
|
||||||
any of dispatchables return an error.
|
dispatcing any of individual upward messages returns an error.
|
||||||
|
|
||||||
`process_pending_upward_dispatchables()`:
|
`process_pending_upward_messages()`:
|
||||||
1. Initialize a cumulative weight counter `T` to 0
|
1. Initialize a cumulative weight counter `T` to 0
|
||||||
1. Iterate over items in `NeedsDispatch` cyclically, starting with `NextDispatchRoundStartWith`. If the item specified is `None` start from the beginning. For each `P` encountered:
|
1. Iterate over items in `NeedsDispatch` cyclically, starting with `NextDispatchRoundStartWith`. If the item specified is `None` start from the beginning. For each `P` encountered:
|
||||||
1. Dequeue `D` the first dispatchable `D` from `RelayDispatchQueues` for `P`
|
1. Dequeue the first upward message `D` from `RelayDispatchQueues` for `P`
|
||||||
1. Decrement the size of the message from `RelayDispatchQueueSize` for `P`
|
1. Decrement the size of the message from `RelayDispatchQueueSize` for `P`
|
||||||
1. Decode `D` into a dispatchable. Otherwise, if succeeded:
|
1. Delegate processing of the message to the runtime. The weight consumed is added to `T`.
|
||||||
1. If `weight_of(D) > config.dispatchable_upward_message_critical_weight` then skip the dispatchable. Otherwise:
|
1. If `T >= config.preferred_dispatchable_upward_messages_step_weight`, set `NextDispatchRoundStartWith` to `P` and finish processing.
|
||||||
1. Execute `D` and add the actual amount of weight consumed to `T`.
|
|
||||||
1. If `weight_of(D) + T > config.preferred_dispatchable_upward_messages_step_weight`, set `NextDispatchRoundStartWith` to `P` and finish processing.
|
|
||||||
> NOTE that in practice we would need to approach the weight calculation more thoroughly, i.e. incorporate all operations
|
|
||||||
> that could take place on the course of handling these dispatchables.
|
|
||||||
1. If `RelayDispatchQueues` for `P` became empty, remove `P` from `NeedsDispatch`.
|
1. If `RelayDispatchQueues` for `P` became empty, remove `P` from `NeedsDispatch`.
|
||||||
1. If `NeedsDispatch` became empty then finish processing and set `NextDispatchRoundStartWith` to `None`.
|
1. If `NeedsDispatch` became empty then finish processing and set `NextDispatchRoundStartWith` to `None`.
|
||||||
|
> NOTE that in practice we would need to approach the weight calculation more thoroughly, i.e. incorporate all operations
|
||||||
|
> that could take place on the course of handling these upward messages.
|
||||||
|
|
||||||
Utility routines.
|
Utility routines.
|
||||||
|
|
||||||
`queue_downward_message(P: ParaId, M: DownwardMessage)`:
|
`queue_downward_message(P: ParaId, M: DownwardMessage)`:
|
||||||
1. Check if the serialized size of `M` exceeds the `config.critical_downward_message_size`. If so, return an error.
|
1. Check if the size of `M` exceeds the `config.max_downward_message_size`. If so, return an error.
|
||||||
1. Wrap `M` into `InboundDownwardMessage` using the current block number for `sent_at`.
|
1. Wrap `M` into `InboundDownwardMessage` using the current block number for `sent_at`.
|
||||||
1. Obtain a new MQC link for the resulting `InboundDownwardMessage` and replace `DownwardMessageQueueHeads` for `P` with the resulting hash.
|
1. Obtain a new MQC link for the resulting `InboundDownwardMessage` and replace `DownwardMessageQueueHeads` for `P` with the resulting hash.
|
||||||
1. Add the resulting `InboundDownwardMessage` into `DownwardMessageQueues` for `P`.
|
1. Add the resulting `InboundDownwardMessage` into `DownwardMessageQueues` for `P`.
|
||||||
|
|
||||||
|
## Entry-points
|
||||||
|
|
||||||
|
The following entry-points are meant to be used for HRMP channel management.
|
||||||
|
|
||||||
|
Those entry-points are meant to be called from a parachain. `origin` is defined as the `ParaId` of
|
||||||
|
the parachain executed the message.
|
||||||
|
|
||||||
|
* `hrmp_init_open_channel(recipient, max_places, max_message_size)`:
|
||||||
|
1. Check that the `origin` is not `recipient`.
|
||||||
|
1. Check that `max_places` is less or equal to `config.hrmp_channel_max_places` and greater than zero.
|
||||||
|
1. Check that `max_message_size` is less or equal to `config.hrmp_channel_max_message_size` and greater than zero.
|
||||||
|
1. Check that `recipient` is a valid para.
|
||||||
|
1. Check that there is no existing channel for `(origin, recipient)` in `HrmpChannels`.
|
||||||
|
1. Check that there is no existing open channel request (`origin`, `recipient`) in `HrmpOpenChannelRequests`.
|
||||||
|
1. Check that the sum of the number of already opened HRMP channels by the `origin` (the size
|
||||||
|
of the set found `HrmpEgressChannelsIndex` for `origin`) and the number of open requests by the
|
||||||
|
`origin` (the value from `HrmpOpenChannelRequestCount` for `origin`) doesn't exceed the limit of
|
||||||
|
channels (`config.hrmp_max_parachain_outbound_channels` or `config.hrmp_max_parathread_outbound_channels`) minus 1.
|
||||||
|
1. Check that `origin`'s balance is more or equal to `config.hrmp_sender_deposit`
|
||||||
|
1. Reserve the deposit for the `origin` according to `config.hrmp_sender_deposit`
|
||||||
|
1. Increase `HrmpOpenChannelRequestCount` by 1 for `origin`.
|
||||||
|
1. Append `(origin, recipient)` to `HrmpOpenChannelRequestsList`.
|
||||||
|
1. Add a new entry to `HrmpOpenChannelRequests` for `(origin, recipient)`
|
||||||
|
1. Set `sender_deposit` to `config.hrmp_sender_deposit`
|
||||||
|
1. Set `limit_used_places` to `max_places`
|
||||||
|
1. Set `limit_message_size` to `max_message_size`
|
||||||
|
1. Set `limit_used_bytes` to `config.hrmp_channel_max_size`
|
||||||
|
* `hrmp_accept_open_channel(sender)`:
|
||||||
|
1. Check that there is an existing request between (`sender`, `origin`) in `HrmpOpenChannelRequests`
|
||||||
|
1. Check that it is not confirmed.
|
||||||
|
1. Check that the sum of the number of inbound HRMP channels opened to `origin` (the size of the set
|
||||||
|
found in `HrmpIngressChannelsIndex` for `origin`) and the number of accepted open requests by the `origin`
|
||||||
|
(the value from `HrmpAcceptedChannelRequestCount` for `origin`) doesn't exceed the limit of channels
|
||||||
|
(`config.hrmp_max_parachain_inbound_channels` or `config.hrmp_max_parathread_inbound_channels`)
|
||||||
|
minus 1.
|
||||||
|
1. Check that `origin`'s balance is more or equal to `config.hrmp_recipient_deposit`.
|
||||||
|
1. Reserve the deposit for the `origin` according to `config.hrmp_recipient_deposit`
|
||||||
|
1. For the request in `HrmpOpenChannelRequests` identified by `(sender, P)`, set `confirmed` flag to `true`.
|
||||||
|
1. Increase `HrmpAcceptedChannelRequestCount` by 1 for `origin`.
|
||||||
|
* `hrmp_close_channel(ch)`:
|
||||||
|
1. Check that `origin` is either `ch.sender` or `ch.recipient`
|
||||||
|
1. Check that `HrmpChannels` for `ch` exists.
|
||||||
|
1. Check that `ch` is not in the `HrmpCloseChannelRequests` set.
|
||||||
|
1. If not already there, insert a new entry `Some(())` to `HrmpCloseChannelRequests` for `ch`
|
||||||
|
and append `ch` to `HrmpCloseChannelRequestsList`.
|
||||||
|
|
||||||
## Session Change
|
## Session Change
|
||||||
|
|
||||||
1. Drain `OutgoingParas`. For each `P` happened to be in the list:
|
1. Drain `OutgoingParas`. For each `P` happened to be in the list:
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ Types required for message passing between the relay-chain and a parachain.
|
|||||||
Actual contents of the messages is specified by the XCM standard.
|
Actual contents of the messages is specified by the XCM standard.
|
||||||
|
|
||||||
```rust,ignore
|
```rust,ignore
|
||||||
|
/// A message sent from a parachain to the relay-chain.
|
||||||
|
type UpwardMessage = Vec<u8>;
|
||||||
|
|
||||||
/// A message sent from the relay-chain down to a parachain.
|
/// A message sent from the relay-chain down to a parachain.
|
||||||
///
|
///
|
||||||
/// The size of the message is limited by the `config.max_downward_message_size`
|
/// The size of the message is limited by the `config.max_downward_message_size`
|
||||||
@@ -28,6 +31,8 @@ struct InboundDownwardMessage {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Horizontal Message Passing
|
||||||
|
|
||||||
## HrmpChannelId
|
## HrmpChannelId
|
||||||
|
|
||||||
A type that uniquely identifies an HRMP channel. An HRMP channel is established between two paras.
|
A type that uniquely identifies an HRMP channel. An HRMP channel is established between two paras.
|
||||||
@@ -46,65 +51,6 @@ struct HrmpChannelId {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Upward Message
|
|
||||||
|
|
||||||
A type of messages dispatched from a parachain to the relay chain.
|
|
||||||
|
|
||||||
```rust,ignore
|
|
||||||
enum ParachainDispatchOrigin {
|
|
||||||
/// As a simple `Origin::Signed`, using `ParaId::account_id` as its value. This is good when
|
|
||||||
/// interacting with standard modules such as `balances`.
|
|
||||||
Signed,
|
|
||||||
/// As the special `Origin::Parachain(ParaId)`. This is good when interacting with parachain-
|
|
||||||
/// aware modules which need to succinctly verify that the origin is a parachain.
|
|
||||||
Parachain,
|
|
||||||
/// As the simple, superuser `Origin::Root`. This can only be done on specially permissioned
|
|
||||||
/// parachains.
|
|
||||||
Root,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// An opaque byte buffer that encodes an entrypoint and the arguments that should be
|
|
||||||
/// provided to it upon the dispatch.
|
|
||||||
///
|
|
||||||
/// NOTE In order to be executable the byte buffer should be decoded which potentially can fail if
|
|
||||||
/// the encoding was changed.
|
|
||||||
type RawDispatchable = Vec<u8>;
|
|
||||||
|
|
||||||
enum UpwardMessage {
|
|
||||||
/// This upward message is meant to schedule execution of a provided dispatchable.
|
|
||||||
Dispatchable {
|
|
||||||
/// The origin with which the dispatchable should be executed.
|
|
||||||
origin: ParachainDispatchOrigin,
|
|
||||||
/// The dispatchable to be executed in its raw form.
|
|
||||||
dispatchable: RawDispatchable,
|
|
||||||
},
|
|
||||||
/// A message for initiation of opening a new HRMP channel between the origin para and the
|
|
||||||
/// given `recipient`.
|
|
||||||
///
|
|
||||||
/// Let `origin` be the parachain that sent this upward message. In that case the channel
|
|
||||||
/// to be opened is (`origin` -> `recipient`).
|
|
||||||
HrmpInitOpenChannel {
|
|
||||||
/// The receiving party in the channel.
|
|
||||||
recipient: ParaId,
|
|
||||||
/// How many messages can be stored in the channel at most.
|
|
||||||
max_places: u32,
|
|
||||||
/// The maximum size of a message in this channel.
|
|
||||||
max_message_size: u32,
|
|
||||||
},
|
|
||||||
/// A message that is meant to confirm the HRMP open channel request initiated earlier by the
|
|
||||||
/// `HrmpInitOpenChannel` by the given `sender`.
|
|
||||||
///
|
|
||||||
/// Let `origin` be the parachain that sent this upward message. In that case the channel
|
|
||||||
/// (`origin` -> `sender`) will be opened during the session change.
|
|
||||||
HrmpAcceptOpenChannel(ParaId),
|
|
||||||
/// A message for closing the specified existing channel `ch`.
|
|
||||||
///
|
|
||||||
/// The channel to be closed is `(ch.sender -> ch.recipient)`. The parachain that sent this
|
|
||||||
/// upward message must be either `ch.sender` or `ch.recipient`.
|
|
||||||
HrmpCloseChannel(HrmpChannelId),
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Horizontal Message
|
## Horizontal Message
|
||||||
|
|
||||||
This is a message sent from a parachain to another parachain that travels through the relay chain.
|
This is a message sent from a parachain to another parachain that travels through the relay chain.
|
||||||
|
|||||||
@@ -54,16 +54,14 @@ struct HostConfiguration {
|
|||||||
/// stage.
|
/// stage.
|
||||||
///
|
///
|
||||||
/// NOTE that this is a soft limit and could be exceeded.
|
/// NOTE that this is a soft limit and could be exceeded.
|
||||||
pub preferred_dispatchable_upward_messages_step_weight: u32,
|
pub preferred_dispatchable_upward_messages_step_weight: Weight,
|
||||||
/// Any dispatchable upward message that requests more than the critical amount is rejected.
|
/// The maximum size of an upward message that can be sent by a candidate.
|
||||||
///
|
///
|
||||||
/// The parameter value is picked up so that no dispatchable can make the block weight exceed
|
/// This parameter affects the upper bound of size of `CandidateCommitments`.
|
||||||
/// the total budget. I.e. that the sum of `preferred_dispatchable_upward_messages_step_weight`
|
pub max_upward_message_size: u32,
|
||||||
/// and `dispatchable_upward_message_critical_weight` doesn't exceed the amount of weight left
|
|
||||||
/// under a typical worst case (e.g. no upgrades, etc) weight consumed by the required phases of
|
|
||||||
/// block execution (i.e. initialization, finalization and inherents).
|
|
||||||
pub dispatchable_upward_message_critical_weight: u32,
|
|
||||||
/// The maximum number of messages that a candidate can contain.
|
/// The maximum number of messages that a candidate can contain.
|
||||||
|
///
|
||||||
|
/// This parameter affects the upper bound of size of `CandidateCommitments`.
|
||||||
pub max_upward_message_num_per_candidate: u32,
|
pub max_upward_message_num_per_candidate: u32,
|
||||||
/// The maximum size of a message that can be put in a downward message queue.
|
/// The maximum size of a message that can be put in a downward message queue.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -425,7 +425,9 @@ mod tests {
|
|||||||
type WeightInfo = ();
|
type WeightInfo = ();
|
||||||
}
|
}
|
||||||
|
|
||||||
impl router::Trait for Test { }
|
impl router::Trait for Test {
|
||||||
|
type UmpSink = ();
|
||||||
|
}
|
||||||
|
|
||||||
impl pallet_session::historical::Trait for Test {
|
impl pallet_session::historical::Trait for Test {
|
||||||
type FullIdentification = pallet_staking::Exposure<u64, Balance>;
|
type FullIdentification = pallet_staking::Exposure<u64, Balance>;
|
||||||
|
|||||||
@@ -58,6 +58,12 @@ pub struct HostConfiguration<BlockNumber> {
|
|||||||
pub thread_availability_period: BlockNumber,
|
pub thread_availability_period: BlockNumber,
|
||||||
/// The amount of blocks ahead to schedule parachains and parathreads.
|
/// The amount of blocks ahead to schedule parachains and parathreads.
|
||||||
pub scheduling_lookahead: u32,
|
pub scheduling_lookahead: u32,
|
||||||
|
/// Total number of individual messages allowed in the parachain -> relay-chain message queue.
|
||||||
|
pub max_upward_queue_count: u32,
|
||||||
|
/// Total size of messages allowed in the parachain -> relay-chain message queue before which
|
||||||
|
/// no further messages may be added to it. If it exceeds this then the queue may contain only
|
||||||
|
/// a single message.
|
||||||
|
pub max_upward_queue_size: u32,
|
||||||
/// The maximum size of a message that can be put in a downward message queue.
|
/// The maximum size of a message that can be put in a downward message queue.
|
||||||
///
|
///
|
||||||
/// Since we require receiving at least one DMP message the obvious upper bound of the size is
|
/// Since we require receiving at least one DMP message the obvious upper bound of the size is
|
||||||
@@ -65,6 +71,19 @@ pub struct HostConfiguration<BlockNumber> {
|
|||||||
/// decide to do with its PoV so this value in practice will be picked as a fraction of the PoV
|
/// decide to do with its PoV so this value in practice will be picked as a fraction of the PoV
|
||||||
/// size.
|
/// size.
|
||||||
pub max_downward_message_size: u32,
|
pub max_downward_message_size: u32,
|
||||||
|
/// The amount of weight we wish to devote to the processing the dispatchable upward messages
|
||||||
|
/// stage.
|
||||||
|
///
|
||||||
|
/// NOTE that this is a soft limit and could be exceeded.
|
||||||
|
pub preferred_dispatchable_upward_messages_step_weight: Weight,
|
||||||
|
/// The maximum size of an upward message that can be sent by a candidate.
|
||||||
|
///
|
||||||
|
/// This parameter affects the size upper bound of the `CandidateCommitments`.
|
||||||
|
pub max_upward_message_size: u32,
|
||||||
|
/// The maximum number of messages that a candidate can contain.
|
||||||
|
///
|
||||||
|
/// This parameter affects the size upper bound of the `CandidateCommitments`.
|
||||||
|
pub max_upward_message_num_per_candidate: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait Trait: frame_system::Trait { }
|
pub trait Trait: frame_system::Trait { }
|
||||||
@@ -198,6 +217,26 @@ decl_module! {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets the maximum items that can present in a upward dispatch queue at once.
|
||||||
|
#[weight = (1_000, DispatchClass::Operational)]
|
||||||
|
pub fn set_max_upward_queue_count(origin, new: u32) -> DispatchResult {
|
||||||
|
ensure_root(origin)?;
|
||||||
|
Self::update_config_member(|config| {
|
||||||
|
sp_std::mem::replace(&mut config.max_upward_queue_count, new) != new
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the maximum total size of items that can present in a upward dispatch queue at once.
|
||||||
|
#[weight = (1_000, DispatchClass::Operational)]
|
||||||
|
pub fn set_max_upward_queue_size(origin, new: u32) -> DispatchResult {
|
||||||
|
ensure_root(origin)?;
|
||||||
|
Self::update_config_member(|config| {
|
||||||
|
sp_std::mem::replace(&mut config.max_upward_queue_size, new) != new
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Set the critical downward message size.
|
/// Set the critical downward message size.
|
||||||
#[weight = (1_000, DispatchClass::Operational)]
|
#[weight = (1_000, DispatchClass::Operational)]
|
||||||
pub fn set_max_downward_message_size(origin, new: u32) -> DispatchResult {
|
pub fn set_max_downward_message_size(origin, new: u32) -> DispatchResult {
|
||||||
@@ -207,6 +246,36 @@ decl_module! {
|
|||||||
});
|
});
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets the soft limit for the phase of dispatching dispatchable upward messages.
|
||||||
|
#[weight = (1_000, DispatchClass::Operational)]
|
||||||
|
pub fn set_preferred_dispatchable_upward_messages_step_weight(origin, new: Weight) -> DispatchResult {
|
||||||
|
ensure_root(origin)?;
|
||||||
|
Self::update_config_member(|config| {
|
||||||
|
sp_std::mem::replace(&mut config.preferred_dispatchable_upward_messages_step_weight, new) != new
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the maximum size of an upward message that can be sent by a candidate.
|
||||||
|
#[weight = (1_000, DispatchClass::Operational)]
|
||||||
|
pub fn set_max_upward_message_size(origin, new: u32) -> DispatchResult {
|
||||||
|
ensure_root(origin)?;
|
||||||
|
Self::update_config_member(|config| {
|
||||||
|
sp_std::mem::replace(&mut config.max_upward_message_size, new) != new
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the maximum number of messages that a candidate can contain.
|
||||||
|
#[weight = (1_000, DispatchClass::Operational)]
|
||||||
|
pub fn set_max_upward_message_num_per_candidate(origin, new: u32) -> DispatchResult {
|
||||||
|
ensure_root(origin)?;
|
||||||
|
Self::update_config_member(|config| {
|
||||||
|
sp_std::mem::replace(&mut config.max_upward_message_num_per_candidate, new) != new
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,7 +354,12 @@ mod tests {
|
|||||||
chain_availability_period: 10,
|
chain_availability_period: 10,
|
||||||
thread_availability_period: 8,
|
thread_availability_period: 8,
|
||||||
scheduling_lookahead: 3,
|
scheduling_lookahead: 3,
|
||||||
|
max_upward_queue_count: 1337,
|
||||||
|
max_upward_queue_size: 228,
|
||||||
max_downward_message_size: 2048,
|
max_downward_message_size: 2048,
|
||||||
|
preferred_dispatchable_upward_messages_step_weight: 20000,
|
||||||
|
max_upward_message_size: 448,
|
||||||
|
max_upward_message_num_per_candidate: 5,
|
||||||
};
|
};
|
||||||
|
|
||||||
assert!(<Configuration as Store>::PendingConfig::get().is_none());
|
assert!(<Configuration as Store>::PendingConfig::get().is_none());
|
||||||
@@ -323,9 +397,24 @@ mod tests {
|
|||||||
Configuration::set_scheduling_lookahead(
|
Configuration::set_scheduling_lookahead(
|
||||||
Origin::root(), new_config.scheduling_lookahead,
|
Origin::root(), new_config.scheduling_lookahead,
|
||||||
).unwrap();
|
).unwrap();
|
||||||
|
Configuration::set_max_upward_queue_count(
|
||||||
|
Origin::root(), new_config.max_upward_queue_count,
|
||||||
|
).unwrap();
|
||||||
|
Configuration::set_max_upward_queue_size(
|
||||||
|
Origin::root(), new_config.max_upward_queue_size,
|
||||||
|
).unwrap();
|
||||||
Configuration::set_max_downward_message_size(
|
Configuration::set_max_downward_message_size(
|
||||||
Origin::root(), new_config.max_downward_message_size,
|
Origin::root(), new_config.max_downward_message_size,
|
||||||
).unwrap();
|
).unwrap();
|
||||||
|
Configuration::set_preferred_dispatchable_upward_messages_step_weight(
|
||||||
|
Origin::root(), new_config.preferred_dispatchable_upward_messages_step_weight,
|
||||||
|
).unwrap();
|
||||||
|
Configuration::set_max_upward_message_size(
|
||||||
|
Origin::root(), new_config.max_upward_message_size,
|
||||||
|
).unwrap();
|
||||||
|
Configuration::set_max_upward_message_num_per_candidate(
|
||||||
|
Origin::root(), new_config.max_upward_message_num_per_candidate,
|
||||||
|
).unwrap();
|
||||||
|
|
||||||
assert_eq!(<Configuration as Store>::PendingConfig::get(), Some(new_config));
|
assert_eq!(<Configuration as Store>::PendingConfig::get(), Some(new_config));
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -155,6 +155,8 @@ decl_error! {
|
|||||||
InternalError,
|
InternalError,
|
||||||
/// The downward message queue is not processed correctly.
|
/// The downward message queue is not processed correctly.
|
||||||
IncorrectDownwardMessageHandling,
|
IncorrectDownwardMessageHandling,
|
||||||
|
/// At least one upward message sent does not pass the acceptance criteria.
|
||||||
|
InvalidUpwardMessages,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -412,6 +414,7 @@ impl<T: Trait> Module<T> {
|
|||||||
&candidate.candidate.commitments.head_data,
|
&candidate.candidate.commitments.head_data,
|
||||||
&candidate.candidate.commitments.new_validation_code,
|
&candidate.candidate.commitments.new_validation_code,
|
||||||
candidate.candidate.commitments.processed_downward_messages,
|
candidate.candidate.commitments.processed_downward_messages,
|
||||||
|
&candidate.candidate.commitments.upward_messages,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
for (i, assignment) in scheduled[skip..].iter().enumerate() {
|
for (i, assignment) in scheduled[skip..].iter().enumerate() {
|
||||||
@@ -544,6 +547,7 @@ impl<T: Trait> Module<T> {
|
|||||||
&validation_outputs.head_data,
|
&validation_outputs.head_data,
|
||||||
&validation_outputs.new_validation_code,
|
&validation_outputs.new_validation_code,
|
||||||
validation_outputs.processed_downward_messages,
|
validation_outputs.processed_downward_messages,
|
||||||
|
&validation_outputs.upward_messages,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -570,6 +574,10 @@ impl<T: Trait> Module<T> {
|
|||||||
receipt.descriptor.para_id,
|
receipt.descriptor.para_id,
|
||||||
commitments.processed_downward_messages,
|
commitments.processed_downward_messages,
|
||||||
);
|
);
|
||||||
|
weight += <router::Module<T>>::enact_upward_messages(
|
||||||
|
receipt.descriptor.para_id,
|
||||||
|
commitments.upward_messages,
|
||||||
|
);
|
||||||
|
|
||||||
Self::deposit_event(
|
Self::deposit_event(
|
||||||
Event::<T>::CandidateIncluded(plain, commitments.head_data.clone())
|
Event::<T>::CandidateIncluded(plain, commitments.head_data.clone())
|
||||||
@@ -693,6 +701,7 @@ impl<T: Trait> CandidateCheckContext<T> {
|
|||||||
head_data: &HeadData,
|
head_data: &HeadData,
|
||||||
new_validation_code: &Option<primitives::v1::ValidationCode>,
|
new_validation_code: &Option<primitives::v1::ValidationCode>,
|
||||||
processed_downward_messages: u32,
|
processed_downward_messages: u32,
|
||||||
|
upward_messages: &[primitives::v1::UpwardMessage],
|
||||||
) -> Result<(), DispatchError> {
|
) -> Result<(), DispatchError> {
|
||||||
ensure!(
|
ensure!(
|
||||||
head_data.0.len() <= self.config.max_head_data_size as _,
|
head_data.0.len() <= self.config.max_head_data_size as _,
|
||||||
@@ -722,6 +731,14 @@ impl<T: Trait> CandidateCheckContext<T> {
|
|||||||
),
|
),
|
||||||
Error::<T>::IncorrectDownwardMessageHandling,
|
Error::<T>::IncorrectDownwardMessageHandling,
|
||||||
);
|
);
|
||||||
|
ensure!(
|
||||||
|
<router::Module<T>>::check_upward_messages(
|
||||||
|
&self.config,
|
||||||
|
para_id,
|
||||||
|
upward_messages,
|
||||||
|
),
|
||||||
|
Error::<T>::InvalidUpwardMessages,
|
||||||
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ use frame_system::ensure_none;
|
|||||||
use crate::{
|
use crate::{
|
||||||
inclusion,
|
inclusion,
|
||||||
scheduler::{self, FreedReason},
|
scheduler::{self, FreedReason},
|
||||||
|
router,
|
||||||
};
|
};
|
||||||
use inherents::{InherentIdentifier, InherentData, MakeFatalError, ProvideInherent};
|
use inherents::{InherentIdentifier, InherentData, MakeFatalError, ProvideInherent};
|
||||||
|
|
||||||
@@ -115,6 +116,9 @@ decl_module! {
|
|||||||
// Note which of the scheduled cores were actually occupied by a backed candidate.
|
// Note which of the scheduled cores were actually occupied by a backed candidate.
|
||||||
<scheduler::Module<T>>::occupied(&occupied);
|
<scheduler::Module<T>>::occupied(&occupied);
|
||||||
|
|
||||||
|
// Give some time slice to dispatch pending upward messages.
|
||||||
|
<router::Module<T>>::process_pending_upward_messages();
|
||||||
|
|
||||||
// And track that we've finished processing the inherent for this block.
|
// And track that we've finished processing the inherent for this block.
|
||||||
Included::set(Some(()));
|
Included::set(Some(()));
|
||||||
|
|
||||||
|
|||||||
@@ -108,7 +108,9 @@ impl crate::paras::Trait for Test {
|
|||||||
type Origin = Origin;
|
type Origin = Origin;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl crate::router::Trait for Test { }
|
impl crate::router::Trait for Test {
|
||||||
|
type UmpSink = crate::router::MockUmpSink;
|
||||||
|
}
|
||||||
|
|
||||||
impl crate::scheduler::Trait for Test { }
|
impl crate::scheduler::Trait for Test { }
|
||||||
|
|
||||||
|
|||||||
@@ -26,13 +26,22 @@ use crate::{
|
|||||||
};
|
};
|
||||||
use sp_std::prelude::*;
|
use sp_std::prelude::*;
|
||||||
use frame_support::{decl_error, decl_module, decl_storage, weights::Weight};
|
use frame_support::{decl_error, decl_module, decl_storage, weights::Weight};
|
||||||
use primitives::v1::{Id as ParaId, InboundDownwardMessage, Hash};
|
use sp_std::collections::vec_deque::VecDeque;
|
||||||
|
use primitives::v1::{Id as ParaId, InboundDownwardMessage, Hash, UpwardMessage};
|
||||||
|
|
||||||
mod dmp;
|
mod dmp;
|
||||||
|
mod ump;
|
||||||
|
|
||||||
pub use dmp::QueueDownwardMessageError;
|
pub use dmp::QueueDownwardMessageError;
|
||||||
|
pub use ump::UmpSink;
|
||||||
|
|
||||||
pub trait Trait: frame_system::Trait + configuration::Trait {}
|
#[cfg(test)]
|
||||||
|
pub use ump::mock_sink::MockUmpSink;
|
||||||
|
|
||||||
|
pub trait Trait: frame_system::Trait + configuration::Trait {
|
||||||
|
/// A place where all received upward messages are funneled.
|
||||||
|
type UmpSink: UmpSink;
|
||||||
|
}
|
||||||
|
|
||||||
decl_storage! {
|
decl_storage! {
|
||||||
trait Store for Module<T: Trait> as Router {
|
trait Store for Module<T: Trait> as Router {
|
||||||
@@ -56,6 +65,44 @@ decl_storage! {
|
|||||||
/// - `B`: is the relay-chain block number in which a message was appended.
|
/// - `B`: is the relay-chain block number in which a message was appended.
|
||||||
/// - `H(M)`: is the hash of the message being appended.
|
/// - `H(M)`: is the hash of the message being appended.
|
||||||
DownwardMessageQueueHeads: map hasher(twox_64_concat) ParaId => Hash;
|
DownwardMessageQueueHeads: map hasher(twox_64_concat) ParaId => Hash;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Upward Message Passing (UMP)
|
||||||
|
*
|
||||||
|
* Storage layout required for UMP, specifically dispatchable upward messages.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// The messages waiting to be handled by the relay-chain originating from a certain parachain.
|
||||||
|
///
|
||||||
|
/// Note that some upward messages might have been already processed by the inclusion logic. E.g.
|
||||||
|
/// channel management messages.
|
||||||
|
///
|
||||||
|
/// The messages are processed in FIFO order.
|
||||||
|
RelayDispatchQueues: map hasher(twox_64_concat) ParaId => VecDeque<UpwardMessage>;
|
||||||
|
/// Size of the dispatch queues. Caches sizes of the queues in `RelayDispatchQueue`.
|
||||||
|
///
|
||||||
|
/// First item in the tuple is the count of messages and second
|
||||||
|
/// is the total length (in bytes) of the message payloads.
|
||||||
|
///
|
||||||
|
/// Note that this is an auxilary mapping: it's possible to tell the byte size and the number of
|
||||||
|
/// messages only looking at `RelayDispatchQueues`. This mapping is separate to avoid the cost of
|
||||||
|
/// loading the whole message queue if only the total size and count are required.
|
||||||
|
///
|
||||||
|
/// Invariant:
|
||||||
|
/// - The set of keys should exactly match the set of keys of `RelayDispatchQueues`.
|
||||||
|
RelayDispatchQueueSize: map hasher(twox_64_concat) ParaId => (u32, u32);
|
||||||
|
/// The ordered list of `ParaId`s that have a `RelayDispatchQueue` entry.
|
||||||
|
///
|
||||||
|
/// Invariant:
|
||||||
|
/// - The set of items from this vector should be exactly the set of the keys in
|
||||||
|
/// `RelayDispatchQueues` and `RelayDispatchQueueSize`.
|
||||||
|
NeedsDispatch: Vec<ParaId>;
|
||||||
|
/// This is the para that gets will get dispatched first during the next upward dispatchable queue
|
||||||
|
/// execution round.
|
||||||
|
///
|
||||||
|
/// Invariant:
|
||||||
|
/// - If `Some(para)`, then `para` must be present in `NeedsDispatch`.
|
||||||
|
NextDispatchRoundStartWith: Option<ParaId>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,6 +133,7 @@ impl<T: Trait> Module<T> {
|
|||||||
let outgoing = OutgoingParas::take();
|
let outgoing = OutgoingParas::take();
|
||||||
for outgoing_para in outgoing {
|
for outgoing_para in outgoing {
|
||||||
Self::clean_dmp_after_outgoing(outgoing_para);
|
Self::clean_dmp_after_outgoing(outgoing_para);
|
||||||
|
Self::clean_ump_after_outgoing(outgoing_para);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,712 @@
|
|||||||
|
// Copyright 2020 Parity Technologies (UK) Ltd.
|
||||||
|
// This file is part of Polkadot.
|
||||||
|
|
||||||
|
// Polkadot 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.
|
||||||
|
|
||||||
|
// Polkadot 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/>.
|
||||||
|
|
||||||
|
use super::{Trait, Module, Store};
|
||||||
|
use crate::configuration::{self, HostConfiguration};
|
||||||
|
use sp_std::prelude::*;
|
||||||
|
use sp_std::collections::{btree_map::BTreeMap, vec_deque::VecDeque};
|
||||||
|
use frame_support::{StorageMap, StorageValue, weights::Weight, traits::Get};
|
||||||
|
use primitives::v1::{Id as ParaId, UpwardMessage};
|
||||||
|
|
||||||
|
/// All upward messages coming from parachains will be funneled into an implementation of this trait.
|
||||||
|
///
|
||||||
|
/// The message is opaque from the perspective of UMP. The message size can range from 0 to
|
||||||
|
/// `config.max_upward_message_size`.
|
||||||
|
///
|
||||||
|
/// It's up to the implementation of this trait to decide what to do with a message as long as it
|
||||||
|
/// returns the amount of weight consumed in the process of handling. Ignoring a message is a valid
|
||||||
|
/// strategy.
|
||||||
|
///
|
||||||
|
/// There are no guarantees on how much time it takes for the message sent by a candidate to end up
|
||||||
|
/// in the sink after the candidate was enacted. That typically depends on the UMP traffic, the sizes
|
||||||
|
/// of upward messages and the configuration of UMP.
|
||||||
|
///
|
||||||
|
/// It is possible that by the time the message is sank the origin parachain was offboarded. It is
|
||||||
|
/// up to the implementer to check that if it cares.
|
||||||
|
pub trait UmpSink {
|
||||||
|
/// Process an incoming upward message and return the amount of weight it consumed.
|
||||||
|
///
|
||||||
|
/// See the trait docs for more details.
|
||||||
|
fn process_upward_message(origin: ParaId, msg: Vec<u8>) -> Weight;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An implementation of a sink that just swallows the message without consuming any weight.
|
||||||
|
impl UmpSink for () {
|
||||||
|
fn process_upward_message(_: ParaId, _: Vec<u8>) -> Weight {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Routines related to the upward message passing.
|
||||||
|
impl<T: Trait> Module<T> {
|
||||||
|
pub(super) fn clean_ump_after_outgoing(outgoing_para: ParaId) {
|
||||||
|
<Self as Store>::RelayDispatchQueueSize::remove(&outgoing_para);
|
||||||
|
<Self as Store>::RelayDispatchQueues::remove(&outgoing_para);
|
||||||
|
|
||||||
|
// Remove the outgoing para from the `NeedsDispatch` list and from
|
||||||
|
// `NextDispatchRoundStartWith`.
|
||||||
|
//
|
||||||
|
// That's needed for maintaining invariant that `NextDispatchRoundStartWith` points to an
|
||||||
|
// existing item in `NeedsDispatch`.
|
||||||
|
<Self as Store>::NeedsDispatch::mutate(|v| {
|
||||||
|
if let Ok(i) = v.binary_search(&outgoing_para) {
|
||||||
|
v.remove(i);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
<Self as Store>::NextDispatchRoundStartWith::mutate(|v| {
|
||||||
|
*v = v.filter(|p| *p == outgoing_para)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check that all the upward messages sent by a candidate pass the acceptance criteria. Returns
|
||||||
|
/// false, if any of the messages doesn't pass.
|
||||||
|
pub(crate) fn check_upward_messages(
|
||||||
|
config: &HostConfiguration<T::BlockNumber>,
|
||||||
|
para: ParaId,
|
||||||
|
upward_messages: &[UpwardMessage],
|
||||||
|
) -> bool {
|
||||||
|
if upward_messages.len() as u32 > config.max_upward_message_num_per_candidate {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (mut para_queue_count, mut para_queue_size) =
|
||||||
|
<Self as Store>::RelayDispatchQueueSize::get(¶);
|
||||||
|
|
||||||
|
for msg in upward_messages {
|
||||||
|
let msg_size = msg.len() as u32;
|
||||||
|
if msg_size > config.max_upward_message_size {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
para_queue_count += 1;
|
||||||
|
para_queue_size += msg_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure that the queue is not overfilled.
|
||||||
|
// we do it here only once since returning false invalidates the whole relay-chain block.
|
||||||
|
para_queue_count <= config.max_upward_queue_count
|
||||||
|
&& para_queue_size <= config.max_upward_queue_size
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enacts all the upward messages sent by a candidate.
|
||||||
|
pub(crate) fn enact_upward_messages(
|
||||||
|
para: ParaId,
|
||||||
|
upward_messages: Vec<UpwardMessage>,
|
||||||
|
) -> Weight {
|
||||||
|
let mut weight = 0;
|
||||||
|
|
||||||
|
if !upward_messages.is_empty() {
|
||||||
|
let (extra_cnt, extra_size) = upward_messages
|
||||||
|
.iter()
|
||||||
|
.fold((0, 0), |(cnt, size), d| (cnt + 1, size + d.len() as u32));
|
||||||
|
|
||||||
|
<Self as Store>::RelayDispatchQueues::mutate(¶, |v| {
|
||||||
|
v.extend(upward_messages.into_iter())
|
||||||
|
});
|
||||||
|
|
||||||
|
<Self as Store>::RelayDispatchQueueSize::mutate(
|
||||||
|
¶,
|
||||||
|
|(ref mut cnt, ref mut size)| {
|
||||||
|
*cnt += extra_cnt;
|
||||||
|
*size += extra_size;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
<Self as Store>::NeedsDispatch::mutate(|v| {
|
||||||
|
if let Err(i) = v.binary_search(¶) {
|
||||||
|
v.insert(i, para);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
weight += T::DbWeight::get().reads_writes(3, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
weight
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Devote some time into dispatching pending upward messages.
|
||||||
|
pub(crate) fn process_pending_upward_messages() {
|
||||||
|
let mut used_weight_so_far = 0;
|
||||||
|
|
||||||
|
let config = <configuration::Module<T>>::config();
|
||||||
|
let mut cursor = NeedsDispatchCursor::new::<T>();
|
||||||
|
let mut queue_cache = QueueCache::new();
|
||||||
|
|
||||||
|
while let Some(dispatchee) = cursor.peek() {
|
||||||
|
if used_weight_so_far >= config.preferred_dispatchable_upward_messages_step_weight {
|
||||||
|
// Then check whether we've reached or overshoot the
|
||||||
|
// preferred weight for the dispatching stage.
|
||||||
|
//
|
||||||
|
// if so - bail.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// dequeue the next message from the queue of the dispatchee
|
||||||
|
let (upward_message, became_empty) = queue_cache.dequeue::<T>(dispatchee);
|
||||||
|
if let Some(upward_message) = upward_message {
|
||||||
|
used_weight_so_far +=
|
||||||
|
T::UmpSink::process_upward_message(dispatchee, upward_message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if became_empty {
|
||||||
|
// the queue is empty now - this para doesn't need attention anymore.
|
||||||
|
cursor.remove();
|
||||||
|
} else {
|
||||||
|
cursor.advance();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor.flush::<T>();
|
||||||
|
queue_cache.flush::<T>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// To avoid constant fetching, deserializing and serialization the queues are cached.
|
||||||
|
///
|
||||||
|
/// After an item dequeued from a queue for the first time, the queue is stored in this struct rather
|
||||||
|
/// than being serialized and persisted.
|
||||||
|
///
|
||||||
|
/// This implementation works best when:
|
||||||
|
///
|
||||||
|
/// 1. when the queues are shallow
|
||||||
|
/// 2. the dispatcher makes more than one cycle
|
||||||
|
///
|
||||||
|
/// if the queues are deep and there are many we would load and keep the queues for a long time,
|
||||||
|
/// thus increasing the peak memory consumption of the wasm runtime. Under such conditions persisting
|
||||||
|
/// queues might play better since it's unlikely that they are going to be requested once more.
|
||||||
|
///
|
||||||
|
/// On the other hand, the situation when deep queues exist and it takes more than one dipsatcher
|
||||||
|
/// cycle to traverse the queues is already sub-optimal and better be avoided.
|
||||||
|
///
|
||||||
|
/// This struct is not supposed to be dropped but rather to be consumed by [`flush`].
|
||||||
|
struct QueueCache(BTreeMap<ParaId, QueueCacheEntry>);
|
||||||
|
|
||||||
|
struct QueueCacheEntry {
|
||||||
|
queue: VecDeque<UpwardMessage>,
|
||||||
|
count: u32,
|
||||||
|
total_size: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl QueueCache {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self(BTreeMap::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dequeues one item from the upward message queue of the given para.
|
||||||
|
///
|
||||||
|
/// Returns `(upward_message, became_empty)`, where
|
||||||
|
///
|
||||||
|
/// - `upward_message` a dequeued message or `None` if the queue _was_ empty.
|
||||||
|
/// - `became_empty` is true if the queue _became_ empty.
|
||||||
|
fn dequeue<T: Trait>(&mut self, para: ParaId) -> (Option<UpwardMessage>, bool) {
|
||||||
|
let cache_entry = self.0.entry(para).or_insert_with(|| {
|
||||||
|
let queue = <Module<T> as Store>::RelayDispatchQueues::get(¶);
|
||||||
|
let (count, total_size) = <Module<T> as Store>::RelayDispatchQueueSize::get(¶);
|
||||||
|
QueueCacheEntry {
|
||||||
|
queue,
|
||||||
|
count,
|
||||||
|
total_size,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let upward_message = cache_entry.queue.pop_front();
|
||||||
|
if let Some(ref msg) = upward_message {
|
||||||
|
cache_entry.count -= 1;
|
||||||
|
cache_entry.total_size -= msg.len() as u32;
|
||||||
|
}
|
||||||
|
|
||||||
|
let became_empty = cache_entry.queue.is_empty();
|
||||||
|
(upward_message, became_empty)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Flushes the updated queues into the storage.
|
||||||
|
fn flush<T: Trait>(self) {
|
||||||
|
// NOTE we use an explicit method here instead of Drop impl because it has unwanted semantics
|
||||||
|
// within runtime. It is dangerous to use because of double-panics and flushing on a panic
|
||||||
|
// is not necessary as well.
|
||||||
|
for (
|
||||||
|
para,
|
||||||
|
QueueCacheEntry {
|
||||||
|
queue,
|
||||||
|
count,
|
||||||
|
total_size,
|
||||||
|
},
|
||||||
|
) in self.0
|
||||||
|
{
|
||||||
|
if queue.is_empty() {
|
||||||
|
// remove the entries altogether.
|
||||||
|
<Module<T> as Store>::RelayDispatchQueues::remove(¶);
|
||||||
|
<Module<T> as Store>::RelayDispatchQueueSize::remove(¶);
|
||||||
|
} else {
|
||||||
|
<Module<T> as Store>::RelayDispatchQueues::insert(¶, queue);
|
||||||
|
<Module<T> as Store>::RelayDispatchQueueSize::insert(¶, (count, total_size));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A cursor that iterates over all entries in `NeedsDispatch`.
|
||||||
|
///
|
||||||
|
/// This cursor will start with the para indicated by `NextDispatchRoundStartWith` storage entry.
|
||||||
|
/// This cursor is cyclic meaning that after reaching the end it will jump to the beginning. Unlike
|
||||||
|
/// an iterator, this cursor allows removing items during the iteration.
|
||||||
|
///
|
||||||
|
/// Each iteration cycle *must be* concluded with a call to either `advance` or `remove`.
|
||||||
|
///
|
||||||
|
/// This struct is not supposed to be dropped but rather to be consumed by [`flush`].
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct NeedsDispatchCursor {
|
||||||
|
needs_dispatch: Vec<ParaId>,
|
||||||
|
cur_idx: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NeedsDispatchCursor {
|
||||||
|
fn new<T: Trait>() -> Self {
|
||||||
|
let needs_dispatch: Vec<ParaId> = <Module<T> as Store>::NeedsDispatch::get();
|
||||||
|
let start_with = <Module<T> as Store>::NextDispatchRoundStartWith::get();
|
||||||
|
|
||||||
|
let start_with_idx = match start_with {
|
||||||
|
Some(para) => match needs_dispatch.binary_search(¶) {
|
||||||
|
Ok(found_idx) => found_idx,
|
||||||
|
Err(_supposed_idx) => {
|
||||||
|
// well that's weird because we maintain an invariant that
|
||||||
|
// `NextDispatchRoundStartWith` must point into one of the items in
|
||||||
|
// `NeedsDispatch`.
|
||||||
|
//
|
||||||
|
// let's select 0 as the starting index as a safe bet.
|
||||||
|
debug_assert!(false);
|
||||||
|
0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
Self {
|
||||||
|
needs_dispatch,
|
||||||
|
cur_idx: start_with_idx,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the item the cursor points to.
|
||||||
|
fn peek(&self) -> Option<ParaId> {
|
||||||
|
self.needs_dispatch.get(self.cur_idx).cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Moves the cursor to the next item.
|
||||||
|
fn advance(&mut self) {
|
||||||
|
if self.needs_dispatch.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.cur_idx = (self.cur_idx + 1) % self.needs_dispatch.len();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes the item under the cursor.
|
||||||
|
fn remove(&mut self) {
|
||||||
|
if self.needs_dispatch.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let _ = self.needs_dispatch.remove(self.cur_idx);
|
||||||
|
|
||||||
|
// we might've removed the last element and that doesn't necessarily mean that `needs_dispatch`
|
||||||
|
// became empty. Reposition the cursor in this case to the beginning.
|
||||||
|
if self.needs_dispatch.get(self.cur_idx).is_none() {
|
||||||
|
self.cur_idx = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Flushes the dispatcher state into the persistent storage.
|
||||||
|
fn flush<T: Trait>(self) {
|
||||||
|
let next_one = self.peek();
|
||||||
|
<Module<T> as Store>::NextDispatchRoundStartWith::set(next_one);
|
||||||
|
<Module<T> as Store>::NeedsDispatch::put(self.needs_dispatch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) mod mock_sink {
|
||||||
|
//! An implementation of a mock UMP sink that allows attaching a probe for mocking the weights
|
||||||
|
//! and checking the sent messages.
|
||||||
|
//!
|
||||||
|
//! A default behavior of the UMP sink is to ignore an incoming message and return 0 weight.
|
||||||
|
//!
|
||||||
|
//! A probe can be attached to the mock UMP sink. When attached, the mock sink would consult the
|
||||||
|
//! probe to check whether the received message was expected and what weight it should return.
|
||||||
|
//!
|
||||||
|
//! There are two rules on how to use a probe:
|
||||||
|
//!
|
||||||
|
//! 1. There can be only one active probe at a time. Creation of another probe while there is
|
||||||
|
//! already an active one leads to a panic. The probe is scoped to a thread where it was created.
|
||||||
|
//!
|
||||||
|
//! 2. All messages expected by the probe must be received by the time of dropping it. Unreceived
|
||||||
|
//! messages will lead to a panic while dropping a probe.
|
||||||
|
|
||||||
|
use super::{UmpSink, UpwardMessage, ParaId};
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use std::collections::vec_deque::VecDeque;
|
||||||
|
use frame_support::weights::Weight;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct UmpExpectation {
|
||||||
|
expected_origin: ParaId,
|
||||||
|
expected_msg: UpwardMessage,
|
||||||
|
mock_weight: Weight,
|
||||||
|
}
|
||||||
|
|
||||||
|
std::thread_local! {
|
||||||
|
// `Some` here indicates that there is an active probe.
|
||||||
|
static HOOK: RefCell<Option<VecDeque<UmpExpectation>>> = RefCell::new(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MockUmpSink;
|
||||||
|
impl UmpSink for MockUmpSink {
|
||||||
|
fn process_upward_message(actual_origin: ParaId, actual_msg: Vec<u8>) -> Weight {
|
||||||
|
HOOK.with(|opt_hook| match &mut *opt_hook.borrow_mut() {
|
||||||
|
Some(hook) => {
|
||||||
|
let UmpExpectation {
|
||||||
|
expected_origin,
|
||||||
|
expected_msg,
|
||||||
|
mock_weight,
|
||||||
|
} = match hook.pop_front() {
|
||||||
|
Some(expectation) => expectation,
|
||||||
|
None => {
|
||||||
|
panic!(
|
||||||
|
"The probe is active but didn't expect the message:\n\n\t{:?}.",
|
||||||
|
actual_msg,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
assert_eq!(expected_origin, actual_origin);
|
||||||
|
assert_eq!(expected_msg, actual_msg);
|
||||||
|
mock_weight
|
||||||
|
}
|
||||||
|
None => 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Probe {
|
||||||
|
_private: (),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Probe {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
HOOK.with(|opt_hook| {
|
||||||
|
let prev = opt_hook.borrow_mut().replace(VecDeque::default());
|
||||||
|
|
||||||
|
// that can trigger if there were two probes were created during one session which
|
||||||
|
// is may be a bit strict, but may save time figuring out what's wrong.
|
||||||
|
// if you land here and you do need the two probes in one session consider
|
||||||
|
// dropping the the existing probe explicitly.
|
||||||
|
assert!(prev.is_none());
|
||||||
|
});
|
||||||
|
Self { _private: () }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add an expected message.
|
||||||
|
///
|
||||||
|
/// The enqueued messages are processed in FIFO order.
|
||||||
|
pub fn assert_msg(
|
||||||
|
&mut self,
|
||||||
|
expected_origin: ParaId,
|
||||||
|
expected_msg: UpwardMessage,
|
||||||
|
mock_weight: Weight,
|
||||||
|
) {
|
||||||
|
HOOK.with(|opt_hook| {
|
||||||
|
opt_hook
|
||||||
|
.borrow_mut()
|
||||||
|
.as_mut()
|
||||||
|
.unwrap()
|
||||||
|
.push_back(UmpExpectation {
|
||||||
|
expected_origin,
|
||||||
|
expected_msg,
|
||||||
|
mock_weight,
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for Probe {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let _ = HOOK.try_with(|opt_hook| {
|
||||||
|
let prev = opt_hook.borrow_mut().take().expect(
|
||||||
|
"this probe was created and hasn't been yet destroyed;
|
||||||
|
the probe cannot be replaced;
|
||||||
|
there is only one probe at a time allowed;
|
||||||
|
thus it cannot be `None`;
|
||||||
|
qed",
|
||||||
|
);
|
||||||
|
|
||||||
|
if !prev.is_empty() {
|
||||||
|
// some messages are left unchecked. We should notify the developer about this.
|
||||||
|
// however, we do so only if the thread doesn't panic already. Otherwise, the
|
||||||
|
// developer would get a SIGILL or SIGABRT without a meaningful error message.
|
||||||
|
if !std::thread::panicking() {
|
||||||
|
panic!(
|
||||||
|
"the probe is dropped and not all expected messages arrived: {:?}",
|
||||||
|
prev
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// an `Err` here signals here that the thread local was already destroyed.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use super::mock_sink::Probe;
|
||||||
|
use crate::router::tests::default_genesis_config;
|
||||||
|
use crate::mock::{Configuration, Router, new_test_ext};
|
||||||
|
use frame_support::IterableStorageMap;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
struct GenesisConfigBuilder {
|
||||||
|
max_upward_message_size: u32,
|
||||||
|
max_upward_message_num_per_candidate: u32,
|
||||||
|
max_upward_queue_count: u32,
|
||||||
|
max_upward_queue_size: u32,
|
||||||
|
preferred_dispatchable_upward_messages_step_weight: Weight,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for GenesisConfigBuilder {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
max_upward_message_size: 16,
|
||||||
|
max_upward_message_num_per_candidate: 2,
|
||||||
|
max_upward_queue_count: 4,
|
||||||
|
max_upward_queue_size: 64,
|
||||||
|
preferred_dispatchable_upward_messages_step_weight: 1000,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GenesisConfigBuilder {
|
||||||
|
fn build(self) -> crate::mock::GenesisConfig {
|
||||||
|
let mut genesis = default_genesis_config();
|
||||||
|
let config = &mut genesis.configuration.config;
|
||||||
|
|
||||||
|
config.max_upward_message_size = self.max_upward_message_size;
|
||||||
|
config.max_upward_message_num_per_candidate = self.max_upward_message_num_per_candidate;
|
||||||
|
config.max_upward_queue_count = self.max_upward_queue_count;
|
||||||
|
config.max_upward_queue_size = self.max_upward_queue_size;
|
||||||
|
config.preferred_dispatchable_upward_messages_step_weight =
|
||||||
|
self.preferred_dispatchable_upward_messages_step_weight;
|
||||||
|
genesis
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn queue_upward_msg(para: ParaId, msg: UpwardMessage) {
|
||||||
|
let msgs = vec![msg];
|
||||||
|
assert!(Router::check_upward_messages(
|
||||||
|
&Configuration::config(),
|
||||||
|
para,
|
||||||
|
&msgs,
|
||||||
|
));
|
||||||
|
let _ = Router::enact_upward_messages(para, msgs);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assert_storage_consistency_exhaustive() {
|
||||||
|
// check that empty queues don't clutter the storage.
|
||||||
|
for (_para, queue) in <Router as Store>::RelayDispatchQueues::iter() {
|
||||||
|
assert!(!queue.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
// actually count the counts and sizes in queues and compare them to the bookkeeped version.
|
||||||
|
for (para, queue) in <Router as Store>::RelayDispatchQueues::iter() {
|
||||||
|
let (expected_count, expected_size) =
|
||||||
|
<Router as Store>::RelayDispatchQueueSize::get(para);
|
||||||
|
let (actual_count, actual_size) =
|
||||||
|
queue.into_iter().fold((0, 0), |(acc_count, acc_size), x| {
|
||||||
|
(acc_count + 1, acc_size + x.len() as u32)
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(expected_count, actual_count);
|
||||||
|
assert_eq!(expected_size, actual_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
// since we wipe the empty queues the sets of paras in queue contents, queue sizes and
|
||||||
|
// need dispatch set should all be equal.
|
||||||
|
let queue_contents_set = <Router as Store>::RelayDispatchQueues::iter()
|
||||||
|
.map(|(k, _)| k)
|
||||||
|
.collect::<HashSet<ParaId>>();
|
||||||
|
let queue_sizes_set = <Router as Store>::RelayDispatchQueueSize::iter()
|
||||||
|
.map(|(k, _)| k)
|
||||||
|
.collect::<HashSet<ParaId>>();
|
||||||
|
let needs_dispatch_set = <Router as Store>::NeedsDispatch::get()
|
||||||
|
.into_iter()
|
||||||
|
.collect::<HashSet<ParaId>>();
|
||||||
|
assert_eq!(queue_contents_set, queue_sizes_set);
|
||||||
|
assert_eq!(queue_contents_set, needs_dispatch_set);
|
||||||
|
|
||||||
|
// `NextDispatchRoundStartWith` should point into a para that is tracked.
|
||||||
|
if let Some(para) = <Router as Store>::NextDispatchRoundStartWith::get() {
|
||||||
|
assert!(queue_contents_set.contains(¶));
|
||||||
|
}
|
||||||
|
|
||||||
|
// `NeedsDispatch` is always sorted.
|
||||||
|
assert!(<Router as Store>::NeedsDispatch::get()
|
||||||
|
.windows(2)
|
||||||
|
.all(|xs| xs[0] <= xs[1]));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dispatch_empty() {
|
||||||
|
new_test_ext(default_genesis_config()).execute_with(|| {
|
||||||
|
assert_storage_consistency_exhaustive();
|
||||||
|
|
||||||
|
// make sure that the case with empty queues is handled properly
|
||||||
|
Router::process_pending_upward_messages();
|
||||||
|
|
||||||
|
assert_storage_consistency_exhaustive();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dispatch_single_message() {
|
||||||
|
let a = ParaId::from(228);
|
||||||
|
let msg = vec![1, 2, 3];
|
||||||
|
|
||||||
|
new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| {
|
||||||
|
let mut probe = Probe::new();
|
||||||
|
|
||||||
|
probe.assert_msg(a, msg.clone(), 0);
|
||||||
|
queue_upward_msg(a, msg);
|
||||||
|
|
||||||
|
Router::process_pending_upward_messages();
|
||||||
|
|
||||||
|
assert_storage_consistency_exhaustive();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dispatch_resume_after_exceeding_dispatch_stage_weight() {
|
||||||
|
let a = ParaId::from(128);
|
||||||
|
let c = ParaId::from(228);
|
||||||
|
let q = ParaId::from(911);
|
||||||
|
|
||||||
|
let a_msg_1 = vec![1, 2, 3];
|
||||||
|
let a_msg_2 = vec![3, 2, 1];
|
||||||
|
let c_msg_1 = vec![4, 5, 6];
|
||||||
|
let c_msg_2 = vec![9, 8, 7];
|
||||||
|
let q_msg = b"we are Q".to_vec();
|
||||||
|
|
||||||
|
new_test_ext(
|
||||||
|
GenesisConfigBuilder {
|
||||||
|
preferred_dispatchable_upward_messages_step_weight: 500,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
.execute_with(|| {
|
||||||
|
queue_upward_msg(q, q_msg.clone());
|
||||||
|
queue_upward_msg(c, c_msg_1.clone());
|
||||||
|
queue_upward_msg(a, a_msg_1.clone());
|
||||||
|
queue_upward_msg(a, a_msg_2.clone());
|
||||||
|
|
||||||
|
assert_storage_consistency_exhaustive();
|
||||||
|
|
||||||
|
// we expect only two first messages to fit in the first iteration.
|
||||||
|
{
|
||||||
|
let mut probe = Probe::new();
|
||||||
|
|
||||||
|
probe.assert_msg(a, a_msg_1.clone(), 300);
|
||||||
|
probe.assert_msg(c, c_msg_1.clone(), 300);
|
||||||
|
Router::process_pending_upward_messages();
|
||||||
|
assert_storage_consistency_exhaustive();
|
||||||
|
|
||||||
|
drop(probe);
|
||||||
|
}
|
||||||
|
|
||||||
|
queue_upward_msg(c, c_msg_2.clone());
|
||||||
|
assert_storage_consistency_exhaustive();
|
||||||
|
|
||||||
|
// second iteration should process the second message.
|
||||||
|
{
|
||||||
|
let mut probe = Probe::new();
|
||||||
|
|
||||||
|
probe.assert_msg(q, q_msg.clone(), 500);
|
||||||
|
Router::process_pending_upward_messages();
|
||||||
|
assert_storage_consistency_exhaustive();
|
||||||
|
|
||||||
|
drop(probe);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3rd iteration.
|
||||||
|
{
|
||||||
|
let mut probe = Probe::new();
|
||||||
|
|
||||||
|
probe.assert_msg(a, a_msg_2.clone(), 100);
|
||||||
|
probe.assert_msg(c, c_msg_2.clone(), 100);
|
||||||
|
Router::process_pending_upward_messages();
|
||||||
|
assert_storage_consistency_exhaustive();
|
||||||
|
|
||||||
|
drop(probe);
|
||||||
|
}
|
||||||
|
|
||||||
|
// finally, make sure that the queue is empty.
|
||||||
|
{
|
||||||
|
let probe = Probe::new();
|
||||||
|
|
||||||
|
Router::process_pending_upward_messages();
|
||||||
|
assert_storage_consistency_exhaustive();
|
||||||
|
|
||||||
|
drop(probe);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dispatch_correctly_handle_remove_of_latest() {
|
||||||
|
let a = ParaId::from(1991);
|
||||||
|
let b = ParaId::from(1999);
|
||||||
|
|
||||||
|
let a_msg_1 = vec![1, 2, 3];
|
||||||
|
let a_msg_2 = vec![3, 2, 1];
|
||||||
|
let b_msg_1 = vec![4, 5, 6];
|
||||||
|
|
||||||
|
new_test_ext(
|
||||||
|
GenesisConfigBuilder {
|
||||||
|
preferred_dispatchable_upward_messages_step_weight: 900,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
.execute_with(|| {
|
||||||
|
// We want to test here an edge case, where we remove the queue with the highest
|
||||||
|
// para id (i.e. last in the needs_dispatch order).
|
||||||
|
//
|
||||||
|
// If the last entry was removed we should proceed execution, assuming we still have
|
||||||
|
// weight available.
|
||||||
|
|
||||||
|
queue_upward_msg(a, a_msg_1.clone());
|
||||||
|
queue_upward_msg(a, a_msg_2.clone());
|
||||||
|
queue_upward_msg(b, b_msg_1.clone());
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut probe = Probe::new();
|
||||||
|
|
||||||
|
probe.assert_msg(a, a_msg_1.clone(), 300);
|
||||||
|
probe.assert_msg(b, b_msg_1.clone(), 300);
|
||||||
|
probe.assert_msg(a, a_msg_2.clone(), 300);
|
||||||
|
|
||||||
|
Router::process_pending_upward_messages();
|
||||||
|
|
||||||
|
drop(probe);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -531,7 +531,9 @@ impl parachains_paras::Trait for Runtime {
|
|||||||
type Origin = Origin;
|
type Origin = Origin;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl parachains_router::Trait for Runtime {}
|
impl parachains_router::Trait for Runtime {
|
||||||
|
type UmpSink = (); // TODO: #1873 To be handled by the XCM receiver.
|
||||||
|
}
|
||||||
|
|
||||||
impl parachains_inclusion_inherent::Trait for Runtime {}
|
impl parachains_inclusion_inherent::Trait for Runtime {}
|
||||||
|
|
||||||
|
|||||||
@@ -452,7 +452,9 @@ impl paras::Trait for Runtime {
|
|||||||
type Origin = Origin;
|
type Origin = Origin;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl router::Trait for Runtime {}
|
impl router::Trait for Runtime {
|
||||||
|
type UmpSink = ();
|
||||||
|
}
|
||||||
|
|
||||||
impl scheduler::Trait for Runtime {}
|
impl scheduler::Trait for Runtime {}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user