referrerpolicy=no-referrer-when-downgrade

polkadot_runtime_parachains/
dmp.rs

1// Copyright (C) Parity Technologies (UK) Ltd.
2// This file is part of Polkadot.
3
4// Polkadot is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8
9// Polkadot is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13
14// You should have received a copy of the GNU General Public License
15// along with Polkadot.  If not, see <http://www.gnu.org/licenses/>.
16
17//! To prevent Out of Memory errors on the `DownwardMessageQueue`, an
18//! exponential fee factor (`DeliveryFeeFactor`) is set. The fee factor
19//! increments exponentially after the number of messages in the
20//! `DownwardMessageQueue` passes a threshold. This threshold is set as:
21//!
22//! ```ignore
23//! // Maximum max sized messages that can be send to
24//! // the DownwardMessageQueue before it runs out of memory
25//! max_messages = MAX_POSSIBLE_ALLOCATION / max_downward_message_size
26//! threshold = max_messages / THRESHOLD_FACTOR
27//! ```
28//! Based on the THRESHOLD_FACTOR, the threshold is set as a fraction of the
29//! total messages. The `DeliveryFeeFactor` increases for a message over the
30//! threshold by:
31//!
32//! `DeliveryFeeFactor = DeliveryFeeFactor *
33//! (EXPONENTIAL_FEE_BASE + MESSAGE_SIZE_FEE_BASE * encoded_message_size_in_KB)`
34//!
35//! And decreases when the number of messages in the `DownwardMessageQueue` fall
36//! below the threshold by:
37//!
38//! `DeliveryFeeFactor = DeliveryFeeFactor / EXPONENTIAL_FEE_BASE`
39//!
40//! As an extra defensive measure, a `max_messages` hard
41//! limit is set to the number of messages in the DownwardMessageQueue. Messages
42//! that would increase the number of messages in the queue above this hard
43//! limit are dropped.
44
45use crate::{
46	configuration::{self, HostConfiguration},
47	initializer, paras, FeeTracker, GetMinFeeFactor,
48};
49use alloc::vec::Vec;
50use core::fmt;
51use frame_support::{pallet_prelude::*, traits::Defensive, weights::WeightMeter};
52use frame_system::pallet_prelude::BlockNumberFor;
53use inbound_downward_queue::InboundDownwardQueue;
54use polkadot_core_primitives::{InboundDownwardQueueMeta, PageIndex};
55use polkadot_primitives::{DownwardMessage, Hash, Id as ParaId, InboundDownwardMessage};
56use sp_core::MAX_POSSIBLE_ALLOCATION;
57use sp_runtime::{
58	traits::{BlakeTwo256, Hash as HashT},
59	FixedU128,
60};
61use xcm::latest::SendError;
62
63pub use pallet::*;
64pub use weights::WeightInfo;
65
66#[cfg(feature = "runtime-benchmarks")]
67mod benchmarking;
68pub mod inbound_downward_queue;
69pub mod migration;
70#[cfg(test)]
71mod mock;
72#[cfg(test)]
73mod tests;
74pub mod weights;
75
76const THRESHOLD_FACTOR: u32 = 2;
77
78/// An error sending a downward message.
79#[derive(Debug)]
80pub enum QueueDownwardMessageError {
81	/// The message being sent exceeds the configured max message size.
82	ExceedsMaxMessageSize,
83	/// Message rejected due to queue being full.
84	ExceedsMaxQueueSize,
85	/// The destination is unknown.
86	Unroutable,
87}
88
89impl From<QueueDownwardMessageError> for SendError {
90	fn from(err: QueueDownwardMessageError) -> Self {
91		match err {
92			QueueDownwardMessageError::ExceedsMaxMessageSize |
93			QueueDownwardMessageError::ExceedsMaxQueueSize => SendError::ExceedsMaxMessageSize,
94			QueueDownwardMessageError::Unroutable => SendError::Unroutable,
95		}
96	}
97}
98
99/// An error returned by [`Pallet::check_processed_downward_messages`] that indicates an acceptance
100/// check didn't pass.
101pub(crate) enum ProcessedDownwardMessagesAcceptanceErr {
102	/// If there are pending messages then `processed_downward_messages` should be at least 1,
103	AdvancementRule,
104	/// `processed_downward_messages` should not be greater than the number of pending messages.
105	Underflow { processed_downward_messages: u32, dmq_length: u32 },
106}
107
108impl fmt::Debug for ProcessedDownwardMessagesAcceptanceErr {
109	fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
110		use ProcessedDownwardMessagesAcceptanceErr::*;
111		match *self {
112			AdvancementRule => {
113				write!(fmt, "DMQ is not empty, but processed_downward_messages is 0",)
114			},
115			Underflow { processed_downward_messages, dmq_length } => write!(
116				fmt,
117				"processed_downward_messages = {}, but dmq_length is only {}",
118				processed_downward_messages, dmq_length,
119			),
120		}
121	}
122}
123
124#[frame_support::pallet]
125pub mod pallet {
126	use super::*;
127
128	#[pallet::pallet]
129	#[pallet::storage_version(migration::STORAGE_VERSION)]
130	#[pallet::without_storage_info]
131	pub struct Pallet<T>(_);
132
133	#[pallet::config]
134	pub trait Config: frame_system::Config + configuration::Config + paras::Config {
135		/// Weight info needed for the dmp pallet.
136		type WeightInfo: WeightInfo;
137	}
138
139	/// Metadata for managing `DownwardMessageQueuePages`.
140	///
141	/// DO NOT MODIFY manually. Only use `InboundDownwardQueue` to preserve invariants.
142	#[pallet::storage]
143	pub type DownwardMessageQueueMeta<T: Config> =
144		StorageMap<_, Twox64Concat, ParaId, InboundDownwardQueueMeta, OptionQuery>;
145
146	/// Linked message data list to hold inbound downward message pages.
147	///
148	/// Messages are not packed and one page is equivalent to one message.
149	/// DO NOT MODIFY manually. Only use `InboundDownwardQueue` to preserve invariants.
150	#[pallet::storage]
151	pub type DownwardMessageQueuePages<T: Config> = StorageDoubleMap<
152		_,
153		Blake2_128Concat,
154		ParaId,
155		Twox64Concat,
156		PageIndex,
157		InboundDownwardMessage<BlockNumberFor<T>>,
158		OptionQuery,
159	>;
160
161	/// Queue with ParaIds and the [first, last) page range to be deleted.
162	///
163	/// DO NOT MODIFY manually. Only use `InboundDownwardQueue` to preserve invariants.
164	#[pallet::storage]
165	pub type DownwardMessageQueueLazyDelete<T: Config> = StorageMap<
166		_,
167		Blake2_128Concat,
168		ParaId,
169		(PageIndex, PageIndex), // Deletion range [first, last)
170		OptionQuery,
171	>;
172
173	/// A mapping that stores the downward message queue MQC head for each para.
174	///
175	/// Each link in this chain has a form:
176	/// `(prev_head, B, H(M))`, where
177	/// - `prev_head`: is the previous head hash or zero if none.
178	/// - `B`: is the relay-chain block number in which a message was appended.
179	/// - `H(M)`: is the hash of the message being appended.
180	#[pallet::storage]
181	pub(crate) type DownwardMessageQueueHeads<T: Config> =
182		StorageMap<_, Twox64Concat, ParaId, Hash, ValueQuery>;
183
184	/// The factor to multiply the base delivery fee by.
185	#[pallet::storage]
186	pub(crate) type DeliveryFeeFactor<T: Config> =
187		StorageMap<_, Twox64Concat, ParaId, FixedU128, ValueQuery, GetMinFeeFactor<Pallet<T>>>;
188
189	#[pallet::event]
190	#[pallet::generate_deposit(pub(super) fn deposit_event)]
191	pub enum Event<T: Config> {
192		/// Legacy V0 DMP queue storage has been cleaned.
193		DmpQueueV0Cleaned { para: ParaId },
194	}
195
196	#[pallet::hooks]
197	impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
198		// NOTE: We disable for our `mock` runtime since that only has 4M ps weight per block :(
199		#[cfg(all(feature = "std", not(test)))]
200		fn integrity_test() {
201			let min_mbm_weight = <T as Config>::WeightInfo::migrate_v0_to_v1_step_base()
202				.saturating_add(<T as Config>::WeightInfo::migrate_v0_to_v1_step_iter())
203				.saturating_add(<T as Config>::WeightInfo::migrate_v0_to_v1_step_msg());
204
205			let max = T::BlockWeights::get().max_block.saturating_div(3);
206			assert!(
207				max.all_gte(min_mbm_weight),
208				"DMP queue migration uses more than 1/3 of max block weight"
209			);
210
211			let lazy_delete_some_weight = <T as Config>::WeightInfo::lazy_delete_some();
212			assert!(
213				max.all_gte(lazy_delete_some_weight),
214				"DMP queue lazy delete uses more than 1/3 block weight"
215			);
216		}
217
218		fn on_idle(_now: BlockNumberFor<T>, weight: Weight) -> Weight {
219			let mut meter = WeightMeter::with_limit(weight);
220
221			InboundDownwardQueue::<T>::lazy_delete_some(&mut meter);
222
223			meter.consumed()
224		}
225
226		#[cfg(feature = "try-runtime")]
227		fn try_state(_now: BlockNumberFor<T>) -> Result<(), sp_runtime::TryRuntimeError> {
228			InboundDownwardQueue::<T>::try_state();
229
230			Ok(())
231		}
232	}
233}
234/// Routines and getters related to downward message passing.
235impl<T: Config> Pallet<T> {
236	/// Block initialization logic, called by initializer.
237	pub(crate) fn initializer_initialize(_now: BlockNumberFor<T>) -> Weight {
238		Weight::zero()
239	}
240
241	/// Block finalization logic, called by initializer.
242	pub(crate) fn initializer_finalize() {}
243
244	/// Called by the initializer to note that a new session has started.
245	pub(crate) fn initializer_on_new_session(
246		_notification: &initializer::SessionChangeNotification<BlockNumberFor<T>>,
247		outgoing_paras: &[ParaId],
248	) {
249		Self::perform_outgoing_para_cleanup(outgoing_paras);
250	}
251
252	/// Iterate over all paras that were noted for offboarding and remove all the data
253	/// associated with them.
254	fn perform_outgoing_para_cleanup(outgoing: &[ParaId]) {
255		for outgoing_para in outgoing {
256			Self::clean_dmp_after_outgoing(outgoing_para);
257		}
258	}
259
260	/// Remove all relevant storage items for an outgoing parachain.
261	fn clean_dmp_after_outgoing(outgoing_para: &ParaId) {
262		InboundDownwardQueue::<T>::delete_all(*outgoing_para);
263		DownwardMessageQueueHeads::<T>::remove(outgoing_para);
264	}
265
266	/// Determine whether enqueuing a downward message to a specific recipient para would result
267	/// in an error. If this returns `Ok(())` the caller can be certain that a call to
268	/// `queue_downward_message` with the same parameters will be successful.
269	pub fn can_queue_downward_message(
270		config: &HostConfiguration<BlockNumberFor<T>>,
271		para: &ParaId,
272		msg: &DownwardMessage,
273	) -> Result<(), QueueDownwardMessageError> {
274		let serialized_len = msg.len() as u32;
275		if serialized_len > config.max_downward_message_size {
276			return Err(QueueDownwardMessageError::ExceedsMaxMessageSize);
277		}
278
279		// Hard limit on Queue size
280		if Self::dmq_length(*para) > Self::dmq_max_length(config.max_downward_message_size) {
281			return Err(QueueDownwardMessageError::ExceedsMaxMessageSize);
282		}
283
284		// If the head exists, we assume the parachain is legit and exists.
285		if !paras::Heads::<T>::contains_key(para) {
286			return Err(QueueDownwardMessageError::Unroutable);
287		}
288
289		Ok(())
290	}
291
292	/// Enqueue a downward message to a specific recipient para.
293	///
294	/// When encoded, the message should not exceed the `config.max_downward_message_size`.
295	/// Otherwise, the message won't be sent and `Err` will be returned.
296	///
297	/// It is possible to send a downward message to a non-existent para. That, however, would lead
298	/// to a dangling storage. If the caller cannot statically prove that the recipient exists
299	/// then the caller should perform a runtime check.
300	pub fn queue_downward_message(
301		config: &HostConfiguration<BlockNumberFor<T>>,
302		para: ParaId,
303		msg: DownwardMessage,
304	) -> Result<(), QueueDownwardMessageError> {
305		let serialized_len = msg.len();
306		Self::can_queue_downward_message(config, &para, &msg)?;
307
308		let inbound = InboundDownwardQueue::<T>::push_back(para, msg)
309			.map_err(|_| QueueDownwardMessageError::ExceedsMaxQueueSize)?;
310		let q_len = InboundDownwardQueue::<T>::len(para).unwrap_or(0);
311
312		// obtain the new link in the MQC and update the head.
313		DownwardMessageQueueHeads::<T>::mutate(para, |head| {
314			let new_head =
315				BlakeTwo256::hash_of(&(*head, inbound.sent_at, T::Hashing::hash_of(&inbound.msg)));
316			*head = new_head;
317		});
318
319		let threshold =
320			Self::dmq_max_length(config.max_downward_message_size).saturating_div(THRESHOLD_FACTOR);
321		if q_len > threshold as u64 {
322			Self::increase_fee_factor(para, serialized_len as u128);
323		}
324
325		Ok(())
326	}
327
328	/// Checks if the number of processed downward messages is valid.
329	pub(crate) fn check_processed_downward_messages(
330		para: ParaId,
331		relay_parent_number: BlockNumberFor<T>,
332		processed_downward_messages: u32,
333	) -> Result<(), ProcessedDownwardMessagesAcceptanceErr> {
334		let dmq_length = Self::dmq_length(para);
335
336		if dmq_length > 0 && processed_downward_messages == 0 {
337			// The advancement rule is for at least one downwards message to be processed
338			// if the queue is non-empty at the relay-parent. Downwards messages are annotated
339			// with the block number, so we compare the earliest (first) against the relay parent.
340			let first = InboundDownwardQueue::<T>::peek_front(para);
341
342			// sanity: if dmq_length is >0 this should always be 'Some'.
343			if first.map_or(false, |msg| msg.sent_at <= relay_parent_number) {
344				return Err(ProcessedDownwardMessagesAcceptanceErr::AdvancementRule);
345			}
346		}
347
348		// Note that we might be allowing a parachain to signal that it's processed
349		// messages that hadn't been placed in the queue at the relay_parent.
350		// only 'stupid' parachains would do it and we don't (and can't) force anyone
351		// to act on messages, so the lenient approach is fine here.
352		if dmq_length < processed_downward_messages {
353			return Err(ProcessedDownwardMessagesAcceptanceErr::Underflow {
354				processed_downward_messages,
355				dmq_length,
356			});
357		}
358
359		Ok(())
360	}
361
362	/// Prunes the specified number of messages from the downward message queue of the given para.
363	pub(crate) fn prune_dmq(para: ParaId, processed_downward_messages: u32) {
364		InboundDownwardQueue::<T>::drop_front_n(para, processed_downward_messages as u64);
365		let q_len = InboundDownwardQueue::<T>::len(para).unwrap_or(0);
366
367		let config = configuration::ActiveConfig::<T>::get();
368		let threshold =
369			Self::dmq_max_length(config.max_downward_message_size).saturating_div(THRESHOLD_FACTOR);
370		if q_len <= threshold as u64 {
371			Self::decrease_fee_factor(para);
372		}
373	}
374
375	/// Returns the Head of Message Queue Chain for the given para or `None` if there is none
376	/// associated with it.
377	#[cfg(test)]
378	fn dmq_mqc_head(para: ParaId) -> Hash {
379		DownwardMessageQueueHeads::<T>::get(&para)
380	}
381
382	/// Returns the number of pending downward messages addressed to the given para.
383	///
384	/// Returns 0 if the para doesn't have an associated downward message queue.
385	pub(crate) fn dmq_length(para: ParaId) -> u32 {
386		InboundDownwardQueue::<T>::len(para)
387			.unwrap_or(0)
388			.try_into()
389			.defensive_unwrap_or(u32::MAX)
390	}
391
392	fn dmq_max_length(max_downward_message_size: u32) -> u32 {
393		MAX_POSSIBLE_ALLOCATION.checked_div(max_downward_message_size).unwrap_or(0)
394	}
395
396	/// DO NOT CALL IN CONSENSUS. Returns the downward message queue contents for the given para.
397	///
398	/// The most recent messages are the latest in the vector.
399	pub fn dmq_contents_do_not_call_in_consensus(
400		recipient: ParaId,
401	) -> Vec<InboundDownwardMessage<BlockNumberFor<T>>> {
402		InboundDownwardQueue::<T>::peek_all_do_not_call_in_consensus(recipient)
403	}
404
405	/// Make the parachain reachable for downward messages.
406	///
407	/// Only useable in benchmarks or tests.
408	#[cfg(any(feature = "runtime-benchmarks", feature = "std"))]
409	pub fn make_parachain_reachable(para: impl Into<ParaId>) {
410		let para = para.into();
411		crate::paras::Heads::<T>::insert(para, para.encode());
412	}
413}
414
415impl<T: Config> FeeTracker for Pallet<T> {
416	type Id = ParaId;
417
418	fn get_fee_factor(id: Self::Id) -> FixedU128 {
419		DeliveryFeeFactor::<T>::get(id)
420	}
421
422	fn set_fee_factor(id: Self::Id, val: FixedU128) {
423		<DeliveryFeeFactor<T>>::set(id, val);
424	}
425}
426
427#[cfg(feature = "runtime-benchmarks")]
428impl<T: Config> crate::EnsureForParachain for Pallet<T> {
429	fn ensure(para: ParaId) {
430		Self::make_parachain_reachable(para);
431	}
432}