referrerpolicy=no-referrer-when-downgrade

cumulus_test_runtime/
test_pallet.rs

1// Copyright (C) Parity Technologies (UK) Ltd.
2// This file is part of Cumulus.
3
4// Cumulus 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// Cumulus 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 Cumulus.  If not, see <http://www.gnu.org/licenses/>.
16
17/// A special pallet that exposes dispatchables that are only useful for testing.
18pub use pallet::*;
19
20use codec::Encode;
21
22/// Some key that we set in genesis and only read in
23/// [`SingleBlockMigrations`](crate::SingleBlockMigrations) to ensure that
24/// [`OnRuntimeUpgrade`](frame_support::traits::OnRuntimeUpgrade) works as expected.
25pub const TEST_RUNTIME_UPGRADE_KEY: &[u8] = b"+test_runtime_upgrade_key+";
26
27/// Generates the storage key for Alice's account on the relay chain.
28pub fn relay_alice_account_key() -> alloc::vec::Vec<u8> {
29	use sp_keyring::Sr25519Keyring;
30
31	let alice = Sr25519Keyring::Alice.to_account_id();
32
33	let mut key = sp_io::hashing::twox_128(b"System").to_vec();
34	key.extend_from_slice(&sp_io::hashing::twox_128(b"Account"));
35	key.extend_from_slice(&sp_io::hashing::blake2_128(&alice.encode()));
36	key.extend_from_slice(&alice.encode());
37	key
38}
39
40#[frame_support::pallet(dev_mode)]
41pub mod pallet {
42	use crate::test_pallet::TEST_RUNTIME_UPGRADE_KEY;
43	use alloc::{vec, vec::Vec};
44	use cumulus_primitives_core::{CumulusDigestItem, ParaId, XcmpMessageSource};
45	use cumulus_primitives_storage_weight_reclaim::get_proof_size;
46	use frame_support::{
47		dispatch::DispatchInfo,
48		inherent::{InherentData, InherentIdentifier, ProvideInherent},
49		pallet_prelude::*,
50		traits::IsSubType,
51		weights::constants::WEIGHT_REF_TIME_PER_SECOND,
52		DebugNoBound,
53	};
54	use frame_system::pallet_prelude::*;
55	use sp_runtime::traits::{Dispatchable, Implication, TransactionExtension};
56
57	/// The inherent identifier for weight consumption.
58	pub const INHERENT_IDENTIFIER: InherentIdentifier = *b"consume0";
59
60	#[pallet::pallet]
61	pub struct Pallet<T>(_);
62
63	#[pallet::config]
64	pub trait Config: frame_system::Config + cumulus_pallet_parachain_system::Config {}
65
66	/// A simple storage map for testing purposes.
67	#[pallet::storage]
68	pub type TestMap<T: Config> = StorageMap<_, Twox64Concat, u32, (), ValueQuery>;
69
70	/// Pending outbound HRMP messages queued by test extrinsics.
71	#[pallet::storage]
72	pub type PendingOutboundHrmpMessages<T: Config> =
73		StorageValue<_, alloc::vec::Vec<(ParaId, alloc::vec::Vec<u8>)>, ValueQuery>;
74
75	impl<T: Config> XcmpMessageSource for Pallet<T> {
76		fn take_outbound_messages(
77			maximum_channels: usize,
78			excluded_recipients: &[ParaId],
79		) -> alloc::vec::Vec<(ParaId, alloc::vec::Vec<u8>)> {
80			PendingOutboundHrmpMessages::<T>::mutate(|messages| {
81				let mut taken_recipients = alloc::vec::Vec::new();
82				let mut result = alloc::vec::Vec::new();
83				messages.retain(|(recipient, data)| {
84					if result.len() >= maximum_channels ||
85						excluded_recipients.contains(recipient) ||
86						taken_recipients.contains(recipient)
87					{
88						return true;
89					}
90					taken_recipients.push(*recipient);
91					result.push((*recipient, data.clone()));
92					false
93				});
94				result
95			})
96		}
97	}
98
99	/// When active, `on_initialize` queues one HRMP message per block, alternating
100	/// between `HRMP_RECIPIENT_HIGH` (odd blocks) and `HRMP_RECIPIENT_LOW` (even blocks).
101	/// This produces descending recipient order across consecutive blocks in a bundle,
102	/// exercising the HRMP message sorting in the collation path.
103	#[pallet::storage]
104	pub type HrmpSendingActive<T: Config> = StorageValue<_, bool, ValueQuery>;
105
106	/// Flag to indicate if a 1s weight should be registered in the next `on_initialize`.
107	#[pallet::storage]
108	pub type ScheduleWeightRegistration<T: Config> = StorageValue<_, bool, ValueQuery>;
109
110	/// Weight to be consumed by the inherent call.
111	#[pallet::storage]
112	pub type InherentWeightConsume<T: Config> = StorageValue<_, Weight, OptionQuery>;
113
114	/// A map that contains on single big value at the current block.
115	///
116	/// In every block we are moving the big value from the previous block to current block. This is
117	/// done to test that the storage proof size between multiple blocks in the same bundle is
118	/// shared.
119	#[pallet::storage]
120	pub type BigValueMove<T: Config> =
121		StorageMap<_, Twox64Concat, BlockNumberFor<T>, Vec<u8>, OptionQuery>;
122
123	pub const HRMP_RECIPIENT_LOW: u32 = 2500;
124	pub const HRMP_RECIPIENT_HIGH: u32 = 2600;
125
126	#[pallet::hooks]
127	impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
128		fn on_initialize(n: BlockNumberFor<T>) -> Weight {
129			if HrmpSendingActive::<T>::get() {
130				let block_num: u32 = n.try_into().unwrap_or(0);
131				let recipient = if block_num % 2 == 1 {
132					ParaId::from(HRMP_RECIPIENT_HIGH)
133				} else {
134					ParaId::from(HRMP_RECIPIENT_LOW)
135				};
136				PendingOutboundHrmpMessages::<T>::mutate(|messages| {
137					messages.push((recipient, vec![block_num as u8]));
138				});
139			}
140
141			if ScheduleWeightRegistration::<T>::get() {
142				let weight_to_register = Weight::from_parts(WEIGHT_REF_TIME_PER_SECOND, 0);
143
144				let left_weight = frame_system::Pallet::<T>::remaining_block_weight();
145
146				if left_weight.can_consume(weight_to_register) {
147					tracing::info!("Consuming 1s of weight :)");
148					// We have enough capacity, consume the flag and register the weight
149					ScheduleWeightRegistration::<T>::kill();
150					return weight_to_register;
151				}
152			}
153
154			if let Some(mut value) = BigValueMove::<T>::take(n - 1u32.into()) {
155				// Modify the value a little bit.
156				let parent_hash = frame_system::Pallet::<T>::parent_hash();
157				value[..parent_hash.as_ref().len()].copy_from_slice(parent_hash.as_ref());
158
159				BigValueMove::<T>::insert(n, value);
160
161				// Depositing the event is important, because then we write the actual proof size
162				// into the state. If some node returns a different proof size on import of this
163				// block, we will detect it this way as the storage root will be different.
164				Self::deposit_event(Event::MovedBigValue {
165					proof_size: get_proof_size().unwrap_or_default(),
166				})
167			}
168
169			Weight::zero()
170		}
171	}
172
173	#[pallet::call]
174	impl<T: Config> Pallet<T> {
175		/// A test dispatchable for setting a custom head data in `validate_block`.
176		#[pallet::weight(0)]
177		pub fn set_custom_validation_head_data(
178			_: OriginFor<T>,
179			custom_header: alloc::vec::Vec<u8>,
180		) -> DispatchResult {
181			cumulus_pallet_parachain_system::Pallet::<T>::set_custom_validation_head_data(
182				custom_header,
183			);
184			Ok(())
185		}
186
187		/// A dispatchable that first reads two values from two different child tries, asserts they
188		/// are the expected values (if the values exist in the state) and then writes two different
189		/// values to these child tries.
190		#[pallet::weight(0)]
191		pub fn read_and_write_child_tries(_: OriginFor<T>) -> DispatchResult {
192			let key = &b"hello"[..];
193			let first_trie = &b"first"[..];
194			let second_trie = &b"second"[..];
195			let first_value = "world1".encode();
196			let second_value = "world2".encode();
197
198			if let Some(res) = sp_io::default_child_storage::get(first_trie, key) {
199				assert_eq!(first_value, res);
200			}
201			if let Some(res) = sp_io::default_child_storage::get(second_trie, key) {
202				assert_eq!(second_value, res);
203			}
204
205			sp_io::default_child_storage::set(first_trie, key, &first_value);
206			sp_io::default_child_storage::set(second_trie, key, &second_value);
207
208			Ok(())
209		}
210
211		/// Reads a key and writes a big value under this key.
212		///
213		/// At genesis this `key` is empty and thus, will only be set in consequent blocks.
214		pub fn read_and_write_big_value(_: OriginFor<T>) -> DispatchResult {
215			let key = &b"really_huge_value"[..];
216			sp_io::storage::get(key);
217			sp_io::storage::set(key, &vec![0u8; 1024 * 1024 * 5]);
218
219			Ok(())
220		}
221
222		/// Stores `()` in `TestMap` for keys from 0 up to `max_key`.
223		#[pallet::weight(0)]
224		pub fn store_values_in_map(_: OriginFor<T>, max_key: u32) -> DispatchResult {
225			for i in 0..=max_key {
226				TestMap::<T>::insert(i, ());
227			}
228			Ok(())
229		}
230
231		/// Removes the value associated with `key` from `TestMap`.
232		#[pallet::weight(0)]
233		pub fn remove_value_from_map(_: OriginFor<T>, key: u32) -> DispatchResult {
234			TestMap::<T>::remove(key);
235			Ok(())
236		}
237
238		/// Directly sets `n` small UMP messages in `PendingUpwardMessages`.
239		#[pallet::weight(0)]
240		pub fn send_n_upward_messages(_: OriginFor<T>, n: u32) -> DispatchResult {
241			let messages: alloc::vec::Vec<_> = (0..n).map(|i| vec![(i % 256) as u8]).collect();
242			cumulus_pallet_parachain_system::PendingUpwardMessages::<T>::put(messages);
243			Ok(())
244		}
245
246		/// Sends a UMP message of specific size (in bytes).
247		#[pallet::weight(0)]
248		pub fn send_upward_message_of_size(_: OriginFor<T>, size: u32) -> DispatchResult {
249			let message = alloc::vec![0u8; size as usize];
250			cumulus_pallet_parachain_system::Pallet::<T>::send_upward_message(message)
251				.map_err(|_| "Failed to send upward message")?;
252			Ok(())
253		}
254
255		/// Queues `n` small HRMP messages to `recipient`.
256		#[pallet::weight(0)]
257		pub fn queue_hrmp_messages(_: OriginFor<T>, n: u32, recipient: ParaId) -> DispatchResult {
258			PendingOutboundHrmpMessages::<T>::mutate(|messages| {
259				for i in 0..n {
260					messages.push((recipient, vec![(i % 256) as u8]));
261				}
262			});
263			Ok(())
264		}
265
266		/// Queues one HRMP message each to `n` consecutive recipients starting from
267		/// `first_recipient`.
268		#[pallet::weight(0)]
269		pub fn queue_hrmp_messages_to_n_recipients(
270			_: OriginFor<T>,
271			n: u32,
272			first_recipient: ParaId,
273		) -> DispatchResult {
274			PendingOutboundHrmpMessages::<T>::mutate(|messages| {
275				for i in 0..n {
276					messages.push((ParaId::from(u32::from(first_recipient) + i), vec![i as u8]));
277				}
278			});
279			Ok(())
280		}
281
282		/// Schedule a 1 second weight registration in the next `on_initialize`.
283		#[pallet::weight(0)]
284		pub fn schedule_weight_registration(_: OriginFor<T>) -> DispatchResult {
285			ScheduleWeightRegistration::<T>::set(true);
286			Ok(())
287		}
288
289		/// Set the weight to be consumed by the next inherent call.
290		#[pallet::weight(0)]
291		pub fn set_inherent_weight_consume(_: OriginFor<T>, weight: Weight) -> DispatchResult {
292			InherentWeightConsume::<T>::put(weight);
293			Ok(())
294		}
295
296		/// Consume weight via inherent call (clears the storage after consuming).
297		#[pallet::weight((
298			InherentWeightConsume::<T>::get().unwrap_or_default(),
299			DispatchClass::Mandatory
300		))]
301		pub fn consume_weight_inherent(origin: OriginFor<T>) -> DispatchResult {
302			ensure_none(origin)?;
303
304			// Clear the storage item to ensure this can only be called once per inherent
305			InherentWeightConsume::<T>::kill();
306
307			Ok(())
308		}
309
310		/// This function registers a high weight usage manually, while it actually only announces
311		/// to use a weight of `0` :)
312		///
313		/// Uses the [`TestTransactionExtension`] logic to ensure the transaction is only accepted
314		/// when we can fit the `1s` weight into the block.
315		#[pallet::weight(0)]
316		pub fn use_more_weight_than_announced(
317			_: OriginFor<T>,
318			_must_be_first_block_in_core: bool,
319		) -> DispatchResult {
320			// Register weight manually.
321			frame_system::Pallet::<T>::register_extra_weight_unchecked(
322				Weight::from_parts(WEIGHT_REF_TIME_PER_SECOND, 0),
323				DispatchClass::Normal,
324			);
325
326			Ok(())
327		}
328
329		/// Deposits the `UseFullCore` digest item to signal that this block should use the full
330		/// core.
331		#[pallet::weight(0)]
332		pub fn set_use_full_core(_: OriginFor<T>) -> DispatchResult {
333			frame_system::Pallet::<T>::deposit_log(CumulusDigestItem::UseFullCore.to_digest_item());
334			Ok(())
335		}
336	}
337
338	#[pallet::inherent]
339	impl<T: Config> ProvideInherent for Pallet<T> {
340		type Call = Call<T>;
341		type Error = sp_inherents::MakeFatalError<()>;
342		const INHERENT_IDENTIFIER: InherentIdentifier = INHERENT_IDENTIFIER;
343
344		fn create_inherent(_data: &InherentData) -> Option<Self::Call> {
345			// Check if there's weight to consume from storage
346			let weight_to_consume = InherentWeightConsume::<T>::get()?;
347
348			// Check if the weight fits in the remaining block capacity
349			let remaining_weight = frame_system::Pallet::<T>::remaining_block_weight();
350
351			if remaining_weight.can_consume(weight_to_consume) {
352				Some(Call::consume_weight_inherent {})
353			} else {
354				// Weight doesn't fit, don't create the inherent
355				None
356			}
357		}
358
359		fn is_inherent(call: &Self::Call) -> bool {
360			matches!(call, Call::consume_weight_inherent {})
361		}
362	}
363
364	#[derive(frame_support::DefaultNoBound)]
365	#[pallet::genesis_config]
366	pub struct GenesisConfig<T: Config> {
367		#[serde(skip)]
368		pub _config: core::marker::PhantomData<T>,
369		/// Controls if the `BigValueMove` logic is enabled.
370		pub enable_big_value_move: bool,
371		/// Activate HRMP sending with descending recipients from genesis.
372		pub enable_hrmp_sending: bool,
373	}
374
375	#[pallet::genesis_build]
376	impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
377		fn build(&self) {
378			sp_io::storage::set(TEST_RUNTIME_UPGRADE_KEY, &[1, 2, 3, 4]);
379
380			if self.enable_big_value_move {
381				BigValueMove::<T>::insert(BlockNumberFor::<T>::from(0u32), vec![0u8; 4 * 1024]);
382			}
383
384			if self.enable_hrmp_sending {
385				HrmpSendingActive::<T>::set(true);
386			}
387		}
388	}
389
390	#[pallet::event]
391	#[pallet::generate_deposit(pub(super) fn deposit_event)]
392	pub enum Event<T: Config> {
393		MovedBigValue { proof_size: u64 },
394	}
395
396	#[derive(
397		DebugNoBound,
398		Encode,
399		Decode,
400		CloneNoBound,
401		EqNoBound,
402		PartialEqNoBound,
403		TypeInfo,
404		DecodeWithMemTracking,
405	)]
406	#[scale_info(skip_type_params(T))]
407	pub struct TestTransactionExtension<T>(core::marker::PhantomData<T>);
408
409	impl<T> Default for TestTransactionExtension<T> {
410		fn default() -> Self {
411			Self(core::marker::PhantomData)
412		}
413	}
414
415	impl<T: Config> TransactionExtension<T::RuntimeCall> for TestTransactionExtension<T>
416	where
417		T: Config + Send + Sync,
418		T::RuntimeCall: IsSubType<Call<T>> + Dispatchable<Info = DispatchInfo>,
419	{
420		const IDENTIFIER: &'static str = "TestTransactionExtension";
421		type Implicit = ();
422		type Val = ();
423		type Pre = ();
424
425		fn validate(
426			&self,
427			origin: T::RuntimeOrigin,
428			call: &T::RuntimeCall,
429			_info: &DispatchInfo,
430			_len: usize,
431			_self_implicit: Self::Implicit,
432			_inherited_implication: &impl Implication,
433			_: TransactionSource,
434		) -> ValidateResult<Self::Val, T::RuntimeCall> {
435			if let Some(call) = call.is_sub_type() {
436				match call {
437					Call::use_more_weight_than_announced { must_be_first_block_in_core } => {
438						if {
439							let digest = frame_system::Pallet::<T>::digest();
440
441							CumulusDigestItem::find_block_bundle_info(&digest)
442								// Default being `true` to support `validate_transaction`
443								.map_or(true, |bi| {
444									// Either we want that the transaction goes into the first block
445									// of a core
446									bi.index == 0 && *must_be_first_block_in_core ||
447										// Or it goes to any block that isn't the first block
448										bi.index > 0 && !*must_be_first_block_in_core
449								})
450						} {
451							Ok((
452								ValidTransaction {
453									provides: vec![vec![1, 2, 3, 4, 5]],
454									..Default::default()
455								},
456								(),
457								origin,
458							))
459						} else {
460							Err(TransactionValidityError::Invalid(
461								InvalidTransaction::ExhaustsResources,
462							))
463						}
464					},
465					_ => Ok((Default::default(), (), origin)),
466				}
467			} else {
468				Ok((Default::default(), (), origin))
469			}
470		}
471
472		fn prepare(
473			self,
474			val: Self::Val,
475			_origin: &T::RuntimeOrigin,
476			_call: &T::RuntimeCall,
477			_info: &DispatchInfo,
478			_len: usize,
479		) -> Result<Self::Pre, TransactionValidityError> {
480			Ok(val)
481		}
482
483		fn weight(&self, _: &T::RuntimeCall) -> Weight {
484			Weight::zero()
485		}
486	}
487}
488
489impl<T: Config> cumulus_pallet_parachain_system::OnSystemEvent for Pallet<T> {
490	fn on_validation_data(_data: &cumulus_primitives_core::PersistedValidationData) {
491		// Nothing to do here for tests
492	}
493
494	fn on_validation_code_applied() {
495		// Nothing to do here for tests
496	}
497
498	fn on_relay_state_proof(
499		relay_state_proof: &cumulus_pallet_parachain_system::relay_state_snapshot::RelayChainStateProof,
500	) -> frame_support::weights::Weight {
501		use crate::{Balance, Nonce};
502		use frame_system::AccountInfo;
503		use pallet_balances::AccountData;
504
505		let alice_key = crate::test_pallet::relay_alice_account_key();
506
507		// Verify that Alice's account is included in the relay proof.
508		relay_state_proof
509			.read_optional_entry::<AccountInfo<Nonce, AccountData<Balance>>>(&alice_key)
510			.expect("Invalid relay chain state proof");
511
512		frame_support::weights::Weight::zero()
513	}
514}