referrerpolicy=no-referrer-when-downgrade

pallet_registrar_relay/
lib.rs

1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: Apache-2.0
5
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// 	http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18//! # Relay-chain registrar pallet
19//!
20//! Relay half of the parachain registrar. Runs on the relay chain, applying registrations
21//! authorized on a parachain (`pallet-registrar-para`) and driving the relay's legacy `paras`
22//! state.
23//!
24//! ## Two-phase registration
25//!
26//! Pushing a multi-megabyte validation code through XCM would be wasteful when the parachain can
27//! commit to the exact bytes and let anybody upload them here directly, so registration arrives in
28//! two pieces:
29//!
30//! 1. [`Pallet::authorize_code`] takes the parachain's request, which carries the head data plus
31//!    the hash and length of the code that is coming, and parks it in [`PendingRegistrations`].
32//!    Only callable by a trusted XCM origin (e.g. the Coretime chain).
33//! 2. [`Pallet::apply_authorized_code`] takes the blob itself. It needs no signature: anybody may
34//!    push the code, because a pending entry already pins down exactly which bytes are acceptable,
35//!    and the parachain has already made the manager pay for them. If the blob matches, the para is
36//!    onboarded and the outcome is reported back to the parachain.
37//!
38//! An authorization does not time out here. If the code never turns up, missing the deadline is the
39//! manager's problem, not this chain's: the parachain sends [`Pallet::cancel_authorization`] when
40//! it gives up, this pallet drops the entry and confirms, and the parachain releases the deposit.
41//! So no per-block sweep runs on the relay chain, and whoever wants the deposit back pays for the
42//! round trip. No deposit is ever taken here.
43//!
44//! ## Runtime requirement
45//!
46//! `apply_authorized_code` authorizes itself through [`frame_support::pallet_macros::authorize`],
47//! so the runtime must carry `frame_system::AuthorizeCall` in its transaction extension pipeline.
48
49#![cfg_attr(not(feature = "std"), no_std)]
50
51extern crate alloc;
52
53use alloc::vec::Vec;
54use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen};
55use frame_support::traits::Get;
56use registrar_primitives::{
57	FailureReason, MessageToPara, MessageToParaV1, MessageToRelay, MessageToRelayV1, Outcome,
58	ParaId, ParachainRegistrar,
59};
60use scale_info::TypeInfo;
61use sp_core::H256;
62
63pub use pallet::*;
64pub use weights::WeightInfo;
65
66pub mod weights;
67
68#[cfg(feature = "runtime-benchmarks")]
69mod benchmarking;
70#[cfg(test)]
71mod mock;
72#[cfg(test)]
73mod tests;
74
75pub trait SendToPara {
76	/// Send `message` to the parachain.
77	///
78	/// `Err(())` means the transport refused the message. Callers here are mid-way through
79	/// applying state that must survive, so they log and carry on rather than unwinding.
80	#[allow(clippy::result_unit_err)]
81	fn send(message: MessageToPara) -> Result<(), ()>;
82}
83
84#[cfg(feature = "std")]
85impl SendToPara for () {
86	fn send(_message: MessageToPara) -> Result<(), ()> {
87		Ok(())
88	}
89}
90
91/// A registration the parachain has asked for, waiting on its validation code.
92#[derive(
93	Encode, Decode, DecodeWithMemTracking, Clone, Eq, PartialEq, TypeInfo, MaxEncodedLen, Debug,
94)]
95#[scale_info(skip_type_params(MaxHeadDataSize))]
96pub struct PendingRegistration<AccountId, MaxHeadDataSize: Get<u32>> {
97	/// The id of the [`MessageToRelayV1::Register`] that created this entry, echoed back in the
98	/// response once the code arrives.
99	pub message_id: u64,
100	/// The account managing this registration on the parachain.
101	pub manager: AccountId,
102	/// The genesis head data, held here until the code arrives.
103	pub genesis_head: frame_support::BoundedVec<u8, MaxHeadDataSize>,
104	/// Blake2-256 hash the validation code must have.
105	pub code_hash: H256,
106	/// Exact length the validation code must have.
107	///
108	/// The parachain sized the manager's deposit from this, so a blob of any other length would
109	/// mean the manager underpaid, even if the hash somehow matched.
110	pub code_len: u32,
111}
112
113/// How much head data a message carries, for weighing [`Pallet::authorize_code`].
114///
115/// Worth doing rather than always charging `MaxHeadDataSize`: that bound is a megabyte on
116/// production relay chains, and a typical genesis head is nowhere near it.
117pub fn head_data_len<AccountId>(message: &MessageToRelay<AccountId>) -> u32 {
118	match message {
119		MessageToRelay::V1(MessageToRelayV1::Register { genesis_head, .. }) => {
120			genesis_head.len() as u32
121		},
122		MessageToRelay::V1(MessageToRelayV1::CancelRegistration { .. }) => 0,
123	}
124}
125
126/// [`PendingRegistration`] as this pallet stores it.
127pub type PendingRegistrationOf<T> =
128	PendingRegistration<<T as frame_system::Config>::AccountId, <T as Config>::MaxHeadDataSize>;
129
130#[frame_support::pallet]
131pub mod pallet {
132	use super::*;
133	use frame_support::pallet_prelude::*;
134	use frame_system::pallet_prelude::*;
135	use sp_runtime::traits::{BlakeTwo256, Hash};
136
137	#[pallet::config]
138	pub trait Config: frame_system::Config {
139		/// The overarching event type.
140		#[allow(deprecated)]
141		type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
142
143		/// A trusted parachain parachain authorized to drive registrations.
144		type ParaOrigin: EnsureOrigin<Self::RuntimeOrigin>;
145
146		/// Sends messages to the parachain.
147		type SendToPara: SendToPara;
148
149		/// The relay chain's parachain registry.
150		type Registrar: ParachainRegistrar<AccountId = Self::AccountId>;
151
152		/// The largest head data this pallet will hold onto while waiting for code.
153		///
154		/// Should be at least the relay chain's `max_head_data_size`.
155		#[pallet::constant]
156		type MaxHeadDataSize: Get<u32>;
157
158		/// The largest validation code [`Pallet::apply_authorized_code`] will accept.
159		///
160		/// Should be at least the relay chain's `max_code_size`.
161		#[pallet::constant]
162		type MaxCodeSize: Get<u32>;
163
164		/// How many registrations may be waiting on their code at once.
165		///
166		/// Bounds the head data this pallet stores while no deposit is held here. Entries only ever
167		/// leave by the code landing or by the parachain cancelling, so a manager who does neither
168		/// occupies a slot for as long as they keep paying the deposit on the parachain.
169		#[pallet::constant]
170		type MaxPendingRegistrations: Get<u32>;
171
172		/// Priority given to a valid [`Pallet::apply_authorized_code`] in the transaction pool.
173		#[pallet::constant]
174		type UnsignedPriority: Get<TransactionPriority>;
175
176		/// Weight information for the extrinsics in this pallet.
177		type WeightInfo: WeightInfo;
178	}
179
180	#[pallet::pallet]
181	pub struct Pallet<T>(_);
182
183	/// Registrations waiting on their validation code, by para id.
184	///
185	/// Counted so [`Config::MaxPendingRegistrations`] can be enforced with a single read.
186	#[pallet::storage]
187	pub type PendingRegistrations<T: Config> =
188		CountedStorageMap<_, Blake2_128Concat, ParaId, PendingRegistrationOf<T>>;
189
190	#[pallet::event]
191	#[pallet::generate_deposit(pub(super) fn deposit_event)]
192	pub enum Event<T: Config> {
193		/// A registration request was accepted and is waiting on its validation code.
194		RegistrationPending { para_id: ParaId, message_id: u64, code_hash: H256 },
195		/// A registration request was rejected out of hand.
196		RegistrationRejected { para_id: ParaId, message_id: u64, reason: FailureReason },
197		/// A para was onboarded.
198		Registered { para_id: ParaId, message_id: u64, manager: T::AccountId },
199		/// An authorization was dropped at the parachain's request.
200		AuthorizationCancelled { para_id: ParaId, message_id: u64 },
201		/// A cancellation arrived after the para had already been onboarded, and was refused.
202		CancellationRefused { para_id: ParaId, message_id: u64 },
203		/// A report could not be sent back to the parachain.
204		///
205		/// The relay chain's own state is already correct; the parachain is now out of step and
206		/// will need its manager to ask again.
207		ReportFailed { para_id: ParaId, message_id: u64 },
208	}
209
210	#[pallet::error]
211	pub enum Error<T> {
212		/// No registration is waiting on code for this para id.
213		NothingPending,
214		/// The validation code does not match the hash the parachain committed to.
215		CodeHashMismatch,
216		/// The validation code is not the length the parachain committed to.
217		CodeLenMismatch,
218		/// The validation code is larger than this pallet will accept.
219		CodeTooLarge,
220		/// The message is not one this call serves.
221		UnexpectedMessage,
222	}
223
224	#[pallet::call]
225	impl<T: Config> Pallet<T> {
226		/// Accept a control-plane message from the parachain's registrar pallet and authorize the
227		/// validation code that will follow.
228		///
229		/// Only callable by a trusted XCM origin (e.g. the Coretime chain), never by users.
230		///
231		/// A request this pallet will not act on is *not* an extrinsic failure. Failing would roll
232		/// back the rejection report along with everything else, and the parachain would sit on a
233		/// held deposit waiting for news that never comes. So a rejection is applied, reported, and
234		/// returns `Ok`.
235		#[pallet::call_index(0)]
236		#[pallet::weight(T::WeightInfo::authorize_code(head_data_len(message)))]
237		pub fn authorize_code(
238			origin: OriginFor<T>,
239			message: MessageToRelay<T::AccountId>,
240		) -> DispatchResult {
241			T::ParaOrigin::ensure_origin_or_root(origin)?;
242
243			match message {
244				MessageToRelay::V1(MessageToRelayV1::Register {
245					para_id,
246					message_id,
247					manager,
248					genesis_head,
249					code_hash,
250					code_len,
251				}) => {
252					Self::on_register_request(
253						para_id,
254						message_id,
255						manager,
256						genesis_head,
257						code_hash,
258						code_len,
259					);
260					Ok(())
261				},
262				_ => Err(Error::<T>::UnexpectedMessage.into()),
263			}
264		}
265
266		/// Upload the validation code for a pending authorization, onboarding the para.
267		///
268		/// Needs no signature and pays no fee. Anybody may submit: the pending entry already fixes
269		/// the exact bytes that will be accepted, and the manager has already paid for them on the
270		/// parachain.
271		#[pallet::call_index(1)]
272		#[pallet::authorize(Self::authorize_apply_authorized_code)]
273		#[pallet::weight_of_authorize(T::WeightInfo::authorize_apply_authorized_code(validation_code.len() as u32))]
274		#[pallet::weight(T::WeightInfo::apply_authorized_code(validation_code.len() as u32))]
275		pub fn apply_authorized_code(
276			origin: OriginFor<T>,
277			para_id: ParaId,
278			validation_code: Vec<u8>,
279		) -> DispatchResultWithPostInfo {
280			ensure_authorized(origin)?;
281
282			let pending = Self::validate_pending_code(para_id, &validation_code)?;
283
284			T::Registrar::register(
285				pending.manager.clone(),
286				para_id,
287				pending.genesis_head.into_inner(),
288				validation_code,
289			)?;
290			PendingRegistrations::<T>::remove(para_id);
291
292			let message_id = pending.message_id;
293			Self::report_registration(para_id, message_id, Ok(()));
294			Self::deposit_event(Event::Registered {
295				para_id,
296				message_id,
297				manager: pending.manager,
298			});
299			Ok(Pays::No.into())
300		}
301
302		/// Drop the authorization held for a para id, at the parachain's request.
303		///
304		/// Only callable by a trusted XCM origin (e.g. the Coretime chain), never by users. This is
305		/// the only way an authorization that never received its code goes away, and the manager
306		/// pays for it on the parachain: nothing here expires on its own.
307		///
308		/// Answered with [`MessageToParaV1::CancelResponse`], which is what lets the parachain
309		/// release the deposit. Refused, and reported as such, if the code did land in the meantime
310		/// and the para is registered: the deposit is then owed after all.
311		///
312		/// Cancelling something that was never pending is not an error. The request may simply have
313		/// been rejected here and the report lost, and the parachain still needs an answer it can
314		/// act on.
315		#[pallet::call_index(2)]
316		#[pallet::weight(T::WeightInfo::cancel_authorization())]
317		pub fn cancel_authorization(
318			origin: OriginFor<T>,
319			message: MessageToRelay<T::AccountId>,
320		) -> DispatchResult {
321			T::ParaOrigin::ensure_origin_or_root(origin)?;
322
323			match message {
324				MessageToRelay::V1(MessageToRelayV1::CancelRegistration {
325					para_id,
326					message_id,
327				}) => {
328					Self::on_cancel_request(para_id, message_id);
329					Ok(())
330				},
331				_ => Err(Error::<T>::UnexpectedMessage.into()),
332			}
333		}
334	}
335
336	impl<T: Config> Pallet<T> {
337		/// Decide whether an unsigned [`Pallet::apply_authorized_code`] may enter the pool and a
338		/// block.
339		///
340		/// Runs exactly the same checks as the dispatch, so the pool and the block never disagree
341		/// about which bytes are acceptable.
342		// `#[pallet::authorize]` hands the call arguments over by reference, so the parameter
343		// types have to mirror the call's exactly. `&[u8]` would not compile.
344		#[allow(clippy::ptr_arg)]
345		pub fn authorize_apply_authorized_code(
346			_source: TransactionSource,
347			para_id: &ParaId,
348			validation_code: &Vec<u8>,
349		) -> TransactionValidityWithRefund {
350			let pending = Self::validate_pending_code(*para_id, validation_code)
351				.map_err(|e| InvalidTransaction::Custom(Self::err_to_code(e)))?;
352
353			// No longevity bound: an authorization does not expire, so the transaction stays valid
354			// until the code is applied or the parachain cancels, and revalidation drops it then.
355			let validity = ValidTransaction::with_tag_prefix("RegistrarApplyAuthorizedCode")
356				.priority(T::UnsignedPriority::get())
357				.and_provides((*para_id, pending.code_hash))
358				.propagate(true)
359				.build()?;
360
361			Ok((validity, Weight::zero()))
362		}
363
364		/// Apply a registration request from the parachain, accepting or rejecting it.
365		fn on_register_request(
366			para_id: ParaId,
367			message_id: u64,
368			manager: T::AccountId,
369			genesis_head: Vec<u8>,
370			code_hash: H256,
371			code_len: u32,
372		) {
373			let Ok(head_len) = u32::try_from(genesis_head.len()) else {
374				return Self::reject(para_id, message_id, FailureReason::InvalidOnboardingData);
375			};
376
377			if T::Registrar::is_registered(para_id) ||
378				PendingRegistrations::<T>::contains_key(para_id)
379			{
380				return Self::reject(para_id, message_id, FailureReason::AlreadyRegistered);
381			}
382			if PendingRegistrations::<T>::count() >= T::MaxPendingRegistrations::get() {
383				return Self::reject(para_id, message_id, FailureReason::TooManyPending);
384			}
385			if code_len > T::MaxCodeSize::get() ||
386				T::Registrar::check_onboarding(head_len, code_len).is_err()
387			{
388				return Self::reject(para_id, message_id, FailureReason::InvalidOnboardingData);
389			}
390			let Ok(genesis_head) = BoundedVec::try_from(genesis_head) else {
391				return Self::reject(para_id, message_id, FailureReason::InvalidOnboardingData);
392			};
393
394			PendingRegistrations::<T>::insert(
395				para_id,
396				PendingRegistration { message_id, manager, genesis_head, code_hash, code_len },
397			);
398
399			Self::deposit_event(Event::RegistrationPending { para_id, message_id, code_hash });
400		}
401
402		/// Turn a request away and tell the parachain to release the deposit.
403		fn reject(para_id: ParaId, message_id: u64, reason: FailureReason) {
404			Self::report_registration(para_id, message_id, Err(reason.clone()));
405			Self::deposit_event(Event::RegistrationRejected { para_id, message_id, reason });
406		}
407
408		/// Drop the authorization for `para_id`, unless the code beat the cancellation here.
409		///
410		/// The relay chain is the authority on which of the two happened first, which is what makes
411		/// it safe for the parachain to release a deposit on the strength of this answer. A para id
412		/// this chain has registered is not one whose deposit can be handed back, so that is the
413		/// whole test. The entry goes either way: once the id is taken, an authorization for it can
414		/// never be applied.
415		fn on_cancel_request(para_id: ParaId, message_id: u64) {
416			PendingRegistrations::<T>::remove(para_id);
417
418			if T::Registrar::is_registered(para_id) {
419				Self::report_cancellation(
420					para_id,
421					message_id,
422					Err(FailureReason::AlreadyRegistered),
423				);
424				return Self::deposit_event(Event::CancellationRefused { para_id, message_id });
425			}
426
427			Self::report_cancellation(para_id, message_id, Ok(()));
428			Self::deposit_event(Event::AuthorizationCancelled { para_id, message_id });
429		}
430
431		/// Check `validation_code` against the pending entry for `para_id`.
432		fn validate_pending_code(
433			para_id: ParaId,
434			validation_code: &[u8],
435		) -> Result<PendingRegistrationOf<T>, Error<T>> {
436			// Bound the work before hashing, so an oversized blob is rejected cheaply.
437			let code_len =
438				u32::try_from(validation_code.len()).map_err(|_| Error::<T>::CodeTooLarge)?;
439			ensure!(code_len <= T::MaxCodeSize::get(), Error::<T>::CodeTooLarge);
440
441			let pending =
442				PendingRegistrations::<T>::get(para_id).ok_or(Error::<T>::NothingPending)?;
443			ensure!(code_len == pending.code_len, Error::<T>::CodeLenMismatch);
444			ensure!(
445				BlakeTwo256::hash(validation_code) == pending.code_hash,
446				Error::<T>::CodeHashMismatch
447			);
448
449			Ok(pending)
450		}
451
452		/// Map a validation failure onto the `InvalidTransaction::Custom` code it reports.
453		pub fn err_to_code(error: Error<T>) -> u8 {
454			match error {
455				Error::<T>::NothingPending => 0,
456				Error::<T>::CodeHashMismatch => 1,
457				Error::<T>::CodeLenMismatch => 2,
458				Error::<T>::CodeTooLarge => 3,
459				Error::<T>::UnexpectedMessage => 4,
460			}
461		}
462
463		/// Tell the parachain how a registration ended.
464		fn report_registration(para_id: ParaId, message_id: u64, outcome: Outcome) {
465			Self::report(
466				para_id,
467				message_id,
468				MessageToParaV1::RegisterResponse { para_id, message_id, outcome },
469			);
470		}
471
472		/// Tell the parachain what became of its cancellation.
473		fn report_cancellation(para_id: ParaId, message_id: u64, outcome: Outcome) {
474			Self::report(
475				para_id,
476				message_id,
477				MessageToParaV1::CancelResponse { para_id, message_id, outcome },
478			);
479		}
480
481		/// Hand a report to the transport.
482		///
483		/// A transport failure is only logged and surfaced as an event: every caller has already
484		/// committed relay-chain state that must not be unwound just because the report bounced.
485		fn report(para_id: ParaId, message_id: u64, message: MessageToParaV1) {
486			if T::SendToPara::send(MessageToPara::V1(message)).is_err() {
487				log::error!(
488					target: "runtime::registrar-relay",
489					"failed to report the outcome for para {para_id} back to the parachain",
490				);
491				Self::deposit_event(Event::ReportFailed { para_id, message_id });
492			}
493		}
494	}
495}