From 1d98c08667bedba66a5b4bc9f1292808b2c5429b Mon Sep 17 00:00:00 2001 From: Sergey Shulepov Date: Wed, 28 Oct 2020 17:27:34 +0100 Subject: [PATCH 01/20] UMP: Update the impl guide --- .../implementers-guide/src/runtime/router.md | 18 +++++++++++++----- .../implementers-guide/src/types/runtime.md | 6 +++--- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/roadmap/implementers-guide/src/runtime/router.md b/roadmap/implementers-guide/src/runtime/router.md index 5907d1650b85..293960e58846 100644 --- a/roadmap/implementers-guide/src/runtime/router.md +++ b/roadmap/implementers-guide/src/runtime/router.md @@ -21,14 +21,24 @@ RelayDispatchQueues: map ParaId => Vec<(ParachainDispatchOrigin, RawDispatchable /// 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 +/// 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 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; -/// 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. +/// +/// Invariant: +/// - If `Some(para)`, then `para` must be present in `NeedsDispatch`. NextDispatchRoundStartWith: Option; ``` @@ -260,9 +270,7 @@ any of dispatchables return an error. 1. Decode `D` into a dispatchable. Otherwise, if succeeded: 1. If `weight_of(D) > config.dispatchable_upward_message_critical_weight` then skip the dispatchable. Otherwise: 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 `T >= config.preferred_dispatchable_upward_messages_step_weight`, set `NextDispatchRoundStartWith` to `P` and finish processing. 1. If `RelayDispatchQueues` for `P` became empty, remove `P` from `NeedsDispatch`. 1. If `NeedsDispatch` became empty then finish processing and set `NextDispatchRoundStartWith` to `None`. diff --git a/roadmap/implementers-guide/src/types/runtime.md b/roadmap/implementers-guide/src/types/runtime.md index 9bcecb7aa5b2..24befc8f5ce4 100644 --- a/roadmap/implementers-guide/src/types/runtime.md +++ b/roadmap/implementers-guide/src/types/runtime.md @@ -35,7 +35,7 @@ struct HostConfiguration { /// The amount of blocks ahead to schedule parathreads. pub scheduling_lookahead: u32, /// Total number of individual messages allowed in the parachain -> relay-chain message queue. - pub max_upward_queue_count: u32, + pub max_upward_queue_capacity: 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. @@ -44,7 +44,7 @@ struct HostConfiguration { /// stage. /// /// 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 parameter value is picked up so that no dispatchable can make the block weight exceed @@ -52,7 +52,7 @@ struct HostConfiguration { /// 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, + pub dispatchable_upward_message_critical_weight: Weight, /// The maximum number of messages that a candidate can contain. pub max_upward_message_num_per_candidate: u32, /// The maximum size of a message that can be put in a downward message queue. From 90e42ba205774f09352c7d11a0b441acd690e374 Mon Sep 17 00:00:00 2001 From: Sergei Shulepov Date: Mon, 14 Sep 2020 12:22:32 +0200 Subject: [PATCH 02/20] UMP: Incorporate XCM related changes into the guide --- roadmap/implementers-guide/src/messaging.md | 26 ++-- .../src/runtime/inclusioninherent.md | 2 +- .../implementers-guide/src/runtime/router.md | 123 +++++++++--------- .../implementers-guide/src/types/messages.md | 64 +-------- .../implementers-guide/src/types/runtime.md | 8 -- 5 files changed, 88 insertions(+), 135 deletions(-) diff --git a/roadmap/implementers-guide/src/messaging.md b/roadmap/implementers-guide/src/messaging.md index 1e782e155be1..edc810e03415 100644 --- a/roadmap/implementers-guide/src/messaging.md +++ b/roadmap/implementers-guide/src/messaging.md @@ -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. 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 - kinds. +from a parachain up to the relay chain. Upward messages are essentially byte blobs. However, they are interpreted +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 -invoke exposed runtime entrypoints, they consume weight and require fees. The difference is that they originate from -a parachain. Each parachain has a queue of dispatchables to be executed. There can be only so many dispatchables at a time. +The XCM standard is a common vocabulary of messages. The XCM standard doesn't require a particular interpretation of +a message. However, the parachains host (e.g. Polkadot) guarantees certain semantics for those. + +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 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. -Upward messages are also used by a parachain 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. +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). ## Horizontal Message Passing diff --git a/roadmap/implementers-guide/src/runtime/inclusioninherent.md b/roadmap/implementers-guide/src/runtime/inclusioninherent.md index 990fd4a32b9a..9290025e2d05 100644 --- a/roadmap/implementers-guide/src/runtime/inclusioninherent.md +++ b/roadmap/implementers-guide/src/runtime/inclusioninherent.md @@ -22,5 +22,5 @@ Included: Option<()>, 1. Invoke `Scheduler::schedule(freed)` 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 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(())`. diff --git a/roadmap/implementers-guide/src/runtime/router.md b/roadmap/implementers-guide/src/runtime/router.md index 293960e58846..af9f861c8469 100644 --- a/roadmap/implementers-guide/src/runtime/router.md +++ b/roadmap/implementers-guide/src/runtime/router.md @@ -15,9 +15,15 @@ OutgoingParas: Vec; ### Upward Message Passing (UMP) ```rust -/// Dispatchable objects ready to be dispatched onto the relay chain. The messages are processed in FIFO order. -RelayDispatchQueues: map ParaId => Vec<(ParachainDispatchOrigin, RawDispatchable)>; +/// 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 ParaId => Vec; /// 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. /// @@ -166,36 +172,8 @@ No initialization routine runs for this module. Candidate Acceptance Function: * `check_upward_messages(P: ParaId, Vec`): - 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. If the message kind is `Dispatchable`: - 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. + 1. Checks that there are at most `config.max_upward_message_num_per_candidate` messages. + 1. Verify that `RelayDispatchQueueSize` for `P` has enough capacity for the 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 `processed_downward_messages` is at least 1 if `DownwardMessageQueues` for `P` is not empty. @@ -232,56 +210,85 @@ Candidate Enactment: * `prune_dmq(P: ParaId, processed_downward_messages)`: 1. Remove the first `processed_downward_messages` from the `DownwardMessageQueues` of `P`. * `enact_upward_messages(P: ParaId, Vec)`: - 1. Process all upward messages in order depending on their kinds: - 1. If the message kind is `Dispatchable`: + 1. Process each upward message `M` in order: 1. Append the message to `RelayDispatchQueues` for `P` 1. Increment the size and the count in `RelayDispatchQueueSize` for `P`. 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. `schedule_para_cleanup(ParaId)`: 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 -any of dispatchables return an error. +The following routine is meant to execute pending entries in upward message queues. This function doesn't fail, even if +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. 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. Decode `D` into a dispatchable. Otherwise, if succeeded: - 1. If `weight_of(D) > config.dispatchable_upward_message_critical_weight` then skip the dispatchable. Otherwise: - 1. Execute `D` and add the actual amount of weight consumed to `T`. + 1. Delegate processing of the message to the runtime. The weight consumed is added to `T`. 1. If `T >= config.preferred_dispatchable_upward_messages_step_weight`, set `NextDispatchRoundStartWith` to `P` and finish processing. 1. If `RelayDispatchQueues` for `P` became empty, remove `P` from `NeedsDispatch`. 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. `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. 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`. +## 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 1. Drain `OutgoingParas`. For each `P` happened to be in the list: diff --git a/roadmap/implementers-guide/src/types/messages.md b/roadmap/implementers-guide/src/types/messages.md index 9551bb627964..2462b969165e 100644 --- a/roadmap/implementers-guide/src/types/messages.md +++ b/roadmap/implementers-guide/src/types/messages.md @@ -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. ```rust,ignore +/// A message sent from a parachain to the relay-chain. +type UpwardMessage = Vec; + /// 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` @@ -28,6 +31,8 @@ struct InboundDownwardMessage { } ``` +## Horizontal Message Passing + ## HrmpChannelId 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; - -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 This is a message sent from a parachain to another parachain that travels through the relay chain. diff --git a/roadmap/implementers-guide/src/types/runtime.md b/roadmap/implementers-guide/src/types/runtime.md index 24befc8f5ce4..c745f96bbd21 100644 --- a/roadmap/implementers-guide/src/types/runtime.md +++ b/roadmap/implementers-guide/src/types/runtime.md @@ -45,14 +45,6 @@ struct HostConfiguration { /// /// NOTE that this is a soft limit and could be exceeded. pub preferred_dispatchable_upward_messages_step_weight: Weight, - /// Any dispatchable upward message that requests more than the critical amount is rejected. - /// - /// The parameter value is picked up so that no dispatchable can make the block weight exceed - /// the total budget. I.e. that the sum of `preferred_dispatchable_upward_messages_step_weight` - /// 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: Weight, /// The maximum number of messages that a candidate can contain. pub max_upward_message_num_per_candidate: u32, /// The maximum size of a message that can be put in a downward message queue. From 02ce8c6d917d43b1af792436ff06ace87a31618e Mon Sep 17 00:00:00 2001 From: Sergei Shulepov Date: Wed, 9 Sep 2020 15:25:34 +0200 Subject: [PATCH 03/20] UMP: Data structures and configuration --- node/core/candidate-validation/src/lib.rs | 4 +- parachain/src/primitives.rs | 38 +------------ primitives/src/v0.rs | 2 +- primitives/src/v1.rs | 3 +- runtime/parachains/src/configuration.rs | 69 +++++++++++++++++++++++ 5 files changed, 74 insertions(+), 42 deletions(-) diff --git a/node/core/candidate-validation/src/lib.rs b/node/core/candidate-validation/src/lib.rs index eea1d757dd5f..2e0c87136601 100644 --- a/node/core/candidate-validation/src/lib.rs +++ b/node/core/candidate-validation/src/lib.rs @@ -501,7 +501,7 @@ fn validate_candidate_exhaustive( mod tests { use super::*; 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 futures::executor; use assert_matches::assert_matches; @@ -847,7 +847,7 @@ mod tests { assert_matches!(v, ValidationResult::Valid(outputs, used_validation_data) => { assert_eq!(outputs.head_data, HeadData(vec![1, 1, 1])); - assert_eq!(outputs.upward_messages, Vec::new()); + assert_eq!(outputs.upward_messages, Vec::::new()); assert_eq!(outputs.new_validation_code, Some(vec![2, 2, 2].into())); assert_eq!(used_validation_data, validation_data); }); diff --git a/parachain/src/primitives.rs b/parachain/src/primitives.rs index 83edc4e0f2a6..e66fb7c0931e 100644 --- a/parachain/src/primitives.rs +++ b/parachain/src/primitives.rs @@ -198,44 +198,8 @@ impl AccountIdConversion 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 for ParachainDispatchOrigin { - type Error = (); - fn try_from(x: u8) -> core::result::Result { - 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. -#[derive(Clone, PartialEq, Eq, Encode, Decode)] -#[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, -} +pub type UpwardMessage = Vec; /// Validation parameters for evaluating the parachain validity function. // TODO: balance downloads (https://github.com/paritytech/polkadot/issues/220) diff --git a/primitives/src/v0.rs b/primitives/src/v0.rs index c058d22dbc56..a01b3c743fa9 100644 --- a/primitives/src/v0.rs +++ b/primitives/src/v0.rs @@ -41,7 +41,7 @@ pub use polkadot_core_primitives::*; pub use parity_scale_codec::Compact; pub use polkadot_parachain::primitives::{ - Id, ParachainDispatchOrigin, LOWEST_USER_ID, UpwardMessage, HeadData, BlockData, + Id, LOWEST_USER_ID, UpwardMessage, HeadData, BlockData, ValidationCode, }; diff --git a/primitives/src/v1.rs b/primitives/src/v1.rs index 167ac16219aa..5347849f7f81 100644 --- a/primitives/src/v1.rs +++ b/primitives/src/v1.rs @@ -36,8 +36,7 @@ pub use polkadot_core_primitives::v1::{ // Export some polkadot-parachain primitives pub use polkadot_parachain::primitives::{ - Id, ParachainDispatchOrigin, LOWEST_USER_ID, UpwardMessage, HeadData, BlockData, - ValidationCode, + Id, LOWEST_USER_ID, UpwardMessage, HeadData, BlockData, ValidationCode, }; // Export some basic parachain primitives from v0. diff --git a/runtime/parachains/src/configuration.rs b/runtime/parachains/src/configuration.rs index 14a57ded184e..e964c1bc3c97 100644 --- a/runtime/parachains/src/configuration.rs +++ b/runtime/parachains/src/configuration.rs @@ -58,6 +58,12 @@ pub struct HostConfiguration { pub thread_availability_period: BlockNumber, /// The amount of blocks ahead to schedule parachains and parathreads. pub scheduling_lookahead: u32, + /// Total number of individual messages allowed in the parachain -> relay-chain message queue. + pub max_upward_queue_capacity: 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. /// /// Since we require receiving at least one DMP message the obvious upper bound of the size is @@ -65,6 +71,13 @@ pub struct HostConfiguration { /// decide to do with its PoV so this value in practice will be picked as a fraction of the PoV /// size. 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 number of messages that a candidate can contain. + pub max_upward_message_num_per_candidate: u32, } pub trait Trait: frame_system::Trait { } @@ -198,6 +211,26 @@ decl_module! { 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_capacity(origin, new: u32) -> DispatchResult { + ensure_root(origin)?; + Self::update_config_member(|config| { + sp_std::mem::replace(&mut config.max_upward_queue_capacity, 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. #[weight = (1_000, DispatchClass::Operational)] pub fn set_max_downward_message_size(origin, new: u32) -> DispatchResult { @@ -207,6 +240,26 @@ decl_module! { }); 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 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 +338,11 @@ mod tests { chain_availability_period: 10, thread_availability_period: 8, scheduling_lookahead: 3, + max_upward_queue_capacity: 1337, + max_upward_queue_size: 228, max_downward_message_size: 2048, + preferred_dispatchable_upward_messages_step_weight: 20000, + max_upward_message_num_per_candidate: 5, }; assert!(::PendingConfig::get().is_none()); @@ -323,9 +380,21 @@ mod tests { Configuration::set_scheduling_lookahead( Origin::root(), new_config.scheduling_lookahead, ).unwrap(); + Configuration::set_max_upward_queue_capacity( + Origin::root(), new_config.max_upward_queue_capacity, + ).unwrap(); + Configuration::set_max_upward_queue_size( + Origin::root(), new_config.max_upward_queue_size, + ).unwrap(); Configuration::set_max_downward_message_size( Origin::root(), new_config.max_downward_message_size, ).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_num_per_candidate( + Origin::root(), new_config.max_upward_message_num_per_candidate, + ).unwrap(); assert_eq!(::PendingConfig::get(), Some(new_config)); }) From e0f9af1671a1bcb2b84bc74184ed9e4610e7cfe6 Mon Sep 17 00:00:00 2001 From: Sergei Shulepov Date: Thu, 10 Sep 2020 15:34:36 +0200 Subject: [PATCH 04/20] UMP: Initial plumbing --- runtime/parachains/src/inclusion.rs | 17 ++++++ runtime/parachains/src/inclusion_inherent.rs | 4 ++ runtime/parachains/src/router.rs | 1 + runtime/parachains/src/router/ump.rs | 59 ++++++++++++++++++++ 4 files changed, 81 insertions(+) create mode 100644 runtime/parachains/src/router/ump.rs diff --git a/runtime/parachains/src/inclusion.rs b/runtime/parachains/src/inclusion.rs index e5e831bcc38b..d5a6e92674b8 100644 --- a/runtime/parachains/src/inclusion.rs +++ b/runtime/parachains/src/inclusion.rs @@ -155,6 +155,8 @@ decl_error! { InternalError, /// The downward message queue is not processed correctly. IncorrectDownwardMessageHandling, + /// At least one upward message sent does not pass the acceptance criteria. + InvalidUpwardMessages, } } @@ -412,6 +414,7 @@ impl Module { &candidate.candidate.commitments.head_data, &candidate.candidate.commitments.new_validation_code, candidate.candidate.commitments.processed_downward_messages, + &candidate.candidate.commitments.upward_messages, )?; for (i, assignment) in scheduled[skip..].iter().enumerate() { @@ -544,6 +547,7 @@ impl Module { &validation_outputs.head_data, &validation_outputs.new_validation_code, validation_outputs.processed_downward_messages, + &validation_outputs.upward_messages, ) } @@ -570,6 +574,10 @@ impl Module { receipt.descriptor.para_id, commitments.processed_downward_messages, ); + weight += >::enact_upward_messages( + receipt.descriptor.para_id, + commitments.upward_messages, + ); Self::deposit_event( Event::::CandidateIncluded(plain, commitments.head_data.clone()) @@ -693,6 +701,7 @@ impl CandidateCheckContext { head_data: &HeadData, new_validation_code: &Option, processed_downward_messages: u32, + upward_messages: &[primitives::v1::UpwardMessage], ) -> Result<(), DispatchError> { ensure!( head_data.0.len() <= self.config.max_head_data_size as _, @@ -722,6 +731,14 @@ impl CandidateCheckContext { ), Error::::IncorrectDownwardMessageHandling, ); + ensure!( + >::check_upward_messages( + &self.config, + para_id, + upward_messages, + ), + Error::::InvalidUpwardMessages, + ); Ok(()) } diff --git a/runtime/parachains/src/inclusion_inherent.rs b/runtime/parachains/src/inclusion_inherent.rs index f9a7465d9128..d827b359c821 100644 --- a/runtime/parachains/src/inclusion_inherent.rs +++ b/runtime/parachains/src/inclusion_inherent.rs @@ -35,6 +35,7 @@ use frame_system::ensure_none; use crate::{ inclusion, scheduler::{self, FreedReason}, + router, }; 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. >::occupied(&occupied); + // Give some time slice to dispatch pending upward messages. + >::process_pending_upward_messages(); + // And track that we've finished processing the inherent for this block. Included::set(Some(())); diff --git a/runtime/parachains/src/router.rs b/runtime/parachains/src/router.rs index ad12a33bc8c7..c4cd29155962 100644 --- a/runtime/parachains/src/router.rs +++ b/runtime/parachains/src/router.rs @@ -29,6 +29,7 @@ use frame_support::{decl_error, decl_module, decl_storage, weights::Weight}; use primitives::v1::{Id as ParaId, InboundDownwardMessage, Hash}; mod dmp; +mod ump; pub use dmp::QueueDownwardMessageError; diff --git a/runtime/parachains/src/router/ump.rs b/runtime/parachains/src/router/ump.rs new file mode 100644 index 000000000000..246a2c989058 --- /dev/null +++ b/runtime/parachains/src/router/ump.rs @@ -0,0 +1,59 @@ +// 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 . + +use super::{Trait, Module}; +use crate::configuration::HostConfiguration; +use sp_std::prelude::*; +use frame_support::weights::Weight; +use primitives::v1::{Id as ParaId, UpwardMessage}; + +impl Module { + /// 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, + para: ParaId, + upward_messages: &[UpwardMessage], + ) -> bool { + drop(para); + + if upward_messages.len() as u32 > config.max_upward_message_num_per_candidate { + return false; + } + + for _ in upward_messages { + return false; + } + + true + } + + /// Enacts all the upward messages sent by a candidate. + pub(crate) fn enact_upward_messages(para: ParaId, upward_messages: Vec) -> Weight { + drop(para); + + for _ in upward_messages { + todo!() + } + + 0 + } + + /// Devote some time into dispatching pending upward messages. + pub(crate) fn process_pending_upward_messages() { + // no-op for now, will be filled in the following commits + } +} From e9dde74b4f60d27cea4d75074075f58a86c6ca1d Mon Sep 17 00:00:00 2001 From: Sergei Shulepov Date: Thu, 17 Sep 2020 19:15:08 +0200 Subject: [PATCH 05/20] UMP: Data layout --- runtime/parachains/src/router.rs | 42 +++++++++++++++++++++++++++- runtime/parachains/src/router/ump.rs | 17 +++++++++-- 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/runtime/parachains/src/router.rs b/runtime/parachains/src/router.rs index c4cd29155962..6b168268a3b8 100644 --- a/runtime/parachains/src/router.rs +++ b/runtime/parachains/src/router.rs @@ -26,7 +26,8 @@ use crate::{ }; use sp_std::prelude::*; 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 ump; @@ -57,6 +58,44 @@ decl_storage! { /// - `B`: is the relay-chain block number in which a message was appended. /// - `H(M)`: is the hash of the message being appended. 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; + /// 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; + /// 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; } } @@ -87,6 +126,7 @@ impl Module { let outgoing = OutgoingParas::take(); for outgoing_para in outgoing { Self::clean_dmp_after_outgoing(outgoing_para); + Self::clean_ump_after_outgoing(outgoing_para); } } diff --git a/runtime/parachains/src/router/ump.rs b/runtime/parachains/src/router/ump.rs index 246a2c989058..33ff9bf36e24 100644 --- a/runtime/parachains/src/router/ump.rs +++ b/runtime/parachains/src/router/ump.rs @@ -14,13 +14,26 @@ // You should have received a copy of the GNU General Public License // along with Polkadot. If not, see . -use super::{Trait, Module}; +use super::{Trait, Module, Store}; use crate::configuration::HostConfiguration; use sp_std::prelude::*; -use frame_support::weights::Weight; +use frame_support::{StorageMap, StorageValue, weights::Weight}; use primitives::v1::{Id as ParaId, UpwardMessage}; impl Module { + pub(super) fn clean_ump_after_outgoing(outgoing_para: ParaId) { + ::RelayDispatchQueueSize::remove(&outgoing_para); + ::RelayDispatchQueues::remove(&outgoing_para); + ::NeedsDispatch::mutate(|v| { + if let Ok(i) = v.binary_search(&outgoing_para) { + v.remove(i); + } + }); + ::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( From c583a41077d25078a9e4d077e5332a14c57c8cbc Mon Sep 17 00:00:00 2001 From: Sergei Shulepov Date: Thu, 17 Sep 2020 19:15:48 +0200 Subject: [PATCH 06/20] UMP: Acceptance criteria & enactment --- runtime/common/src/paras_registrar.rs | 4 +- runtime/parachains/src/mock.rs | 4 +- runtime/parachains/src/router.rs | 9 +- runtime/parachains/src/router/ump.rs | 611 +++++++++++++++++++++++++- runtime/rococo-v1/src/lib.rs | 4 +- runtime/test-runtime/src/lib.rs | 4 +- 6 files changed, 620 insertions(+), 16 deletions(-) diff --git a/runtime/common/src/paras_registrar.rs b/runtime/common/src/paras_registrar.rs index 7c9ed36a9751..ab58878abddd 100644 --- a/runtime/common/src/paras_registrar.rs +++ b/runtime/common/src/paras_registrar.rs @@ -425,7 +425,9 @@ mod tests { type WeightInfo = (); } - impl router::Trait for Test { } + impl router::Trait for Test { + type UmpSink = (); + } impl pallet_session::historical::Trait for Test { type FullIdentification = pallet_staking::Exposure; diff --git a/runtime/parachains/src/mock.rs b/runtime/parachains/src/mock.rs index 490c8083ad8f..4fdd6e0e6155 100644 --- a/runtime/parachains/src/mock.rs +++ b/runtime/parachains/src/mock.rs @@ -108,7 +108,9 @@ impl crate::paras::Trait for Test { 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 { } diff --git a/runtime/parachains/src/router.rs b/runtime/parachains/src/router.rs index 6b168268a3b8..b773bd9c1270 100644 --- a/runtime/parachains/src/router.rs +++ b/runtime/parachains/src/router.rs @@ -33,8 +33,15 @@ mod dmp; mod ump; 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! { trait Store for Module as Router { diff --git a/runtime/parachains/src/router/ump.rs b/runtime/parachains/src/router/ump.rs index 33ff9bf36e24..203575bcfeab 100644 --- a/runtime/parachains/src/router/ump.rs +++ b/runtime/parachains/src/router/ump.rs @@ -15,15 +15,51 @@ // along with Polkadot. If not, see . use super::{Trait, Module, Store}; -use crate::configuration::HostConfiguration; +use crate::configuration::{self, HostConfiguration}; use sp_std::prelude::*; -use frame_support::{StorageMap, StorageValue, weights::Weight}; +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 and there is no upper bound. +/// +/// 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) -> Weight; +} + +/// An implementation of a sink that just swallows the message without consuming any weight. +impl UmpSink for () { + fn process_upward_message(_: ParaId, _: Vec) -> Weight { + 0 + } +} + +/// Routines related to the upward message passing. impl Module { pub(super) fn clean_ump_after_outgoing(outgoing_para: ParaId) { ::RelayDispatchQueueSize::remove(&outgoing_para); ::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`. ::NeedsDispatch::mutate(|v| { if let Ok(i) = v.binary_search(&outgoing_para) { v.remove(i); @@ -41,13 +77,23 @@ impl Module { para: ParaId, upward_messages: &[UpwardMessage], ) -> bool { - drop(para); - if upward_messages.len() as u32 > config.max_upward_message_num_per_candidate { return false; } - for _ in upward_messages { + let (mut para_queue_count, mut para_queue_size) = + ::RelayDispatchQueueSize::get(¶); + + for msg in upward_messages { + para_queue_count += 1; + para_queue_size += msg.len() as u32; + } + + // make sure that the queue is not overfilled. + // we do it here only once since returning false invalidates the whole relay-chain block. + if para_queue_count > config.max_upward_queue_capacity + || para_queue_size > config.max_upward_queue_size + { return false; } @@ -55,18 +101,561 @@ impl Module { } /// Enacts all the upward messages sent by a candidate. - pub(crate) fn enact_upward_messages(para: ParaId, upward_messages: Vec) -> Weight { - drop(para); + pub(crate) fn enact_upward_messages( + para: ParaId, + upward_messages: Vec, + ) -> 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)); + + ::RelayDispatchQueues::mutate(¶, |v| { + v.extend(upward_messages.into_iter()) + }); - for _ in upward_messages { - todo!() + ::RelayDispatchQueueSize::mutate( + ¶, + |(ref mut cnt, ref mut size)| { + *cnt += extra_cnt; + *size += extra_size; + }, + ); + + ::NeedsDispatch::mutate(|v| { + if let Err(i) = v.binary_search(¶) { + v.insert(i, para); + } + }); + + weight += T::DbWeight::get().reads_writes(3, 3); } - 0 + weight } /// Devote some time into dispatching pending upward messages. pub(crate) fn process_pending_upward_messages() { - // no-op for now, will be filled in the following commits + let mut used_weight_so_far = 0; + + let config = >::config(); + let mut cursor = NeedsDispatchCursor::new::(); + 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::(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::(); + queue_cache.flush::(); + } +} + +/// 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); + +struct QueueCacheEntry { + queue: VecDeque, + 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(&mut self, para: ParaId) -> (Option, bool) { + let cache_entry = self.0.entry(para).or_insert_with(|| { + let queue = as Store>::RelayDispatchQueues::get(¶); + let (count, total_size) = 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(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. + as Store>::RelayDispatchQueues::remove(¶); + as Store>::RelayDispatchQueueSize::remove(¶); + } else { + as Store>::RelayDispatchQueues::insert(¶, queue); + 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, + cur_idx: usize, +} + +impl NeedsDispatchCursor { + fn new() -> Self { + let needs_dispatch: Vec = as Store>::NeedsDispatch::get(); + let start_with = 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 { + 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); + } + + /// Flushes the dispatcher state into the persistent storage. + fn flush(self) { + let next_one = self.peek(); + as Store>::NextDispatchRoundStartWith::set(next_one); + 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>> = RefCell::new(None); + } + + pub struct MockUmpSink; + impl UmpSink for MockUmpSink { + fn process_upward_message(actual_origin: ParaId, actual_msg: Vec) -> 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_num_per_candidate: u32, + max_upward_queue_capacity: u32, + max_upward_queue_size: u32, + preferred_dispatchable_upward_messages_step_weight: Weight, + } + + impl Default for GenesisConfigBuilder { + fn default() -> Self { + Self { + max_upward_message_num_per_candidate: 2, + max_upward_queue_capacity: 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_num_per_candidate = self.max_upward_message_num_per_candidate; + config.max_upward_queue_capacity = self.max_upward_queue_capacity; + 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 ::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 ::RelayDispatchQueues::iter() { + let (expected_count, expected_size) = + ::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 = ::RelayDispatchQueues::iter() + .map(|(k, _)| k) + .collect::>(); + let queue_sizes_set = ::RelayDispatchQueueSize::iter() + .map(|(k, _)| k) + .collect::>(); + let needs_dispatch_set = ::NeedsDispatch::get() + .into_iter() + .collect::>(); + 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) = ::NextDispatchRoundStartWith::get() { + assert!(queue_contents_set.contains(¶)); + } + + // `NeedsDispatch` is always sorted. + assert!(::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); + } + }); } } diff --git a/runtime/rococo-v1/src/lib.rs b/runtime/rococo-v1/src/lib.rs index f6195e48f5b9..9123a7c4b79d 100644 --- a/runtime/rococo-v1/src/lib.rs +++ b/runtime/rococo-v1/src/lib.rs @@ -765,7 +765,9 @@ impl parachains_paras::Trait for Runtime { 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 {} diff --git a/runtime/test-runtime/src/lib.rs b/runtime/test-runtime/src/lib.rs index 6e4913a0dc88..0153d87b4782 100644 --- a/runtime/test-runtime/src/lib.rs +++ b/runtime/test-runtime/src/lib.rs @@ -452,7 +452,9 @@ impl paras::Trait for Runtime { type Origin = Origin; } -impl router::Trait for Runtime {} +impl router::Trait for Runtime { + type UmpSink = (); +} impl scheduler::Trait for Runtime {} From ea5d29cb3476fb6a948cc2b4b4107d34d17936eb Mon Sep 17 00:00:00 2001 From: Sergey Shulepov Date: Tue, 27 Oct 2020 18:52:40 +0100 Subject: [PATCH 07/20] UMP: Fix dispatcher bug and add the test for it --- runtime/parachains/src/router/ump.rs | 48 ++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/runtime/parachains/src/router/ump.rs b/runtime/parachains/src/router/ump.rs index 203575bcfeab..3b0b6672b08e 100644 --- a/runtime/parachains/src/router/ump.rs +++ b/runtime/parachains/src/router/ump.rs @@ -317,6 +317,12 @@ impl NeedsDispatchCursor { 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. @@ -658,4 +664,46 @@ mod tests { } }); } + + #[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); + } + }); + } + } From 294b97e415c6085ce6ed420c74502c7bae950c1a Mon Sep 17 00:00:00 2001 From: Sergey Shulepov Date: Wed, 28 Oct 2020 18:28:57 +0100 Subject: [PATCH 08/20] UMP: Constrain the maximum size of an UMP message This commit addresses the UMP part of https://github.com/paritytech/polkadot/issues/1869 --- .../implementers-guide/src/runtime/router.md | 1 + .../implementers-guide/src/types/runtime.md | 6 ++++++ runtime/parachains/src/configuration.rs | 20 +++++++++++++++++++ runtime/parachains/src/router/ump.rs | 9 +++++++-- 4 files changed, 34 insertions(+), 2 deletions(-) diff --git a/roadmap/implementers-guide/src/runtime/router.md b/roadmap/implementers-guide/src/runtime/router.md index af9f861c8469..734081604371 100644 --- a/roadmap/implementers-guide/src/runtime/router.md +++ b/roadmap/implementers-guide/src/runtime/router.md @@ -173,6 +173,7 @@ Candidate Acceptance Function: * `check_upward_messages(P: ParaId, Vec`): 1. Checks that there are at most `config.max_upward_message_num_per_candidate` messages. + 1. Checks that no message exceeds `config.max_upward_message_size`. 1. Verify that `RelayDispatchQueueSize` for `P` has enough capacity for the messages * `check_processed_downward_messages(P: ParaId, processed_downward_messages)`: 1. Checks that `DownwardMessageQueues` for `P` is at least `processed_downward_messages` long. diff --git a/roadmap/implementers-guide/src/types/runtime.md b/roadmap/implementers-guide/src/types/runtime.md index c745f96bbd21..fc1868c32c60 100644 --- a/roadmap/implementers-guide/src/types/runtime.md +++ b/roadmap/implementers-guide/src/types/runtime.md @@ -45,7 +45,13 @@ struct HostConfiguration { /// /// 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 upper bound of size of `CandidateCommitments`. + pub max_upward_message_size: u32, /// 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, /// The maximum size of a message that can be put in a downward message queue. /// diff --git a/runtime/parachains/src/configuration.rs b/runtime/parachains/src/configuration.rs index e964c1bc3c97..3f7f32f39153 100644 --- a/runtime/parachains/src/configuration.rs +++ b/runtime/parachains/src/configuration.rs @@ -76,7 +76,13 @@ pub struct HostConfiguration { /// /// 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, } @@ -251,6 +257,16 @@ decl_module! { 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 { @@ -342,6 +358,7 @@ mod tests { max_upward_queue_size: 228, max_downward_message_size: 2048, preferred_dispatchable_upward_messages_step_weight: 20000, + max_upward_message_size: 448, max_upward_message_num_per_candidate: 5, }; @@ -392,6 +409,9 @@ mod tests { 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(); diff --git a/runtime/parachains/src/router/ump.rs b/runtime/parachains/src/router/ump.rs index 3b0b6672b08e..484004dbfa19 100644 --- a/runtime/parachains/src/router/ump.rs +++ b/runtime/parachains/src/router/ump.rs @@ -23,7 +23,8 @@ 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 and there is no upper bound. +/// 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 @@ -85,8 +86,12 @@ impl Module { ::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.len() as u32; + para_queue_size += msg_size; } // make sure that the queue is not overfilled. From d1dbf727c6f6a5ae034cd042df3bc22ec639b683 Mon Sep 17 00:00:00 2001 From: Sergey Shulepov Date: Thu, 29 Oct 2020 15:54:18 +0100 Subject: [PATCH 09/20] Fix failing test due to misconfiguration --- runtime/parachains/src/router/ump.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/runtime/parachains/src/router/ump.rs b/runtime/parachains/src/router/ump.rs index 484004dbfa19..1949d8f5bc2a 100644 --- a/runtime/parachains/src/router/ump.rs +++ b/runtime/parachains/src/router/ump.rs @@ -479,6 +479,7 @@ mod tests { use std::collections::HashSet; struct GenesisConfigBuilder { + max_upward_message_size: u32, max_upward_message_num_per_candidate: u32, max_upward_queue_capacity: u32, max_upward_queue_size: u32, @@ -488,6 +489,7 @@ mod tests { impl Default for GenesisConfigBuilder { fn default() -> Self { Self { + max_upward_message_size: 16, max_upward_message_num_per_candidate: 2, max_upward_queue_capacity: 4, max_upward_queue_size: 64, @@ -501,6 +503,7 @@ mod tests { 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_capacity = self.max_upward_queue_capacity; config.max_upward_queue_size = self.max_upward_queue_size; From 72654e82ac016bbf2d58764c690cc40f633a8195 Mon Sep 17 00:00:00 2001 From: Sergey Shulepov Date: Thu, 29 Oct 2020 15:57:17 +0100 Subject: [PATCH 10/20] Make the type of RelayDispatchQueueSize be more apparent in the guide --- roadmap/implementers-guide/src/runtime/router.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roadmap/implementers-guide/src/runtime/router.md b/roadmap/implementers-guide/src/runtime/router.md index 734081604371..4310aa711fc9 100644 --- a/roadmap/implementers-guide/src/runtime/router.md +++ b/roadmap/implementers-guide/src/runtime/router.md @@ -33,7 +33,7 @@ RelayDispatchQueues: map ParaId => Vec; /// /// Invariant: /// - The set of keys should exactly match the set of keys of `RelayDispatchQueues`. -RelayDispatchQueueSize: map ParaId => (u32, u32); +RelayDispatchQueueSize: map ParaId => (u32, u32); // (num_messages, total_bytes) /// The ordered list of `ParaId`s that have a `RelayDispatchQueue` entry. /// /// Invariant: From d2d717cd50d0cf91248601e39d001e9a3cab9285 Mon Sep 17 00:00:00 2001 From: Sergei Shulepov Date: Wed, 30 Sep 2020 16:15:24 +0200 Subject: [PATCH 11/20] parachain-primitives: Add an escape hatch for extracting the inner u32 --- parachain/src/primitives.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/parachain/src/primitives.rs b/parachain/src/primitives.rs index e66fb7c0931e..7387de99d66d 100644 --- a/parachain/src/primitives.rs +++ b/parachain/src/primitives.rs @@ -127,6 +127,14 @@ impl Id { /// Use IsSystem instead. #[deprecated] pub fn is_system(&self) -> bool { self.0 < USER_INDEX_START } + + /// Returns the para id represented as a u32. + /// + /// An escape hatch meant to be used only in exceptional situations. For instance, when you need + /// to put the ID on wire but have no luxury of referencing this type directly. + pub fn reveal_inner_u32(self) -> u32 { + self.0 + } } pub trait IsSystem { From 1e7368ea2d2ab1535feb53a604c9caaae4a55368 Mon Sep 17 00:00:00 2001 From: Sergey Shulepov Date: Wed, 28 Oct 2020 17:29:14 +0100 Subject: [PATCH 12/20] HRMP: Update the impl guide --- roadmap/implementers-guide/src/runtime/inclusion.md | 7 +++---- roadmap/implementers-guide/src/runtime/router.md | 13 ++++++++++--- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/roadmap/implementers-guide/src/runtime/inclusion.md b/roadmap/implementers-guide/src/runtime/inclusion.md index 17dbdc94cc02..58136d84a879 100644 --- a/roadmap/implementers-guide/src/runtime/inclusion.md +++ b/roadmap/implementers-guide/src/runtime/inclusion.md @@ -71,8 +71,7 @@ All failed checks should lead to an unrecoverable error making the block invalid 1. call `Router::check_upward_messages(para, commitments.upward_messages)` to check that the upward messages are valid. 1. call `Router::check_processed_downward_messages(para, commitments.processed_downward_messages)` to check that the DMQ is properly drained. 1. call `Router::check_hrmp_watermark(para, commitments.hrmp_watermark)` for each candidate to check rules of processing the HRMP watermark. - 1. check that in the commitments of each candidate the horizontal messages are sorted by ascending recipient ParaId and there is no two horizontal messages have the same recipient. - 1. using `Router::verify_outbound_hrmp(sender, commitments.horizontal_messages)` ensure that the each candidate send a valid set of horizontal messages + 1. using `Router::check_outbound_hrmp(sender, commitments.horizontal_messages)` ensure that the each candidate send a valid set of horizontal messages 1. create an entry in the `PendingAvailability` map for each backed candidate with a blank `availability_votes` bitfield. 1. create a corresponding entry in the `PendingAvailabilityCommitments` with the commitments. 1. Return a `Vec` of all scheduled cores of the list of passed assignments that a candidate was successfully backed for, sorted ascending by CoreIndex. @@ -80,9 +79,9 @@ All failed checks should lead to an unrecoverable error making the block invalid 1. If the receipt contains a code upgrade, Call `Paras::schedule_code_upgrade(para_id, code, relay_parent_number + config.validationl_upgrade_delay)`. > TODO: Note that this is safe as long as we never enact candidates where the relay parent is across a session boundary. In that case, which we should be careful to avoid with contextual execution, the configuration might have changed and the para may de-sync from the host's understanding of it. 1. call `Router::enact_upward_messages` for each backed candidate, using the [`UpwardMessage`s](../types/messages.md#upward-message) from the [`CandidateCommitments`](../types/candidate.md#candidate-commitments). - 1. call `Router::queue_outbound_hrmp` with the para id of the candidate and the list of horizontal messages taken from the commitment, - 1. call `Router::prune_hrmp` with the para id of the candiate and the candidate's `hrmp_watermark`. 1. call `Router::prune_dmq` with the para id of the candidate and the candidate's `processed_downward_messages`. + 1. call `Router::prune_hrmp` with the para id of the candiate and the candidate's `hrmp_watermark`. + 1. call `Router::queue_outbound_hrmp` with the para id of the candidate and the list of horizontal messages taken from the commitment, 1. Call `Paras::note_new_head` using the `HeadData` from the receipt and `relay_parent_number`. * `collect_pending`: diff --git a/roadmap/implementers-guide/src/runtime/router.md b/roadmap/implementers-guide/src/runtime/router.md index 4310aa711fc9..f0abd4495eed 100644 --- a/roadmap/implementers-guide/src/runtime/router.md +++ b/roadmap/implementers-guide/src/runtime/router.md @@ -90,6 +90,8 @@ struct HrmpChannel { sender_deposit: Balance, /// The amount that the recipient supplied as a deposit when accepting opening this channel. recipient_deposit: Balance, + /// The maximum message size that could be put into the channel. + limit_message_size: u32, /// The maximum number of messages that can be pending in the channel at once. limit_used_places: u32, /// The maximum total size of the messages that can be pending in the channel at once. @@ -104,12 +106,12 @@ struct HrmpChannel { used_bytes: u32, /// A head of the Message Queue Chain for this channel. Each link in this chain has a form: /// `(prev_head, B, H(M))`, where - /// - `prev_head`: is the previous value of `mqc_head`. + /// - `prev_head`: is the previous value of `mqc_head` or zero if none. /// - `B`: is the [relay-chain] block number in which a message was appended /// - `H(M)`: is the hash of the message being appended. /// This value is initialized to a special value that consists of all zeroes which indicates /// that no messages were previously added. - mqc_head: Hash, + mqc_head: Option, } ``` HRMP related storage layout @@ -184,7 +186,8 @@ Candidate Acceptance Function: 1. `new_hrmp_watermark` should be either 1. equal to the context's block number 1. or in `HrmpChannelDigests` for `P` an entry with the block number should exist -* `verify_outbound_hrmp(sender: ParaId, Vec)`: +* `check_outbound_hrmp(sender: ParaId, Vec)`: + 1. Checks that horizontal messages are sorted by ascending recipient ParaId and there is no two horizontal messages have the same recipient. 1. For each horizontal message `M` with the channel `C` identified by `(sender, M.recipient)` check: 1. exists 1. `M`'s payload size doesn't exceed a preconfigured limit `C.limit_message_size` @@ -208,6 +211,10 @@ Candidate Enactment: 1. Decrement `C.used_places` 1. Decrement `C.used_bytes` by `M`'s payload size. 1. Set `HrmpWatermarks` for `P` to be equal to `new_hrmp_watermark` + > NOTE: That collecting digests can be inefficient and the time it takes grows very fast. Thanks to the aggresive + > parametrization this shouldn't be a big of a deal. + > If that becomes a problem consider introducing an extra dictionary which says at what block the given sender + > sent a message to the recipient. * `prune_dmq(P: ParaId, processed_downward_messages)`: 1. Remove the first `processed_downward_messages` from the `DownwardMessageQueues` of `P`. * `enact_upward_messages(P: ParaId, Vec)`: From 105255e8dedce5581fd5d5771aa332042a064315 Mon Sep 17 00:00:00 2001 From: Sergei Shulepov Date: Wed, 30 Sep 2020 14:45:00 +0200 Subject: [PATCH 13/20] HRMP: Incorporate the channel notifications into the guide --- .../implementers-guide/src/runtime/router.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/roadmap/implementers-guide/src/runtime/router.md b/roadmap/implementers-guide/src/runtime/router.md index f0abd4495eed..f8231a227c86 100644 --- a/roadmap/implementers-guide/src/runtime/router.md +++ b/roadmap/implementers-guide/src/runtime/router.md @@ -278,6 +278,12 @@ the parachain executed the message. 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. Send a downward message to `recipient` notifying about an inbound HRMP channel request. + - The DM is sent using `queue_downward_message`. + - The DM is represented by the `HrmpNewChannelOpenRequest` XCM message. + - `sender` is set to `origin`, + - `max_message_size` is set to `max_message_size`, + - `max_capacity` is set to `max_places`. * `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. @@ -290,12 +296,23 @@ the parachain executed the message. 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`. + 1. Send a downward message to `sender` notifying that the channel request was accepted. + - The DM is sent using `queue_downward_message`. + - The DM is represented by the `HrmpChannelAccepted` XCM message. + - `recipient` is set to `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`. + 1. Send a downward message to the opposite party notifying about the channel closing. + - The DM is sent using `queue_downward_message`. + - The DM is represented by the `HrmpChannelClosing` XCM message with: + - `initator` is set to `origin`, + - `sender` is set to `ch.sender`, + - `recipient` is set to `ch.recipient`. + - The opposite party is `ch.sender` if `origin` is `ch.recipient` and `ch.recipient` if `origin` is `ch.sender`. ## Session Change From 256964487a36de395b3912c01a1da40edfd8fcea Mon Sep 17 00:00:00 2001 From: Sergey Shulepov Date: Wed, 28 Oct 2020 19:15:24 +0100 Subject: [PATCH 14/20] HRMP: Renaming in the impl guide --- .../implementers-guide/src/runtime/router.md | 50 ++++++++++--------- .../implementers-guide/src/types/runtime.md | 4 +- 2 files changed, 28 insertions(+), 26 deletions(-) diff --git a/roadmap/implementers-guide/src/runtime/router.md b/roadmap/implementers-guide/src/runtime/router.md index f8231a227c86..3125e59145bb 100644 --- a/roadmap/implementers-guide/src/runtime/router.md +++ b/roadmap/implementers-guide/src/runtime/router.md @@ -78,10 +78,12 @@ struct HrmpOpenChannelRequest { age: SessionIndex, /// The amount that the sender supplied at the time of creation of this request. sender_deposit: Balance, + /// The maximum message size that could be put into the channel. + max_message_size: u32, /// The maximum number of messages that can be pending in the channel at once. - limit_used_places: u32, + max_capacity: u32, /// The maximum total size of the messages that can be pending in the channel at once. - limit_used_bytes: u32, + max_total_size: u32, } /// A metadata of an HRMP channel. @@ -93,17 +95,17 @@ struct HrmpChannel { /// The maximum message size that could be put into the channel. limit_message_size: u32, /// The maximum number of messages that can be pending in the channel at once. - limit_used_places: u32, + max_capacity: u32, /// The maximum total size of the messages that can be pending in the channel at once. - limit_used_bytes: u32, + max_total_size: u32, /// The maximum message size that could be put into the channel. - limit_message_size: u32, + max_message_size: u32, /// The current number of messages pending in the channel. - /// Invariant: should be less or equal to `limit_used_places`. - used_places: u32, + /// Invariant: should be less or equal to `max_capacity`. + msg_count: u32, /// The total size in bytes of all message payloads in the channel. - /// Invariant: should be less or equal to `limit_used_bytes`. - used_bytes: u32, + /// Invariant: should be less or equal to `max_total_size`. + total_size: u32, /// A head of the Message Queue Chain for this channel. Each link in this chain has a form: /// `(prev_head, B, H(M))`, where /// - `prev_head`: is the previous value of `mqc_head` or zero if none. @@ -190,9 +192,9 @@ Candidate Acceptance Function: 1. Checks that horizontal messages are sorted by ascending recipient ParaId and there is no two horizontal messages have the same recipient. 1. For each horizontal message `M` with the channel `C` identified by `(sender, M.recipient)` check: 1. exists - 1. `M`'s payload size doesn't exceed a preconfigured limit `C.limit_message_size` - 1. `M`'s payload size summed with the `C.used_bytes` doesn't exceed a preconfigured limit `C.limit_used_bytes`. - 1. `C.used_places + 1` doesn't exceed a preconfigured limit `C.limit_used_places`. + 1. `M`'s payload size doesn't exceed a preconfigured limit `C.max_message_size` + 1. `M`'s payload size summed with the `C.total_size` doesn't exceed a preconfigured limit `C.max_total_size`. + 1. `C.msg_count + 1` doesn't exceed a preconfigured limit `C.max_capacity`. Candidate Enactment: @@ -200,16 +202,16 @@ Candidate Enactment: 1. For each horizontal message `HM` with the channel `C` identified by `(sender, HM.recipient)`: 1. Append `HM` into `HrmpChannelContents` that corresponds to `C` with `sent_at` equals to the current block number. 1. Locate or create an entry in `HrmpChannelDigests` for `HM.recipient` and append `sender` into the entry's list. - 1. Increment `C.used_places` - 1. Increment `C.used_bytes` by `HM`'s payload size + 1. Increment `C.msg_count` + 1. Increment `C.total_size` by `HM`'s payload size 1. Append a new link to the MQC and save the new head in `C.mqc_head`. Note that the current block number as of enactment is used for the link. * `prune_hrmp(recipient, new_hrmp_watermark)`: 1. From `HrmpChannelDigests` for `recipient` remove all entries up to an entry with block number equal to `new_hrmp_watermark`. 1. From the removed digests construct a set of paras that sent new messages within the interval between the old and new watermarks. 1. For each channel `C` identified by `(sender, recipient)` for each `sender` coming from the set, prune messages up to the `new_hrmp_watermark`. 1. For each pruned message `M` from channel `C`: - 1. Decrement `C.used_places` - 1. Decrement `C.used_bytes` by `M`'s payload size. + 1. Decrement `C.msg_count` + 1. Decrement `C.total_size` by `M`'s payload size. 1. Set `HrmpWatermarks` for `P` to be equal to `new_hrmp_watermark` > NOTE: That collecting digests can be inefficient and the time it takes grows very fast. Thanks to the aggresive > parametrization this shouldn't be a big of a deal. @@ -258,10 +260,10 @@ 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)`: +* `hrmp_init_open_channel(recipient, proposed_max_capacity, proposed_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 `proposed_max_capacity` is less or equal to `config.hrmp_channel_max_capacity` and greater than zero. + 1. Check that `proposed_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`. @@ -275,15 +277,15 @@ the parachain executed the message. 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` + 1. Set `max_capacity` to `proposed_max_capacity` + 1. Set `max_message_size` to `proposed_max_message_size` + 1. Set `max_total_size` to `config.hrmp_channel_max_total_size` 1. Send a downward message to `recipient` notifying about an inbound HRMP channel request. - The DM is sent using `queue_downward_message`. - The DM is represented by the `HrmpNewChannelOpenRequest` XCM message. - `sender` is set to `origin`, - - `max_message_size` is set to `max_message_size`, - - `max_capacity` is set to `max_places`. + - `max_message_size` is set to `proposed_max_message_size`, + - `max_capacity` is set to `proposed_max_capacity`. * `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. diff --git a/roadmap/implementers-guide/src/types/runtime.md b/roadmap/implementers-guide/src/types/runtime.md index fc1868c32c60..fbadbfabe862 100644 --- a/roadmap/implementers-guide/src/types/runtime.md +++ b/roadmap/implementers-guide/src/types/runtime.md @@ -67,9 +67,9 @@ struct HostConfiguration { /// The deposit that the recipient should provide for accepting opening an HRMP channel. pub hrmp_recipient_deposit: u32, /// The maximum number of messages allowed in an HRMP channel at once. - pub hrmp_channel_max_places: u32, + pub hrmp_channel_max_capacity: u32, /// The maximum total size of messages in bytes allowed in an HRMP channel at once. - pub hrmp_channel_max_size: u32, + pub hrmp_channel_max_total_size: u32, /// The maximum number of inbound HRMP channels a parachain is allowed to accept. pub hrmp_max_parachain_inbound_channels: u32, /// The maximum number of inbound HRMP channels a parathread is allowed to accept. From 687ee7c33b9b376ee8eae3511d51cbf911dcd177 Mon Sep 17 00:00:00 2001 From: Sergey Shulepov Date: Wed, 28 Oct 2020 20:04:02 +0100 Subject: [PATCH 15/20] HRMP: Constrain the maximum number of HRMP messages per candidate This commit addresses the HRMP part of https://github.com/paritytech/polkadot/issues/1869 --- roadmap/implementers-guide/src/runtime/router.md | 1 + roadmap/implementers-guide/src/types/runtime.md | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/roadmap/implementers-guide/src/runtime/router.md b/roadmap/implementers-guide/src/runtime/router.md index 3125e59145bb..64306fb89665 100644 --- a/roadmap/implementers-guide/src/runtime/router.md +++ b/roadmap/implementers-guide/src/runtime/router.md @@ -189,6 +189,7 @@ Candidate Acceptance Function: 1. equal to the context's block number 1. or in `HrmpChannelDigests` for `P` an entry with the block number should exist * `check_outbound_hrmp(sender: ParaId, Vec)`: + 1. Checks that there are at most `config.hrmp_max_message_num_per_candidate` messages. 1. Checks that horizontal messages are sorted by ascending recipient ParaId and there is no two horizontal messages have the same recipient. 1. For each horizontal message `M` with the channel `C` identified by `(sender, M.recipient)` check: 1. exists diff --git a/roadmap/implementers-guide/src/types/runtime.md b/roadmap/implementers-guide/src/types/runtime.md index fbadbfabe862..d3e1da7916eb 100644 --- a/roadmap/implementers-guide/src/types/runtime.md +++ b/roadmap/implementers-guide/src/types/runtime.md @@ -75,10 +75,16 @@ struct HostConfiguration { /// The maximum number of inbound HRMP channels a parathread is allowed to accept. pub hrmp_max_parathread_inbound_channels: u32, /// The maximum size of a message that could ever be put into an HRMP channel. + /// + /// This parameter affects the upper bound of size of `CandidateCommitments`. pub hrmp_channel_max_message_size: u32, /// The maximum number of outbound HRMP channels a parachain is allowed to open. pub hrmp_max_parachain_outbound_channels: u32, /// The maximum number of outbound HRMP channels a parathread is allowed to open. pub hrmp_max_parathread_outbound_channels: u32, + /// The maximum number of outbound HRMP messages can be sent by a candidate. + /// + /// This parameter affects the upper bound of size of `CandidateCommitments`. + pub hrmp_max_message_num_per_candidate: u32, } ``` From 8b23e809afa4097242ef5a4ad3199f85a0ee25b3 Mon Sep 17 00:00:00 2001 From: Sergei Shulepov Date: Wed, 30 Sep 2020 16:13:34 +0200 Subject: [PATCH 16/20] XCM: Introduce HRMP related message types --- xcm/src/v0/mod.rs | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/xcm/src/v0/mod.rs b/xcm/src/v0/mod.rs index d371f1e80976..67b6e4026c33 100644 --- a/xcm/src/v0/mod.rs +++ b/xcm/src/v0/mod.rs @@ -158,6 +158,51 @@ pub enum Xcm { /// /// Errors: RelayedFrom { superorigin: MultiLocation, inner: Box }, + + /// A message to notify about a new incoming HRMP channel. This message is meant to be sent by the + /// relay-chain to a para. + /// + /// - `sender`: The sender in the to-be opened channel. Also, the initiator of the channel opening. + /// - `max_message_size`: The maximum size of a message proposed by the sender. + /// - `max_capacity`: The maximum number of messages that can be queued in the channel. + /// + /// Safety: The message should originate directly from the relay-chain. + /// + /// Kind: *System Notification* + HrmpNewChannelOpenRequest { + #[codec(compact)] sender: u32, + #[codec(compact)] max_message_size: u32, + #[codec(compact)] max_capacity: u32, + }, + + /// A message to notify about that a previously sent open channel request has been accepted by + /// the recipient. That means that the channel will be opened during the next relay-chain session + /// change. This message is meant to be sent by the relay-chain to a para. + /// + /// Safety: The message should originate directly from the relay-chain. + /// + /// Kind: *System Notification* + /// + /// Errors: + HrmpChannelAccepted { + #[codec(compact)] recipient: u32, + }, + + /// A message to notify that the other party in an open channel decided to close it. In particular, + /// `inititator` is going to close the channel opened from `sender` to the `recipient`. The close + /// will be enacted at the next relay-chain session change. This message is meant to be sent by + /// the relay-chain to a para. + /// + /// Safety: The message should originate directly from the relay-chain. + /// + /// Kind: *System Notification* + /// + /// Errors: + HrmpChannelClosing { + #[codec(compact)] initiator: u32, + #[codec(compact)] sender: u32, + #[codec(compact)] recipient: u32, + }, } impl From for VersionedXcm { From ca2315610300f5f165bbe3c1e581565ce55600ae Mon Sep 17 00:00:00 2001 From: Sergei Shulepov Date: Mon, 14 Sep 2020 20:01:43 +0200 Subject: [PATCH 17/20] HRMP: Data structures and plumbing --- core-primitives/src/lib.rs | 19 +++++++++++++++++++ node/collation-generation/src/lib.rs | 4 ++++ node/core/backing/src/lib.rs | 10 ++++++++++ node/core/candidate-selection/src/lib.rs | 2 ++ node/core/candidate-validation/src/lib.rs | 6 ++++++ node/primitives/src/lib.rs | 8 ++++++-- parachain/src/primitives.rs | 18 ++++++++++++++++-- .../adder/src/wasm_validation.rs | 9 ++++++--- primitives/src/v1.rs | 18 +++++++++++++----- 9 files changed, 82 insertions(+), 12 deletions(-) diff --git a/core-primitives/src/lib.rs b/core-primitives/src/lib.rs index d3f567f66a00..97d1222231f2 100644 --- a/core-primitives/src/lib.rs +++ b/core-primitives/src/lib.rs @@ -94,6 +94,25 @@ pub struct InboundDownwardMessage { pub msg: DownwardMessage, } +/// An HRMP message seen from the perspective of a recipient. +#[derive(codec::Encode, codec::Decode, Clone, sp_runtime::RuntimeDebug, PartialEq)] +pub struct InboundHrmpMessage { + /// The block number at which this message was sent. + /// Specifically, it is the block number at which the candidate that sends this message was + /// enacted. + pub sent_at: BlockNumber, + /// The message payload. + pub data: sp_std::vec::Vec, +} + +#[derive(codec::Encode, codec::Decode, Clone, sp_runtime::RuntimeDebug, PartialEq, Eq, Hash)] +pub struct OutboundHrmpMessage { + /// The para that will get this message in its downward message queue. + pub recipient: Id, + /// The message payload. + pub data: sp_std::vec::Vec, +} + /// V1 primitives. pub mod v1 { pub use super::*; diff --git a/node/collation-generation/src/lib.rs b/node/collation-generation/src/lib.rs index 564769c0d63c..67e46a943723 100644 --- a/node/collation-generation/src/lib.rs +++ b/node/collation-generation/src/lib.rs @@ -276,10 +276,12 @@ async fn handle_new_activations( let commitments = CandidateCommitments { upward_messages: collation.upward_messages, + horizontal_messages: collation.horizontal_messages, new_validation_code: collation.new_validation_code, head_data: collation.head_data, erasure_root, processed_downward_messages: collation.processed_downward_messages, + hrmp_watermark: collation.hrmp_watermark, }; let ccr = CandidateReceipt { @@ -384,12 +386,14 @@ mod tests { fn test_collation() -> Collation { Collation { upward_messages: Default::default(), + horizontal_messages: Default::default(), new_validation_code: Default::default(), head_data: Default::default(), proof_of_validity: PoV { block_data: BlockData(Vec::new()), }, processed_downward_messages: Default::default(), + hrmp_watermark: Default::default(), } } diff --git a/node/core/backing/src/lib.rs b/node/core/backing/src/lib.rs index 9b3102d97b97..aeb6cb955091 100644 --- a/node/core/backing/src/lib.rs +++ b/node/core/backing/src/lib.rs @@ -678,10 +678,12 @@ impl CandidateBackingJob { let commitments = CandidateCommitments { upward_messages: outputs.upward_messages, + horizontal_messages: outputs.horizontal_messages, erasure_root, new_validation_code: outputs.new_validation_code, head_data: outputs.head_data, processed_downward_messages: outputs.processed_downward_messages, + hrmp_watermark: outputs.hrmp_watermark, }; let res = match with_commitments(commitments) { @@ -1158,9 +1160,11 @@ mod tests { tx.send(Ok( ValidationResult::Valid(ValidationOutputs { head_data: expected_head_data.clone(), + horizontal_messages: Vec::new(), upward_messages: Vec::new(), new_validation_code: None, processed_downward_messages: 0, + hrmp_watermark: 0, }, test_state.validation_data.persisted), )).unwrap(); } @@ -1278,8 +1282,10 @@ mod tests { ValidationResult::Valid(ValidationOutputs { head_data: expected_head_data.clone(), upward_messages: Vec::new(), + horizontal_messages: Vec::new(), new_validation_code: None, processed_downward_messages: 0, + hrmp_watermark: 0, }, test_state.validation_data.persisted), )).unwrap(); } @@ -1416,8 +1422,10 @@ mod tests { ValidationResult::Valid(ValidationOutputs { head_data: expected_head_data.clone(), upward_messages: Vec::new(), + horizontal_messages: Vec::new(), new_validation_code: None, processed_downward_messages: 0, + hrmp_watermark: 0, }, test_state.validation_data.persisted), )).unwrap(); } @@ -1571,8 +1579,10 @@ mod tests { ValidationResult::Valid(ValidationOutputs { head_data: expected_head_data.clone(), upward_messages: Vec::new(), + horizontal_messages: Vec::new(), new_validation_code: None, processed_downward_messages: 0, + hrmp_watermark: 0, }, test_state.validation_data.persisted), )).unwrap(); } diff --git a/node/core/candidate-selection/src/lib.rs b/node/core/candidate-selection/src/lib.rs index 4399a90f5994..2dd36de9eb93 100644 --- a/node/core/candidate-selection/src/lib.rs +++ b/node/core/candidate-selection/src/lib.rs @@ -492,8 +492,10 @@ mod tests { ValidationOutputs { head_data: HeadData(head_data), upward_messages: Vec::new(), + horizontal_messages: Vec::new(), new_validation_code: None, processed_downward_messages: 0, + hrmp_watermark: 0, }, PersistedValidationData { parent_head: HeadData(parent_head_data), diff --git a/node/core/candidate-validation/src/lib.rs b/node/core/candidate-validation/src/lib.rs index 2e0c87136601..b0d1a489437d 100644 --- a/node/core/candidate-validation/src/lib.rs +++ b/node/core/candidate-validation/src/lib.rs @@ -489,8 +489,10 @@ fn validate_candidate_exhaustive( let outputs = ValidationOutputs { head_data: res.head_data, upward_messages: res.upward_messages, + horizontal_messages: res.horizontal_messages, new_validation_code: res.new_validation_code, processed_downward_messages: res.processed_downward_messages, + hrmp_watermark: res.hrmp_watermark, }; Ok(ValidationResult::Valid(outputs, persisted_validation_data)) } @@ -833,7 +835,9 @@ mod tests { head_data: HeadData(vec![1, 1, 1]), new_validation_code: Some(vec![2, 2, 2].into()), upward_messages: Vec::new(), + horizontal_messages: Vec::new(), processed_downward_messages: 0, + hrmp_watermark: 0, }; let v = validate_candidate_exhaustive::( @@ -848,7 +852,9 @@ mod tests { assert_matches!(v, ValidationResult::Valid(outputs, used_validation_data) => { assert_eq!(outputs.head_data, HeadData(vec![1, 1, 1])); assert_eq!(outputs.upward_messages, Vec::::new()); + assert_eq!(outputs.horizontal_messages, Vec::new()); assert_eq!(outputs.new_validation_code, Some(vec![2, 2, 2].into())); + assert_eq!(outputs.hrmp_watermark, 0); assert_eq!(used_validation_data, validation_data); }); } diff --git a/node/primitives/src/lib.rs b/node/primitives/src/lib.rs index ddcec114ebe2..a80e5a96d920 100644 --- a/node/primitives/src/lib.rs +++ b/node/primitives/src/lib.rs @@ -28,7 +28,7 @@ use polkadot_primitives::v1::{ Hash, CommittedCandidateReceipt, CandidateReceipt, CompactStatement, EncodeAs, Signed, SigningContext, ValidatorIndex, ValidatorId, UpwardMessage, ValidationCode, PersistedValidationData, ValidationData, - HeadData, PoV, CollatorPair, Id as ParaId, ValidationOutputs, + HeadData, PoV, CollatorPair, Id as ParaId, OutboundHrmpMessage, ValidationOutputs, }; use polkadot_statement_table::{ generic::{ @@ -252,9 +252,11 @@ impl std::convert::TryFrom for MisbehaviorReport { /// - does not contain the erasure root; that's computed at the Polkadot level, not at Cumulus /// - contains a proof of validity. #[derive(Clone, Encode, Decode)] -pub struct Collation { +pub struct Collation { /// Messages destined to be interpreted by the Relay chain itself. pub upward_messages: Vec, + /// The horizontal messages sent by the parachain. + pub horizontal_messages: Vec>, /// New validation code. pub new_validation_code: Option, /// The head-data produced as a result of execution. @@ -263,6 +265,8 @@ pub struct Collation { pub proof_of_validity: PoV, /// The number of messages processed from the DMQ. pub processed_downward_messages: u32, + /// The mark which specifies the block number up to which all inbound HRMP messages are processed. + pub hrmp_watermark: BlockNumber, } /// Configuration for the collation generator diff --git a/parachain/src/primitives.rs b/parachain/src/primitives.rs index 7387de99d66d..5e7ec9e0c302 100644 --- a/parachain/src/primitives.rs +++ b/parachain/src/primitives.rs @@ -28,7 +28,7 @@ use serde::{Serialize, Deserialize}; #[cfg(feature = "std")] use sp_core::bytes; -use polkadot_core_primitives::Hash; +use polkadot_core_primitives::{Hash, OutboundHrmpMessage}; /// Block number type used by the relay chain. pub use polkadot_core_primitives::BlockNumber as RelayChainBlockNumber; @@ -206,6 +206,16 @@ impl AccountIdConversion for Id { } } +/// A type that uniquely identifies an HRMP channel. An HRMP channel is unidirectional +#[derive(Clone, PartialEq, Eq, Encode, Decode, RuntimeDebug)] +#[cfg_attr(feature = "std", derive(Hash))] +pub struct HrmpChannelId { + /// The para that acts as the sender in this channel. + pub sender: Id, + /// The para that acts as the recipient in this channel. + pub recipient: Id, +} + /// A message from a parachain to its Relay Chain. pub type UpwardMessage = Vec; @@ -232,7 +242,7 @@ pub struct ValidationParams { } /// The result of parachain validation. -// TODO: egress and balance uploads (https://github.com/paritytech/polkadot/issues/220) +// TODO: balance uploads (https://github.com/paritytech/polkadot/issues/220) #[derive(PartialEq, Eq, Encode)] #[cfg_attr(feature = "std", derive(Debug, Decode))] pub struct ValidationResult { @@ -242,8 +252,12 @@ pub struct ValidationResult { pub new_validation_code: Option, /// Upward messages send by the Parachain. pub upward_messages: Vec, + /// Outbound horizontal messages sent by the parachain. + pub horizontal_messages: Vec>, /// Number of downward messages that were processed by the Parachain. /// /// It is expected that the Parachain processes them from first to last. pub processed_downward_messages: u32, + /// The mark which specifies the block number up to which all inbound HRMP messages are processed. + pub hrmp_watermark: RelayChainBlockNumber, } diff --git a/parachain/test-parachains/adder/src/wasm_validation.rs b/parachain/test-parachains/adder/src/wasm_validation.rs index c0f3b56dc8e4..0283c923f24f 100644 --- a/parachain/test-parachains/adder/src/wasm_validation.rs +++ b/parachain/test-parachains/adder/src/wasm_validation.rs @@ -17,12 +17,13 @@ //! WASM validation for adder parachain. use crate::{HeadData, BlockData}; -use core::{intrinsics, panic}; +use core::panic; +use sp_std::vec::Vec; use parachain::primitives::{ValidationResult, HeadData as GenericHeadData}; use codec::{Encode, Decode}; #[no_mangle] -pub extern fn validate_block(params: *const u8, len: usize) -> u64 { +pub extern "C" fn validate_block(params: *const u8, len: usize) -> u64 { let params = unsafe { parachain::load_params(params, len) }; let parent_head = HeadData::decode(&mut ¶ms.parent_head.0[..]) .expect("invalid parent head format."); @@ -37,8 +38,10 @@ pub extern fn validate_block(params: *const u8, len: usize) -> u64 { &ValidationResult { head_data: GenericHeadData(new_head.encode()), new_validation_code: None, - upward_messages: sp_std::vec::Vec::new(), + upward_messages: Vec::new(), + horizontal_messages: Vec::new(), processed_downward_messages: 0, + hrmp_watermark: params.relay_chain_height, } ), Err(_) => panic!("execution failure"), diff --git a/primitives/src/v1.rs b/primitives/src/v1.rs index 5347849f7f81..9b810842f17d 100644 --- a/primitives/src/v1.rs +++ b/primitives/src/v1.rs @@ -29,14 +29,14 @@ pub use runtime_primitives::traits::{BlakeTwo256, Hash as HashT}; // Export some core primitives. pub use polkadot_core_primitives::v1::{ - BlockNumber, Moment, Signature, AccountPublic, AccountId, AccountIndex, - ChainId, Hash, Nonce, Balance, Header, Block, BlockId, UncheckedExtrinsic, - Remark, DownwardMessage, InboundDownwardMessage, + BlockNumber, Moment, Signature, AccountPublic, AccountId, AccountIndex, ChainId, Hash, Nonce, + Balance, Header, Block, BlockId, UncheckedExtrinsic, Remark, DownwardMessage, + InboundDownwardMessage, InboundHrmpMessage, OutboundHrmpMessage, }; // Export some polkadot-parachain primitives pub use polkadot_parachain::primitives::{ - Id, LOWEST_USER_ID, UpwardMessage, HeadData, BlockData, ValidationCode, + Id, LOWEST_USER_ID, HrmpChannelId, UpwardMessage, HeadData, BlockData, ValidationCode, }; // Export some basic parachain primitives from v0. @@ -317,18 +317,24 @@ pub struct ValidationOutputs { pub head_data: HeadData, /// Upward messages to the relay chain. pub upward_messages: Vec, + /// The horizontal messages sent by the parachain. + pub horizontal_messages: Vec>, /// The new validation code submitted by the execution, if any. pub new_validation_code: Option, /// The number of messages processed from the DMQ. pub processed_downward_messages: u32, + /// The mark which specifies the block number up to which all inbound HRMP messages are processed. + pub hrmp_watermark: BlockNumber, } /// Commitments made in a `CandidateReceipt`. Many of these are outputs of validation. #[derive(PartialEq, Eq, Clone, Encode, Decode)] #[cfg_attr(feature = "std", derive(Debug, Default, Hash))] -pub struct CandidateCommitments { +pub struct CandidateCommitments { /// Messages destined to be interpreted by the Relay chain itself. pub upward_messages: Vec, + /// Horizontal messages sent by the parachain. + pub horizontal_messages: Vec>, /// The root of a block's erasure encoding Merkle tree. pub erasure_root: Hash, /// New validation code. @@ -337,6 +343,8 @@ pub struct CandidateCommitments { pub head_data: HeadData, /// The number of messages processed from the DMQ. pub processed_downward_messages: u32, + /// The mark which specifies the block number up to which all inbound HRMP messages are processed. + pub hrmp_watermark: N, } impl CandidateCommitments { From 423cea6ba10d26f15261c64914de7d516c9fcce2 Mon Sep 17 00:00:00 2001 From: Sergei Shulepov Date: Sat, 19 Sep 2020 20:12:21 +0200 Subject: [PATCH 18/20] HRMP: Configuration --- runtime/parachains/src/configuration.rs | 182 +++++++++++++++++++++++- 1 file changed, 181 insertions(+), 1 deletion(-) diff --git a/runtime/parachains/src/configuration.rs b/runtime/parachains/src/configuration.rs index 3f7f32f39153..1d0454f97e6e 100644 --- a/runtime/parachains/src/configuration.rs +++ b/runtime/parachains/src/configuration.rs @@ -19,7 +19,7 @@ //! Configuration can change only at session boundaries and is buffered until then. use sp_std::prelude::*; -use primitives::v1::ValidatorId; +use primitives::v1::{Balance, ValidatorId}; use frame_support::{ decl_storage, decl_module, decl_error, dispatch::DispatchResult, @@ -84,6 +84,32 @@ pub struct HostConfiguration { /// /// This parameter affects the size upper bound of the `CandidateCommitments`. pub max_upward_message_num_per_candidate: u32, + /// Number of sessions after which an HRMP open channel request expires. + pub hrmp_open_request_ttl: u32, + /// The deposit that the sender should provide for opening an HRMP channel. + pub hrmp_sender_deposit: Balance, + /// The deposit that the recipient should provide for accepting opening an HRMP channel. + pub hrmp_recipient_deposit: Balance, + /// The maximum number of messages allowed in an HRMP channel at once. + pub hrmp_channel_max_capacity: u32, + /// The maximum total size of messages in bytes allowed in an HRMP channel at once. + pub hrmp_channel_max_total_size: u32, + /// The maximum number of inbound HRMP channels a parachain is allowed to accept. + pub hrmp_max_parachain_inbound_channels: u32, + /// The maximum number of inbound HRMP channels a parathread is allowed to accept. + pub hrmp_max_parathread_inbound_channels: u32, + /// The maximum size of a message that could ever be put into an HRMP channel. + /// + /// This parameter affects the upper bound of size of `CandidateCommitments`. + pub hrmp_channel_max_message_size: u32, + /// The maximum number of outbound HRMP channels a parachain is allowed to open. + pub hrmp_max_parachain_outbound_channels: u32, + /// The maximum number of outbound HRMP channels a parathread is allowed to open. + pub hrmp_max_parathread_outbound_channels: u32, + /// The maximum number of outbound HRMP messages can be sent by a candidate. + /// + /// This parameter affects the upper bound of size of `CandidateCommitments`. + pub hrmp_max_message_num_per_candidate: u32, } pub trait Trait: frame_system::Trait { } @@ -276,6 +302,105 @@ decl_module! { }); Ok(()) } + + #[weight = (1_000, DispatchClass::Operational)] + pub fn set_hrmp_open_request_ttl(origin, new: u32) -> DispatchResult { + ensure_root(origin)?; + Self::update_config_member(|config| { + sp_std::mem::replace(&mut config.hrmp_open_request_ttl, new) != new + }); + Ok(()) + } + + #[weight = (1_000, DispatchClass::Operational)] + pub fn set_hrmp_sender_deposit(origin, new: Balance) -> DispatchResult { + ensure_root(origin)?; + Self::update_config_member(|config| { + sp_std::mem::replace(&mut config.hrmp_sender_deposit, new) != new + }); + Ok(()) + } + + #[weight = (1_000, DispatchClass::Operational)] + pub fn set_hrmp_recipient_deposit(origin, new: Balance) -> DispatchResult { + ensure_root(origin)?; + Self::update_config_member(|config| { + sp_std::mem::replace(&mut config.hrmp_recipient_deposit, new) != new + }); + Ok(()) + } + + #[weight = (1_000, DispatchClass::Operational)] + pub fn set_hrmp_channel_max_capacity(origin, new: u32) -> DispatchResult { + ensure_root(origin)?; + Self::update_config_member(|config| { + sp_std::mem::replace(&mut config.hrmp_channel_max_capacity, new) != new + }); + Ok(()) + } + + #[weight = (1_000, DispatchClass::Operational)] + pub fn set_hrmp_channel_max_total_size(origin, new: u32) -> DispatchResult { + ensure_root(origin)?; + Self::update_config_member(|config| { + sp_std::mem::replace(&mut config.hrmp_channel_max_total_size, new) != new + }); + Ok(()) + } + + #[weight = (1_000, DispatchClass::Operational)] + pub fn set_hrmp_max_parachain_inbound_channels(origin, new: u32) -> DispatchResult { + ensure_root(origin)?; + Self::update_config_member(|config| { + sp_std::mem::replace(&mut config.hrmp_max_parachain_inbound_channels, new) != new + }); + Ok(()) + } + + #[weight = (1_000, DispatchClass::Operational)] + pub fn set_hrmp_max_parathread_inbound_channels(origin, new: u32) -> DispatchResult { + ensure_root(origin)?; + Self::update_config_member(|config| { + sp_std::mem::replace(&mut config.hrmp_max_parathread_inbound_channels, new) != new + }); + Ok(()) + } + + #[weight = (1_000, DispatchClass::Operational)] + pub fn set_hrmp_channel_max_message_size(origin, new: u32) -> DispatchResult { + ensure_root(origin)?; + Self::update_config_member(|config| { + sp_std::mem::replace(&mut config.hrmp_channel_max_message_size, new) != new + }); + Ok(()) + } + + #[weight = (1_000, DispatchClass::Operational)] + pub fn set_hrmp_max_parachain_outbound_channels(origin, new: u32) -> DispatchResult { + ensure_root(origin)?; + Self::update_config_member(|config| { + sp_std::mem::replace(&mut config.hrmp_max_parachain_outbound_channels, new) != new + }); + Ok(()) + } + + #[weight = (1_000, DispatchClass::Operational)] + pub fn set_hrmp_max_parathread_outbound_channels(origin, new: u32) -> DispatchResult { + ensure_root(origin)?; + Self::update_config_member(|config| { + sp_std::mem::replace(&mut config.hrmp_max_parathread_outbound_channels, new) != new + }); + Ok(()) + } + + #[weight = (1_000, DispatchClass::Operational)] + pub fn set_hrmp_max_message_num_per_candidate(origin, new: u32) -> DispatchResult { + ensure_root(origin)?; + Self::update_config_member(|config| { + sp_std::mem::replace(&mut config.hrmp_max_message_num_per_candidate, new) != new + }); + Ok(()) + } } } @@ -360,6 +485,17 @@ mod tests { preferred_dispatchable_upward_messages_step_weight: 20000, max_upward_message_size: 448, max_upward_message_num_per_candidate: 5, + hrmp_open_request_ttl: 1312, + hrmp_sender_deposit: 22, + hrmp_recipient_deposit: 4905, + hrmp_channel_max_capacity: 3921, + hrmp_channel_max_total_size: 7687, + hrmp_max_parachain_inbound_channels: 3722, + hrmp_max_parathread_inbound_channels: 1967, + hrmp_channel_max_message_size: 8192, + hrmp_max_parachain_outbound_channels: 100, + hrmp_max_parathread_outbound_channels: 200, + hrmp_max_message_num_per_candidate: 20, }; assert!(::PendingConfig::get().is_none()); @@ -415,6 +551,50 @@ mod tests { Configuration::set_max_upward_message_num_per_candidate( Origin::root(), new_config.max_upward_message_num_per_candidate, ).unwrap(); + Configuration::set_hrmp_open_request_ttl( + Origin::root(), + new_config.hrmp_open_request_ttl, + ).unwrap(); + Configuration::set_hrmp_sender_deposit( + Origin::root(), + new_config.hrmp_sender_deposit, + ).unwrap(); + Configuration::set_hrmp_recipient_deposit( + Origin::root(), + new_config.hrmp_recipient_deposit, + ).unwrap(); + Configuration::set_hrmp_channel_max_capacity( + Origin::root(), + new_config.hrmp_channel_max_capacity, + ).unwrap(); + Configuration::set_hrmp_channel_max_total_size( + Origin::root(), + new_config.hrmp_channel_max_total_size, + ).unwrap(); + Configuration::set_hrmp_max_parachain_inbound_channels( + Origin::root(), + new_config.hrmp_max_parachain_inbound_channels, + ).unwrap(); + Configuration::set_hrmp_max_parathread_inbound_channels( + Origin::root(), + new_config.hrmp_max_parathread_inbound_channels, + ).unwrap(); + Configuration::set_hrmp_channel_max_message_size( + Origin::root(), + new_config.hrmp_channel_max_message_size, + ).unwrap(); + Configuration::set_hrmp_max_parachain_outbound_channels( + Origin::root(), + new_config.hrmp_max_parachain_outbound_channels, + ).unwrap(); + Configuration::set_hrmp_max_parathread_outbound_channels( + Origin::root(), + new_config.hrmp_max_parathread_outbound_channels, + ).unwrap(); + Configuration::set_hrmp_max_message_num_per_candidate( + Origin::root(), + new_config.hrmp_max_message_num_per_candidate, + ).unwrap(); assert_eq!(::PendingConfig::get(), Some(new_config)); }) From 52ee2a7465b87d147d87feb4b671c34dbb820d70 Mon Sep 17 00:00:00 2001 From: Sergei Shulepov Date: Tue, 6 Oct 2020 13:27:15 +0200 Subject: [PATCH 19/20] HRMP: Data layout --- runtime/parachains/src/router.rs | 64 ++++++++++++++++++++++++--- runtime/parachains/src/router/hrmp.rs | 64 +++++++++++++++++++++++++++ 2 files changed, 123 insertions(+), 5 deletions(-) create mode 100644 runtime/parachains/src/router/hrmp.rs diff --git a/runtime/parachains/src/router.rs b/runtime/parachains/src/router.rs index b773bd9c1270..893250a09d0e 100644 --- a/runtime/parachains/src/router.rs +++ b/runtime/parachains/src/router.rs @@ -20,18 +20,19 @@ //! routing the messages at their destinations and informing the parachains about the incoming //! messages. -use crate::{ - configuration, - initializer, -}; +use crate::{configuration, initializer}; use sp_std::prelude::*; use frame_support::{decl_error, decl_module, decl_storage, weights::Weight}; use sp_std::collections::vec_deque::VecDeque; -use primitives::v1::{Id as ParaId, InboundDownwardMessage, Hash, UpwardMessage}; +use primitives::v1::{ + Id as ParaId, InboundDownwardMessage, Hash, UpwardMessage, HrmpChannelId, InboundHrmpMessage, +}; +mod hrmp; mod dmp; mod ump; +use hrmp::{HrmpOpenChannelRequest, HrmpChannel}; pub use dmp::QueueDownwardMessageError; pub use ump::UmpSink; @@ -103,6 +104,59 @@ decl_storage! { /// Invariant: /// - If `Some(para)`, then `para` must be present in `NeedsDispatch`. NextDispatchRoundStartWith: Option; + + /* + * Horizontally Relay-routed Message Passing (HRMP) + * + * HRMP related storage layout + */ + + /// The set of pending HRMP open channel requests. + /// + /// The set is accompanied by a list for iteration. + /// + /// Invariant: + /// - There are no channels that exists in list but not in the set and vice versa. + HrmpOpenChannelRequests: map hasher(twox_64_concat) HrmpChannelId => Option; + HrmpOpenChannelRequestsList: Vec; + + /// This mapping tracks how many open channel requests are inititated by a given sender para. + /// Invariant: `HrmpOpenChannelRequests` should contain the same number of items that has `(X, _)` + /// as the number of `HrmpOpenChannelRequestCount` for `X`. + HrmpOpenChannelRequestCount: map hasher(twox_64_concat) ParaId => u32; + /// This mapping tracks how many open channel requests were accepted by a given recipient para. + /// Invariant: `HrmpOpenChannelRequests` should contain the same number of items `(_, X)` with + /// `confirmed` set to true, as the number of `HrmpAcceptedChannelRequestCount` for `X`. + HrmpAcceptedChannelRequestCount: map hasher(twox_64_concat) ParaId => u32; + + /// A set of pending HRMP close channel requests that are going to be closed during the session change. + /// Used for checking if a given channel is registered for closure. + /// + /// The set is accompanied by a list for iteration. + /// + /// Invariant: + /// - There are no channels that exists in list but not in the set and vice versa. + HrmpCloseChannelRequests: map hasher(twox_64_concat) HrmpChannelId => Option<()>; + HrmpCloseChannelRequestsList: Vec; + + /// The HRMP watermark associated with each para. + HrmpWatermarks: map hasher(twox_64_concat) ParaId => Option; + /// HRMP channel data associated with each para. + HrmpChannels: map hasher(twox_64_concat) HrmpChannelId => Option; + /// The indexes that map all senders to their recievers and vise versa. + /// Invariants: + /// - for each ingress index entry for `P` each item `I` in the index should present in `HrmpChannels` as `(I, P)`. + /// - for each egress index entry for `P` each item `E` in the index should present in `HrmpChannels` as `(P, E)`. + /// - there should be no other dangling channels in `HrmpChannels`. + HrmpIngressChannelsIndex: map hasher(twox_64_concat) ParaId => Vec; + HrmpEgressChannelsIndex: map hasher(twox_64_concat) ParaId => Vec; + /// Storage for the messages for each channel. + /// Invariant: cannot be non-empty if the corresponding channel in `HrmpChannels` is `None`. + HrmpChannelContents: map hasher(twox_64_concat) HrmpChannelId => Vec>; + /// Maintains a mapping that can be used to answer the question: + /// What paras sent a message at the given block number for a given reciever. + /// Invariant: The para ids vector is never empty. + HrmpChannelDigests: map hasher(twox_64_concat) ParaId => Vec<(T::BlockNumber, Vec)>; } } diff --git a/runtime/parachains/src/router/hrmp.rs b/runtime/parachains/src/router/hrmp.rs new file mode 100644 index 000000000000..1e8d9d1a4d7c --- /dev/null +++ b/runtime/parachains/src/router/hrmp.rs @@ -0,0 +1,64 @@ +// 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 . + +use primitives::v1::{Balance, Hash, SessionIndex}; +use codec::{Encode, Decode}; + +/// A description of a request to open an HRMP channel. +#[derive(Encode, Decode)] +pub struct HrmpOpenChannelRequest { + /// Indicates if this request was confirmed by the recipient. + pub confirmed: bool, + /// How many session boundaries ago this request was seen. + pub age: SessionIndex, + /// The amount that the sender supplied at the time of creation of this request. + pub sender_deposit: Balance, + /// The maximum message size that could be put into the channel. + pub max_message_size: u32, + /// The maximum number of messages that can be pending in the channel at once. + pub max_capacity: u32, + /// The maximum total size of the messages that can be pending in the channel at once. + pub max_total_size: u32, +} + +/// A metadata of an HRMP channel. +#[derive(Encode, Decode)] +pub struct HrmpChannel { + /// The amount that the sender supplied as a deposit when opening this channel. + pub sender_deposit: Balance, + /// The amount that the recipient supplied as a deposit when accepting opening this channel. + pub recipient_deposit: Balance, + /// The maximum number of messages that can be pending in the channel at once. + pub max_capacity: u32, + /// The maximum total size of the messages that can be pending in the channel at once. + pub max_total_size: u32, + /// The maximum message size that could be put into the channel. + pub max_message_size: u32, + /// The current number of messages pending in the channel. + /// Invariant: should be less or equal to `max_capacity`.s`. + pub msg_count: u32, + /// The total size in bytes of all message payloads in the channel. + /// Invariant: should be less or equal to `max_total_size`. + pub total_size: u32, + /// A head of the Message Queue Chain for this channel. Each link in this chain has a form: + /// `(prev_head, B, H(M))`, where + /// - `prev_head`: is the previous value of `mqc_head` or zero if none. + /// - `B`: is the [relay-chain] block number in which a message was appended + /// - `H(M)`: is the hash of the message being appended. + /// This value is initialized to a special value that consists of all zeroes which indicates + /// that no messages were previously added. + pub mqc_head: Option, +} From 8ff407e5caaa7497b857d679a55f5a61119209d9 Mon Sep 17 00:00:00 2001 From: Sergei Shulepov Date: Wed, 23 Sep 2020 17:02:47 +0200 Subject: [PATCH 20/20] HRMP: Acceptance & Enactment --- Cargo.lock | 1 + runtime/common/src/paras_registrar.rs | 1 + runtime/parachains/Cargo.toml | 2 + runtime/parachains/src/inclusion.rs | 60 +++++ runtime/parachains/src/mock.rs | 1 + runtime/parachains/src/paras.rs | 6 + runtime/parachains/src/router.rs | 274 ++++++++++++++++++- runtime/parachains/src/router/hrmp.rs | 375 +++++++++++++++++++++++++- runtime/parachains/src/util.rs | 3 +- runtime/rococo-v1/src/lib.rs | 1 + runtime/test-runtime/src/lib.rs | 1 + 11 files changed, 716 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 867faeafbb39..8b457aa0cf20 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5433,6 +5433,7 @@ dependencies = [ "sp-std", "sp-trie", "sp-version", + "xcm", ] [[package]] diff --git a/runtime/common/src/paras_registrar.rs b/runtime/common/src/paras_registrar.rs index ab58878abddd..dab0bb02e250 100644 --- a/runtime/common/src/paras_registrar.rs +++ b/runtime/common/src/paras_registrar.rs @@ -427,6 +427,7 @@ mod tests { impl router::Trait for Test { type UmpSink = (); + type Origin = Origin; } impl pallet_session::historical::Trait for Test { diff --git a/runtime/parachains/Cargo.toml b/runtime/parachains/Cargo.toml index 25d65b8da34c..a19bfe6b0b99 100644 --- a/runtime/parachains/Cargo.toml +++ b/runtime/parachains/Cargo.toml @@ -34,6 +34,7 @@ pallet-vesting = { git = "https://github.com/paritytech/substrate", branch = "ma pallet-offences = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false } frame-benchmarking = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false, optional = true } +xcm = { package = "xcm", path = "../../xcm", default-features = false } primitives = { package = "polkadot-primitives", path = "../../primitives", default-features = false } libsecp256k1 = { version = "0.3.2", default-features = false, optional = true } @@ -84,6 +85,7 @@ std = [ "frame-system/std", "pallet-timestamp/std", "pallet-vesting/std", + "xcm/std", ] runtime-benchmarks = [ "libsecp256k1/hmac", diff --git a/runtime/parachains/src/inclusion.rs b/runtime/parachains/src/inclusion.rs index d5a6e92674b8..8947f1324aa9 100644 --- a/runtime/parachains/src/inclusion.rs +++ b/runtime/parachains/src/inclusion.rs @@ -157,6 +157,10 @@ decl_error! { IncorrectDownwardMessageHandling, /// At least one upward message sent does not pass the acceptance criteria. InvalidUpwardMessages, + /// The candidate didn't follow the rules of HRMP watermark advancement. + HrmpWatermarkMishandling, + /// The HRMP messages sent by the candidate is not valid. + InvalidOutboundHrmp, } } @@ -415,6 +419,8 @@ impl Module { &candidate.candidate.commitments.new_validation_code, candidate.candidate.commitments.processed_downward_messages, &candidate.candidate.commitments.upward_messages, + candidate.candidate.commitments.hrmp_watermark, + &candidate.candidate.commitments.horizontal_messages, )?; for (i, assignment) in scheduled[skip..].iter().enumerate() { @@ -548,6 +554,8 @@ impl Module { &validation_outputs.new_validation_code, validation_outputs.processed_downward_messages, &validation_outputs.upward_messages, + validation_outputs.hrmp_watermark, + &validation_outputs.horizontal_messages, ) } @@ -578,6 +586,14 @@ impl Module { receipt.descriptor.para_id, commitments.upward_messages, ); + weight += >::prune_hrmp( + receipt.descriptor.para_id, + T::BlockNumber::from(commitments.hrmp_watermark), + ); + weight += >::queue_outbound_hrmp( + receipt.descriptor.para_id, + commitments.horizontal_messages, + ); Self::deposit_event( Event::::CandidateIncluded(plain, commitments.head_data.clone()) @@ -702,6 +718,8 @@ impl CandidateCheckContext { new_validation_code: &Option, processed_downward_messages: u32, upward_messages: &[primitives::v1::UpwardMessage], + hrmp_watermark: primitives::v1::BlockNumber, + horizontal_messages: &[primitives::v1::OutboundHrmpMessage], ) -> Result<(), DispatchError> { ensure!( head_data.0.len() <= self.config.max_head_data_size as _, @@ -739,6 +757,23 @@ impl CandidateCheckContext { ), Error::::InvalidUpwardMessages, ); + ensure!( + >::check_hrmp_watermark( + para_id, + self.relay_parent_number, + // TODO: Hmm, we should settle on a single represenation of T::BlockNumber + T::BlockNumber::from(hrmp_watermark), + ), + Error::::HrmpWatermarkMishandling, + ); + ensure!( + >::check_outbound_hrmp( + &self.config, + para_id, + horizontal_messages, + ), + Error::::InvalidOutboundHrmp, + ); Ok(()) } @@ -946,6 +981,7 @@ mod tests { relay_parent: Hash, persisted_validation_data_hash: Hash, new_validation_code: Option, + hrmp_watermark: BlockNumber, } impl TestCandidateBuilder { @@ -961,6 +997,7 @@ mod tests { commitments: CandidateCommitments { head_data: self.head_data, new_validation_code: self.new_validation_code, + hrmp_watermark: self.hrmp_watermark, ..Default::default() }, } @@ -1359,6 +1396,9 @@ mod tests { let chain_b = ParaId::from(2); let thread_a = ParaId::from(3); + // The block number of the relay-parent for testing. + const RELAY_PARENT_NUM: BlockNumber = 4; + let paras = vec![(chain_a, true), (chain_b, true), (thread_a, false)]; let validators = vec![ Sr25519Keyring::Alice, @@ -1421,6 +1461,7 @@ mod tests { relay_parent: System::parent_hash(), pov_hash: Hash::from([1; 32]), persisted_validation_data_hash: make_vdata_hash(chain_a).unwrap(), + hrmp_watermark: RELAY_PARENT_NUM, ..Default::default() }.build(); collator_sign_candidate( @@ -1454,6 +1495,7 @@ mod tests { relay_parent: System::parent_hash(), pov_hash: Hash::from([1; 32]), persisted_validation_data_hash: make_vdata_hash(chain_a).unwrap(), + hrmp_watermark: RELAY_PARENT_NUM, ..Default::default() }.build(); let mut candidate_b = TestCandidateBuilder { @@ -1461,6 +1503,7 @@ mod tests { relay_parent: System::parent_hash(), pov_hash: Hash::from([2; 32]), persisted_validation_data_hash: make_vdata_hash(chain_b).unwrap(), + hrmp_watermark: RELAY_PARENT_NUM, ..Default::default() }.build(); @@ -1510,6 +1553,7 @@ mod tests { relay_parent: System::parent_hash(), pov_hash: Hash::from([1; 32]), persisted_validation_data_hash: make_vdata_hash(chain_a).unwrap(), + hrmp_watermark: RELAY_PARENT_NUM, ..Default::default() }.build(); collator_sign_candidate( @@ -1579,6 +1623,7 @@ mod tests { relay_parent: System::parent_hash(), pov_hash: Hash::from([1; 32]), persisted_validation_data_hash: make_vdata_hash(thread_a).unwrap(), + hrmp_watermark: RELAY_PARENT_NUM, ..Default::default() }.build(); @@ -1618,6 +1663,7 @@ mod tests { relay_parent: System::parent_hash(), pov_hash: Hash::from([1; 32]), persisted_validation_data_hash: make_vdata_hash(thread_a).unwrap(), + hrmp_watermark: RELAY_PARENT_NUM, ..Default::default() }.build(); @@ -1656,6 +1702,7 @@ mod tests { relay_parent: System::parent_hash(), pov_hash: Hash::from([1; 32]), persisted_validation_data_hash: make_vdata_hash(chain_a).unwrap(), + hrmp_watermark: RELAY_PARENT_NUM, ..Default::default() }.build(); @@ -1703,6 +1750,7 @@ mod tests { relay_parent: System::parent_hash(), pov_hash: Hash::from([1; 32]), persisted_validation_data_hash: make_vdata_hash(chain_a).unwrap(), + hrmp_watermark: RELAY_PARENT_NUM, ..Default::default() }.build(); @@ -1743,6 +1791,7 @@ mod tests { pov_hash: Hash::from([1; 32]), new_validation_code: Some(vec![5, 6, 7, 8].into()), persisted_validation_data_hash: make_vdata_hash(chain_a).unwrap(), + hrmp_watermark: RELAY_PARENT_NUM, ..Default::default() }.build(); @@ -1785,6 +1834,7 @@ mod tests { relay_parent: System::parent_hash(), pov_hash: Hash::from([1; 32]), persisted_validation_data_hash: [42u8; 32].into(), + hrmp_watermark: RELAY_PARENT_NUM, ..Default::default() }.build(); @@ -1820,6 +1870,9 @@ mod tests { let chain_b = ParaId::from(2); let thread_a = ParaId::from(3); + // The block number of the relay-parent for testing. + const RELAY_PARENT_NUM: BlockNumber = 4; + let paras = vec![(chain_a, true), (chain_b, true), (thread_a, false)]; let validators = vec![ Sr25519Keyring::Alice, @@ -1880,6 +1933,7 @@ mod tests { relay_parent: System::parent_hash(), pov_hash: Hash::from([1; 32]), persisted_validation_data_hash: make_vdata_hash(chain_a).unwrap(), + hrmp_watermark: RELAY_PARENT_NUM, ..Default::default() }.build(); collator_sign_candidate( @@ -1892,6 +1946,7 @@ mod tests { relay_parent: System::parent_hash(), pov_hash: Hash::from([2; 32]), persisted_validation_data_hash: make_vdata_hash(chain_b).unwrap(), + hrmp_watermark: RELAY_PARENT_NUM, ..Default::default() }.build(); collator_sign_candidate( @@ -1904,6 +1959,7 @@ mod tests { relay_parent: System::parent_hash(), pov_hash: Hash::from([3; 32]), persisted_validation_data_hash: make_vdata_hash(thread_a).unwrap(), + hrmp_watermark: RELAY_PARENT_NUM, ..Default::default() }.build(); collator_sign_candidate( @@ -2001,6 +2057,9 @@ mod tests { fn can_include_candidate_with_ok_code_upgrade() { let chain_a = ParaId::from(1); + // The block number of the relay-parent for testing. + const RELAY_PARENT_NUM: BlockNumber = 4; + let paras = vec![(chain_a, true)]; let validators = vec![ Sr25519Keyring::Alice, @@ -2044,6 +2103,7 @@ mod tests { pov_hash: Hash::from([1; 32]), persisted_validation_data_hash: make_vdata_hash(chain_a).unwrap(), new_validation_code: Some(vec![1, 2, 3].into()), + hrmp_watermark: RELAY_PARENT_NUM, ..Default::default() }.build(); collator_sign_candidate( diff --git a/runtime/parachains/src/mock.rs b/runtime/parachains/src/mock.rs index 4fdd6e0e6155..3da3a6448128 100644 --- a/runtime/parachains/src/mock.rs +++ b/runtime/parachains/src/mock.rs @@ -109,6 +109,7 @@ impl crate::paras::Trait for Test { } impl crate::router::Trait for Test { + type Origin = Origin; type UmpSink = crate::router::MockUmpSink; } diff --git a/runtime/parachains/src/paras.rs b/runtime/parachains/src/paras.rs index 4d7f1e2a07c5..84bdf6cf73ad 100644 --- a/runtime/parachains/src/paras.rs +++ b/runtime/parachains/src/paras.rs @@ -541,6 +541,12 @@ impl Module { } } + /// Returns whether the given ID refers to a valid para. + pub(crate) fn is_valid_para(id: ParaId) -> bool { + Self::parachains().binary_search(&id).is_ok() + || Self::is_parathread(id) + } + /// Whether a para ID corresponds to any live parathread. pub(crate) fn is_parathread(id: ParaId) -> bool { Parathreads::get(&id).is_some() diff --git a/runtime/parachains/src/router.rs b/runtime/parachains/src/router.rs index 893250a09d0e..239da3253fbc 100644 --- a/runtime/parachains/src/router.rs +++ b/runtime/parachains/src/router.rs @@ -20,9 +20,11 @@ //! routing the messages at their destinations and informing the parachains about the incoming //! messages. -use crate::{configuration, initializer}; +use crate::{configuration, paras, initializer, ensure_parachain}; use sp_std::prelude::*; -use frame_support::{decl_error, decl_module, decl_storage, weights::Weight}; +use frame_support::{ + decl_error, decl_module, decl_storage, ensure, dispatch::DispatchResult, weights::Weight, +}; use sp_std::collections::vec_deque::VecDeque; use primitives::v1::{ Id as ParaId, InboundDownwardMessage, Hash, UpwardMessage, HrmpChannelId, InboundHrmpMessage, @@ -39,7 +41,11 @@ pub use ump::UmpSink; #[cfg(test)] pub use ump::mock_sink::MockUmpSink; -pub trait Trait: frame_system::Trait + configuration::Trait { +pub trait Trait: frame_system::Trait + configuration::Trait + paras::Trait { + type Origin: From + + From<::Origin> + + Into::Origin>>; + /// A place where all received upward messages are funneled. type UmpSink: UmpSink; } @@ -161,13 +167,262 @@ decl_storage! { } decl_error! { - pub enum Error for Module { } + pub enum Error for Module { + /// The sender tried to open a channel to themselves. + OpenHrmpChannelToSelf, + /// The recipient is not a valid para. + OpenHrmpChannelInvalidRecipient, + /// The requested capacity is zero. + OpenHrmpChannelZeroPlaces, + /// The requested capacity exceeds the global limit. + OpenHrmpChannelTooManyPlaces, + /// The requested maximum message size is 0. + OpenHrmpChannelZeroMessageSize, + /// The open request requested the message size that exceeds the global limit. + OpenHrmpChannelTooBigMessage, + /// The channel already exists + OpenHrmpChannelAlreadyExists, + /// There is already a request to open the same channel. + OpenHrmpChannelAlreadyRequested, + /// The sender already has the maximum number of allowed outbound channels. + OpenHrmpChannelLimitExceeded, + /// The channel from the sender to the origin doesn't exist. + AcceptHrmpChannelDoesntExist, + /// The channel is already confirmed. + AcceptHrmpChannelAlreadyConfirmed, + /// The recipient already has the maximum number of allowed inbound channels. + AcceptHrmpChannelLimitExceeded, + /// The origin tries to close a channel where it is neither the sender nor the recipient. + CloseHrmpChannelUnauthorized, + /// The channel to be closed doesn't exist. + CloseHrmpChannelDoesntExist, + /// The channel close request is already requested. + CloseHrmpChannelAlreadyUnderway, + } } decl_module! { /// The router module. pub struct Module for enum Call where origin: ::Origin { type Error = Error; + + #[weight = 0] + fn hrmp_init_open_channel( + origin, + recipient: ParaId, + proposed_max_capacity: u32, + proposed_max_message_size: u32, + ) -> DispatchResult { + let origin = ensure_parachain(::Origin::from(origin))?; + ensure!(origin != recipient, Error::::OpenHrmpChannelToSelf); + ensure!( + >::is_valid_para(recipient), + Error::::OpenHrmpChannelInvalidRecipient, + ); + + let config = >::config(); + ensure!( + proposed_max_capacity > 0, + Error::::OpenHrmpChannelZeroPlaces, + ); + ensure!( + proposed_max_capacity <= config.hrmp_channel_max_capacity, + Error::::OpenHrmpChannelTooManyPlaces, + ); + ensure!( + proposed_max_message_size > 0, + Error::::OpenHrmpChannelZeroMessageSize, + ); + ensure!( + proposed_max_message_size <= config.hrmp_channel_max_message_size, + Error::::OpenHrmpChannelTooBigMessage, + ); + + let channel_id = HrmpChannelId { + sender: origin, + recipient, + }; + ensure!( + ::HrmpOpenChannelRequests::get(&channel_id).is_none(), + Error::::OpenHrmpChannelAlreadyExists, + ); + ensure!( + ::HrmpChannels::get(&channel_id).is_none(), + Error::::OpenHrmpChannelAlreadyRequested, + ); + + let egress_cnt = + ::HrmpEgressChannelsIndex::decode_len(&origin).unwrap_or(0) as u32; + let open_req_cnt = ::HrmpOpenChannelRequestCount::get(&origin); + let channel_num_limit = if >::is_parathread(origin) { + config.hrmp_max_parathread_outbound_channels + } else { + config.hrmp_max_parachain_outbound_channels + }; + ensure!( + egress_cnt + open_req_cnt < channel_num_limit, + Error::::OpenHrmpChannelLimitExceeded, + ); + + // TODO: Deposit + + ::HrmpOpenChannelRequestCount::insert(&origin, open_req_cnt + 1); + ::HrmpOpenChannelRequests::insert( + &channel_id, + HrmpOpenChannelRequest { + confirmed: false, + age: 0, + sender_deposit: config.hrmp_sender_deposit, + max_capacity: proposed_max_capacity, + max_message_size: proposed_max_message_size, + max_total_size: config.hrmp_channel_max_total_size, + }, + ); + ::HrmpOpenChannelRequestsList::append(channel_id); + + let notification_bytes = { + use xcm::v0::Xcm; + use codec::Encode as _; + + Xcm::HrmpNewChannelOpenRequest { + sender: origin.reveal_inner_u32(), + max_capacity: proposed_max_capacity, + max_message_size: proposed_max_message_size, + }.encode() + }; + if let Err(dmp::QueueDownwardMessageError::ExceedsMaxMessageSize) = + Self::queue_downward_message( + &config, + recipient, + notification_bytes, + ) + { + // this should never happen unless the max downward message size is configured to an + // jokingly small number. + debug_assert!(false); + } + + Ok(()) + } + + #[weight = 0] + fn hrmp_accept_open_channel(origin, sender: ParaId) -> DispatchResult { + let origin = ensure_parachain(::Origin::from(origin))?; + + let channel_id = HrmpChannelId { + sender, + recipient: origin, + }; + let mut channel_req = ::HrmpOpenChannelRequests::get(&channel_id) + .ok_or(Error::::AcceptHrmpChannelDoesntExist)?; + ensure!( + !channel_req.confirmed, + Error::::AcceptHrmpChannelAlreadyConfirmed, + ); + + // check if by accepting this open channel request, this parachain would exceed the + // number of inbound channels. + let config = >::config(); + let channel_num_limit = if >::is_parathread(origin) { + config.hrmp_max_parathread_inbound_channels + } else { + config.hrmp_max_parachain_inbound_channels + }; + let ingress_cnt = + ::HrmpIngressChannelsIndex::decode_len(&origin).unwrap_or(0) as u32; + let accepted_cnt = ::HrmpAcceptedChannelRequestCount::get(&origin); + ensure!( + ingress_cnt + accepted_cnt < channel_num_limit, + Error::::AcceptHrmpChannelLimitExceeded, + ); + + // TODO: Deposit + + // persist the updated open channel request and then increment the number of accepted + // channels. + channel_req.confirmed = true; + ::HrmpOpenChannelRequests::insert(&channel_id, channel_req); + ::HrmpAcceptedChannelRequestCount::insert(&origin, accepted_cnt + 1); + + let notification_bytes = { + use xcm::v0::Xcm; + use codec::Encode as _; + + Xcm::HrmpChannelAccepted { + recipient: origin.reveal_inner_u32(), + }.encode() + }; + if let Err(dmp::QueueDownwardMessageError::ExceedsMaxMessageSize) = + Self::queue_downward_message( + &config, + sender, + notification_bytes, + ) + { + // this should never happen unless the max downward message size is configured to an + // jokingly small number. + debug_assert!(false); + } + + Ok(()) + } + + #[weight = 0] + fn hrmp_close_channel(origin, channel_id: HrmpChannelId) -> DispatchResult { + let origin = ensure_parachain(::Origin::from(origin))?; + + // check if the origin is allowed to close the channel. + ensure!( + origin == channel_id.sender || origin == channel_id.recipient, + Error::::CloseHrmpChannelUnauthorized, + ); + + // check if the channel requested to close does exist. + ensure!( + ::HrmpChannels::get(&channel_id).is_some(), + Error::::CloseHrmpChannelDoesntExist, + ); + + // check that there is no outstanding close request for this channel + ensure!( + ::HrmpCloseChannelRequests::get(&channel_id).is_none(), + Error::::CloseHrmpChannelAlreadyUnderway, + ); + + ::HrmpCloseChannelRequests::insert(&channel_id, ()); + ::HrmpCloseChannelRequestsList::append(channel_id.clone()); + + let config = >::config(); + let notification_bytes = { + use xcm::v0::Xcm; + use codec::Encode as _; + + Xcm::HrmpChannelClosing { + initiator: origin.reveal_inner_u32(), + sender: channel_id.sender.reveal_inner_u32(), + recipient: channel_id.recipient.reveal_inner_u32(), + }.encode() + }; + let opposite_party = + if origin == channel_id.sender { + channel_id.recipient + } else { + channel_id.sender + }; + if let Err(dmp::QueueDownwardMessageError::ExceedsMaxMessageSize) = + Self::queue_downward_message( + &config, + opposite_party, + notification_bytes, + ) + { + // this should never happen unless the max downward message size is configured to an + // jokingly small number. + debug_assert!(false); + } + + Ok(()) + } } } @@ -182,12 +437,21 @@ impl Module { /// Called by the initializer to note that a new session has started. pub(crate) fn initializer_on_new_session( - _notification: &initializer::SessionChangeNotification, + notification: &initializer::SessionChangeNotification, ) { + Self::perform_outgoing_para_cleanup(); + Self::process_hrmp_open_channel_requests(¬ification.prev_config); + Self::process_hrmp_close_channel_requests(); + } + + /// Iterate over all paras that were registered for offboarding and remove all the data + /// associated with them. + fn perform_outgoing_para_cleanup() { let outgoing = OutgoingParas::take(); for outgoing_para in outgoing { Self::clean_dmp_after_outgoing(outgoing_para); Self::clean_ump_after_outgoing(outgoing_para); + Self::clean_hrmp_after_outgoing(outgoing_para); } } diff --git a/runtime/parachains/src/router/hrmp.rs b/runtime/parachains/src/router/hrmp.rs index 1e8d9d1a4d7c..cf819addabeb 100644 --- a/runtime/parachains/src/router/hrmp.rs +++ b/runtime/parachains/src/router/hrmp.rs @@ -14,8 +14,18 @@ // You should have received a copy of the GNU General Public License // along with Polkadot. If not, see . -use primitives::v1::{Balance, Hash, SessionIndex}; -use codec::{Encode, Decode}; +use super::{Module, Store, Trait}; +use crate::{configuration::HostConfiguration, paras}; +use codec::{Decode, Encode}; +use frame_support::{traits::Get, weights::Weight, StorageMap, StorageValue}; +use primitives::v1::{ + Balance, Hash, HrmpChannelId, Id as ParaId, InboundHrmpMessage, OutboundHrmpMessage, + SessionIndex, +}; +use sp_runtime::traits::{BlakeTwo256, Hash as HashT}; +use sp_std::collections::btree_set::BTreeSet; +use sp_std::mem; +use sp_std::prelude::*; /// A description of a request to open an HRMP channel. #[derive(Encode, Decode)] @@ -62,3 +72,364 @@ pub struct HrmpChannel { /// that no messages were previously added. pub mqc_head: Option, } + +/// Routines and getters related to HRMP. +impl Module { + /// Remove all storage entries associated with the given para. + pub(super) fn clean_hrmp_after_outgoing(outgoing_para: ParaId) { + ::HrmpOpenChannelRequestCount::remove(&outgoing_para); + ::HrmpAcceptedChannelRequestCount::remove(&outgoing_para); + + // close all channels where the outgoing para acts as the recipient. + for sender in ::HrmpIngressChannelsIndex::take(&outgoing_para) { + Self::close_hrmp_channel(&HrmpChannelId { + sender, + recipient: outgoing_para.clone(), + }); + } + // close all channels where the outgoing para acts as the sender. + for recipient in ::HrmpEgressChannelsIndex::take(&outgoing_para) { + Self::close_hrmp_channel(&HrmpChannelId { + sender: outgoing_para.clone(), + recipient, + }); + } + } + + /// Iterate over all open channel requests and: + /// + /// - prune the stale requests + /// - enact the confirmed requests + pub(super) fn process_hrmp_open_channel_requests(config: &HostConfiguration) { + let mut open_req_channels = ::HrmpOpenChannelRequestsList::get(); + if open_req_channels.is_empty() { + return; + } + + // iterate the vector starting from the end making our way to the beginning. This way we + // can leverage `swap_remove` to efficiently remove an item during iteration. + let mut idx = open_req_channels.len(); + loop { + // bail if we've iterated over all items. + if idx == 0 { + break; + } + + idx -= 1; + let channel_id = open_req_channels[idx].clone(); + let mut request = ::HrmpOpenChannelRequests::get(&channel_id) + .expect( + "can't be `None` due to the invariant that the list contains the same items as the set; qed" + ); + + if request.confirmed { + if >::is_valid_para(channel_id.sender) + && >::is_valid_para(channel_id.sender) + { + ::HrmpChannels::insert( + &channel_id, + HrmpChannel { + sender_deposit: request.sender_deposit, + recipient_deposit: config.hrmp_recipient_deposit, + max_capacity: request.max_capacity, + max_total_size: request.max_total_size, + max_message_size: request.max_message_size, + msg_count: 0, + total_size: 0, + mqc_head: None, + }, + ); + + ::HrmpIngressChannelsIndex::mutate(&channel_id.recipient, |v| { + if let Err(i) = v.binary_search(&channel_id.sender) { + v.insert(i, channel_id.sender); + } + }); + ::HrmpEgressChannelsIndex::mutate(&channel_id.sender, |v| { + if let Err(i) = v.binary_search(&channel_id.recipient) { + v.insert(i, channel_id.recipient); + } + }); + } + + ::HrmpOpenChannelRequestCount::mutate(&channel_id.sender, |v| { + *v -= 1; + }); + ::HrmpAcceptedChannelRequestCount::mutate( + &channel_id.recipient, + |v| { + *v -= 1; + }, + ); + + let _ = open_req_channels.swap_remove(idx); + ::HrmpOpenChannelRequests::remove(&channel_id); + } else { + request.age += 1; + if request.age == config.hrmp_open_request_ttl { + // got stale + + ::HrmpOpenChannelRequestCount::mutate(&channel_id.sender, |v| { + *v -= 1; + }); + + // TODO: return deposit + + let _ = open_req_channels.swap_remove(idx); + ::HrmpOpenChannelRequests::remove(&channel_id); + } + } + } + + ::HrmpOpenChannelRequestsList::put(open_req_channels); + } + + /// Iterate over all close channel requests unconditionally closing the channels. + pub(super) fn process_hrmp_close_channel_requests() { + let close_reqs = ::HrmpCloseChannelRequestsList::take(); + for condemned_ch_id in close_reqs { + ::HrmpCloseChannelRequests::remove(&condemned_ch_id); + Self::close_hrmp_channel(&condemned_ch_id); + + // clean up the indexes. + ::HrmpEgressChannelsIndex::mutate(&condemned_ch_id.sender, |v| { + if let Ok(i) = v.binary_search(&condemned_ch_id.recipient) { + v.remove(i); + } + }); + ::HrmpIngressChannelsIndex::mutate(&condemned_ch_id.recipient, |v| { + if let Ok(i) = v.binary_search(&condemned_ch_id.sender) { + v.remove(i); + } + }); + } + } + + /// Close and remove the designated HRMP channel. + /// + /// This includes returning the deposits. However, it doesn't include updating the ingress/egress + /// indicies. + pub(super) fn close_hrmp_channel(channel_id: &HrmpChannelId) { + // TODO: return deposits + + ::HrmpChannels::remove(channel_id); + ::HrmpChannelContents::remove(channel_id); + } + + /// Check that the candidate of the given recipient controls the HRMP watermark properly. + pub(crate) fn check_hrmp_watermark( + recipient: ParaId, + relay_chain_parent_number: T::BlockNumber, + new_hrmp_watermark: T::BlockNumber, + ) -> bool { + // First, check where the watermark CANNOT legally land. + // + // (a) For ensuring that messages are eventually, a rule requires each parablock new + // watermark should be greater than the last one. + // + // (b) However, a parachain cannot read into "the future", therefore the watermark should + // not be greater than the relay-chain context block which the parablock refers to. + if let Some(last_watermark) = ::HrmpWatermarks::get(&recipient) { + if new_hrmp_watermark <= last_watermark { + return false; + } + } + if new_hrmp_watermark > relay_chain_parent_number { + return false; + } + + // Second, check where the watermark CAN land. It's one of the following: + // + // (a) The relay parent block number. + // (b) A relay-chain block in which this para received at least one message. + if new_hrmp_watermark == relay_chain_parent_number { + true + } else { + let digest = ::HrmpChannelDigests::get(&recipient); + digest + .binary_search_by_key(&new_hrmp_watermark, |(block_no, _)| *block_no) + .is_ok() + } + } + + pub(crate) fn check_outbound_hrmp( + config: &HostConfiguration, + sender: ParaId, + out_hrmp_msgs: &[OutboundHrmpMessage], + ) -> bool { + if out_hrmp_msgs.len() as u32 > config.hrmp_max_message_num_per_candidate { + return false; + } + + let mut last_recipient = None::; + + for out_msg in out_hrmp_msgs { + match last_recipient { + // the messages must be sorted in ascending order and there must be no two messages sent + // to the same recipient. Thus we can check that every recipient is strictly greater than + // the previous one. + Some(last_recipient) if out_msg.recipient <= last_recipient => { + return false; + } + _ => last_recipient = Some(out_msg.recipient), + } + + let channel_id = HrmpChannelId { + sender, + recipient: out_msg.recipient, + }; + + let channel = match ::HrmpChannels::get(&channel_id) { + Some(channel) => channel, + None => return false, + }; + + if out_msg.data.len() as u32 > channel.max_message_size { + return false; + } + + if channel.total_size + out_msg.data.len() as u32 > channel.max_total_size { + return false; + } + + if channel.msg_count + 1 > channel.max_capacity { + return false; + } + } + + true + } + + pub(crate) fn prune_hrmp(recipient: ParaId, new_hrmp_watermark: T::BlockNumber) -> Weight { + let mut weight = 0; + + // sift through the incoming messages digest to collect the paras that sent at least one + // message to this parachain between the old and new watermarks. + let senders = ::HrmpChannelDigests::mutate(&recipient, |digest| { + let mut senders = BTreeSet::new(); + let mut residue = Vec::with_capacity(digest.len()); + for (block_no, paras_sent_msg) in mem::replace(digest, Vec::new()) { + if block_no <= new_hrmp_watermark { + senders.extend(paras_sent_msg); + } else { + residue.push((block_no, paras_sent_msg)); + } + } + *digest = residue; + senders + }); + weight += T::DbWeight::get().reads_writes(1, 1); + + // having all senders we can trivially find out the channels which we need to prune. + let channels_to_prune = senders + .into_iter() + .map(|sender| HrmpChannelId { sender, recipient }); + for channel_id in channels_to_prune { + // prune each channel up to the new watermark keeping track how many messages we removed + // and what is the total byte size of them. + let (cnt, size) = + ::HrmpChannelContents::mutate(&channel_id, |outbound_messages| { + let (mut cnt, mut size) = (0, 0); + + let mut residue = Vec::with_capacity(outbound_messages.len()); + for msg in mem::replace(outbound_messages, Vec::new()).into_iter() { + if msg.sent_at <= new_hrmp_watermark { + cnt += 1; + size += msg.data.len(); + } else { + residue.push(msg); + } + } + *outbound_messages = residue; + + (cnt, size) + }); + + // update the channel metadata. + ::HrmpChannels::mutate(&channel_id, |channel| { + if let Some(ref mut channel) = channel { + channel.msg_count -= cnt as u32; + channel.total_size -= size as u32; + } + }); + + weight += T::DbWeight::get().reads_writes(2, 2); + } + + ::HrmpWatermarks::insert(&recipient, new_hrmp_watermark); + weight += T::DbWeight::get().reads_writes(0, 1); + + weight + } + + /// Process the outbound HRMP messages by putting them into the appropriate recipient queues. + /// + /// Returns the amount of weight consumed. + pub(crate) fn queue_outbound_hrmp( + sender: ParaId, + out_hrmp_msgs: Vec>, + ) -> Weight { + let mut weight = 0; + let now = >::block_number(); + + for out_msg in out_hrmp_msgs { + let channel_id = HrmpChannelId { + sender, + recipient: out_msg.recipient, + }; + + let mut channel = match ::HrmpChannels::get(&channel_id) { + Some(channel) => channel, + None => { + // apparently, that since acceptance of this candidate the recipient was + // offboarded and the channel no longer exists. + continue; + } + }; + + let inbound = InboundHrmpMessage { + sent_at: now, + data: out_msg.data, + }; + + // book keeping + channel.msg_count += 1; + channel.total_size += inbound.data.len() as u32; + + // compute the new MQC head of the channel + let prev_head = channel.mqc_head.clone().unwrap_or(Default::default()); + let new_head = BlakeTwo256::hash_of(&( + prev_head, + inbound.sent_at, + T::Hashing::hash_of(&inbound.data), + )); + channel.mqc_head = Some(new_head); + + ::HrmpChannels::insert(&channel_id, channel); + ::HrmpChannelContents::append(&channel_id, inbound); + + weight += T::DbWeight::get().reads_writes(2, 2); + } + + weight + } + + pub(crate) fn hrmp_mqc_heads(recipient: ParaId) -> Vec<(ParaId, Hash)> { + let sender_set = ::HrmpIngressChannelsIndex::get(&recipient); + if sender_set.is_empty() { + return Vec::new(); + } + + let mut mqc_heads = Vec::with_capacity(sender_set.len()); + for sender in sender_set { + let channel_metadata = + ::HrmpChannels::get(&HrmpChannelId { sender, recipient }); + let mqc_head = channel_metadata + .and_then(|metadata| metadata.mqc_head) + .unwrap_or(Hash::default()); + mqc_heads.push((sender, mqc_head)); + } + + mqc_heads + } +} diff --git a/runtime/parachains/src/util.rs b/runtime/parachains/src/util.rs index 0f6becfd6005..34946de3e3d2 100644 --- a/runtime/parachains/src/util.rs +++ b/runtime/parachains/src/util.rs @@ -19,7 +19,6 @@ use sp_runtime::traits::{One, Saturating}; use primitives::v1::{Id as ParaId, PersistedValidationData, TransientValidationData}; -use sp_std::prelude::*; use crate::{configuration, paras, router}; @@ -34,7 +33,7 @@ pub fn make_persisted_validation_data( Some(PersistedValidationData { parent_head: >::para_head(¶_id)?, block_number: relay_parent_number, - hrmp_mqc_heads: Vec::new(), + hrmp_mqc_heads: >::hrmp_mqc_heads(para_id), dmq_mqc_head: >::dmq_mqc_head(para_id), }) } diff --git a/runtime/rococo-v1/src/lib.rs b/runtime/rococo-v1/src/lib.rs index 9123a7c4b79d..75bedb49c459 100644 --- a/runtime/rococo-v1/src/lib.rs +++ b/runtime/rococo-v1/src/lib.rs @@ -766,6 +766,7 @@ impl parachains_paras::Trait for Runtime { } impl parachains_router::Trait for Runtime { + type Origin = Origin; type UmpSink = (); // TODO: #1873 To be handled by the XCM receiver. } diff --git a/runtime/test-runtime/src/lib.rs b/runtime/test-runtime/src/lib.rs index 0153d87b4782..ab152a5019c1 100644 --- a/runtime/test-runtime/src/lib.rs +++ b/runtime/test-runtime/src/lib.rs @@ -453,6 +453,7 @@ impl paras::Trait for Runtime { } impl router::Trait for Runtime { + type Origin = Origin; type UmpSink = (); }