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::receive`] takes the parachain's request, which carries the head data plus the hash
31//!    and length of the code that is coming, and parks it in [`PendingRegistrations`]. Only
32//!    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//! A registration authorization never times out here. The parachain sends
39//! [`MessageToRelayV1::CancelRegistration`] when the manager gives up, this pallet drops the entry
40//! and confirms, and the parachain releases the deposit. So the relay chain runs no per-block
41//! sweep, and whoever wants the deposit back pays for the round trip. No deposit is taken here.
42//!
43//! Code upgrade authorizations do lapse instead: no deposit is at stake, and their validity
44//! depends on relay-chain state the parachain does not track.
45//!
46//! ## Deregistration
47//!
48//! Applied as it arrives and answered once. A para already gone is confirmed, so the parachain's
49//! retry after a lost answer settles instead of being refused.
50//!
51//! ## Locking
52//!
53//! The relay chain also tells the parachain when a para produces a head, so the chain holding the
54//! manager relationship can lock the para. Add this pallet to `paras::Config::OnNewHead`.
55//!
56//! ## Runtime requirement
57//!
58//! `apply_authorized_code` authorizes itself through [`frame_support::pallet_macros::authorize`],
59//! so the runtime must carry `frame_system::AuthorizeCall` in its transaction extension pipeline.
60
61#![cfg_attr(not(feature = "std"), no_std)]
62
63extern crate alloc;
64
65use alloc::vec::Vec;
66use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen};
67use frame_support::{traits::Get, weights::Weight};
68pub use pallet::*;
69use polkadot_primitives::{HeadData, Id};
70use polkadot_runtime_parachains::paras::OnNewHead;
71use registrar_primitives::{
72	FailureReason, MessageToPara, MessageToParaV1, MessageToRelay, MessageToRelayV1, Outcome,
73	ParaId, ParachainRegistrar,
74};
75use scale_info::TypeInfo;
76use sp_core::H256;
77pub use weights::WeightInfo;
78
79pub mod weights;
80
81const LOG_TARGET: &str = "runtime::registrar-relay";
82
83#[cfg(feature = "runtime-benchmarks")]
84mod benchmarking;
85#[cfg(test)]
86mod mock;
87#[cfg(test)]
88mod tests;
89
90pub trait SendToPara {
91	/// Send `message` to the parachain.
92	///
93	/// `Err(())` means the transport refused the message; callers log and carry on rather than
94	/// unwind committed state.
95	#[allow(clippy::result_unit_err)]
96	fn send(message: MessageToPara) -> Result<(), ()>;
97}
98
99#[cfg(feature = "std")]
100impl SendToPara for () {
101	fn send(_message: MessageToPara) -> Result<(), ()> {
102		Ok(())
103	}
104}
105
106/// A registration the parachain has asked for, waiting on its validation code.
107#[derive(
108	Encode, Decode, DecodeWithMemTracking, Clone, Eq, PartialEq, TypeInfo, MaxEncodedLen, Debug,
109)]
110#[scale_info(skip_type_params(MaxHeadDataSize))]
111pub struct PendingRegistration<AccountId, MaxHeadDataSize: Get<u32>> {
112	/// The id of the [`MessageToRelayV1::Register`] that created this entry, echoed back in the
113	/// response once the code arrives.
114	pub message_id: u64,
115	/// The account managing this registration on the parachain.
116	pub manager: AccountId,
117	/// The genesis head data, held here until the code arrives.
118	pub genesis_head: frame_support::BoundedVec<u8, MaxHeadDataSize>,
119	/// Blake2-256 hash the validation code must have.
120	pub code_hash: H256,
121	/// Exact length the validation code must have; the parachain sized the deposit from it.
122	pub code_len: u32,
123}
124
125/// [`PendingRegistration`] as this pallet stores it.
126pub type PendingRegistrationOf<T> =
127	PendingRegistration<<T as frame_system::Config>::AccountId, <T as Config>::MaxHeadDataSize>;
128
129#[frame_support::pallet]
130pub mod pallet {
131	use super::*;
132	use frame_support::pallet_prelude::*;
133	use frame_system::pallet_prelude::*;
134	use sp_runtime::traits::{BlakeTwo256, Hash};
135
136	#[pallet::config]
137	pub trait Config: frame_system::Config {
138		/// The overarching event type.
139		#[allow(deprecated)]
140		type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
141
142		/// A trusted parachain parachain authorized to drive registrations.
143		type ParaOrigin: EnsureOrigin<Self::RuntimeOrigin>;
144
145		/// Sends messages to the parachain.
146		type SendToPara: SendToPara;
147
148		/// The relay chain's parachain registry.
149		type Registrar: ParachainRegistrar<AccountId = Self::AccountId>;
150
151		/// The largest head data this pallet will hold onto while waiting for code.
152		///
153		/// Should be at least the relay chain's `max_head_data_size`.
154		#[pallet::constant]
155		type MaxHeadDataSize: Get<u32>;
156
157		/// The largest validation code [`Pallet::apply_authorized_code`] will accept.
158		///
159		/// Should be at least the relay chain's `max_code_size`.
160		#[pallet::constant]
161		type MaxCodeSize: Get<u32>;
162
163		/// How many registrations may be waiting on their code at once.
164		///
165		/// Bounds the head data stored here, where no deposit is held.
166		#[pallet::constant]
167		type MaxPendingRegistrations: Get<u32>;
168
169		/// Priority given to a valid [`Pallet::apply_authorized_code`] in the transaction pool.
170		#[pallet::constant]
171		type UnsignedPriority: Get<TransactionPriority>;
172
173		/// Weight information for the extrinsics in this pallet.
174		type WeightInfo: WeightInfo;
175	}
176
177	#[pallet::pallet]
178	pub struct Pallet<T>(_);
179
180	/// Registrations waiting on their validation code, by para id.
181	///
182	/// Counted so [`Config::MaxPendingRegistrations`] can be enforced with a single read.
183	#[pallet::storage]
184	pub type PendingRegistrations<T: Config> =
185		CountedStorageMap<_, Blake2_128Concat, ParaId, PendingRegistrationOf<T>>;
186
187	/// Paras registered here that have not produced their first head yet, by para id.
188	#[pallet::storage]
189	pub type AwaitingFirstHead<T: Config> = StorageMap<_, Blake2_128Concat, ParaId, ()>;
190
191	#[pallet::event]
192	#[pallet::generate_deposit(pub(super) fn deposit_event)]
193	pub enum Event<T: Config> {
194		/// A registration request was accepted and is waiting on its validation code.
195		RegistrationPending { para_id: ParaId, message_id: u64, code_hash: H256 },
196		/// A registration request was rejected out of hand.
197		RegistrationRejected { para_id: ParaId, message_id: u64, reason: FailureReason },
198		/// A para was onboarded.
199		Registered { para_id: ParaId, message_id: u64, manager: T::AccountId },
200		/// An authorization was dropped at the parachain's request.
201		AuthorizationCancelled { para_id: ParaId, message_id: u64 },
202		/// A cancellation arrived after the para had already been onboarded, and was refused.
203		CancellationRefused { para_id: ParaId, message_id: u64 },
204		/// A report could not be sent back to the parachain.
205		ReportFailed { para_id: ParaId, message_id: u64 },
206		/// The parachain was told that a para produced its first head.
207		HeadNoted { para_id: ParaId },
208		/// A para's first head could not be reported to the parachain.
209		HeadNoteFailed { para_id: ParaId },
210		/// A para was dropped from the registry, or had already left it.
211		Deregistered { para_id: ParaId, message_id: u64 },
212		/// The registry refused to drop a para.
213		DeregistrationRejected { para_id: ParaId, message_id: u64 },
214	}
215
216	#[pallet::error]
217	pub enum Error<T> {
218		/// No registration is waiting on code for this para id.
219		NothingPending,
220		/// The validation code does not match the hash the parachain committed to.
221		CodeHashMismatch,
222		/// The validation code is not the length the parachain committed to.
223		CodeLenMismatch,
224		/// The validation code is larger than this pallet will accept.
225		CodeTooLarge,
226	}
227
228	#[pallet::call]
229	impl<T: Config> Pallet<T> {
230		/// Accept a control-plane message from the parachain's registrar pallet.
231		///
232		/// The origin is a trusted XCM origin, not a user. A rejected request still returns `Ok`:
233		/// failing would roll back the rejection report and leave the parachain holding a deposit
234		/// for news that never comes.
235		#[pallet::call_index(0)]
236		#[pallet::weight(match message {
237			MessageToRelay::V1(MessageToRelayV1::Register { genesis_head, .. }) =>
238				T::WeightInfo::receive_register(genesis_head.len() as u32),
239			MessageToRelay::V1(MessageToRelayV1::CancelRegistration { .. }) =>
240				T::WeightInfo::receive_cancel_registration(),
241			MessageToRelay::V1(MessageToRelayV1::Deregister { .. }) =>
242				T::WeightInfo::receive_deregister(),
243			MessageToRelay::V1(MessageToRelayV1::AuthorizeCodeUpgrade { .. }) |
244			MessageToRelay::V1(MessageToRelayV1::SetCurrentHead { .. }) => Weight::MAX,
245		})]
246		pub fn receive(
247			origin: OriginFor<T>,
248			message: MessageToRelay<T::AccountId>,
249		) -> DispatchResult {
250			T::ParaOrigin::ensure_origin_or_root(origin)?;
251
252			match message {
253				MessageToRelay::V1(MessageToRelayV1::Register {
254					para_id,
255					message_id,
256					manager,
257					genesis_head,
258					code_hash,
259					code_len,
260				}) => Self::on_register_request(
261					para_id,
262					message_id,
263					manager,
264					genesis_head,
265					code_hash,
266					code_len,
267				),
268				MessageToRelay::V1(MessageToRelayV1::CancelRegistration {
269					para_id,
270					message_id,
271				}) => Self::on_cancel_request(para_id, message_id),
272				MessageToRelay::V1(MessageToRelayV1::Deregister { para_id, message_id }) => {
273					Self::on_deregister_request(para_id, message_id)
274				},
275				MessageToRelay::V1(MessageToRelayV1::AuthorizeCodeUpgrade {
276					para_id,
277					message_id,
278					code_hash,
279					code_len,
280				}) => Self::on_authorize_code_upgrade_request(
281					para_id, message_id, code_hash, code_len,
282				),
283				MessageToRelay::V1(MessageToRelayV1::SetCurrentHead {
284					para_id,
285					message_id,
286					head,
287				}) => Self::on_set_current_head_request(para_id, message_id, head),
288			}
289
290			Ok(())
291		}
292
293		/// Upload the validation code for a pending authorization, onboarding the para.
294		///
295		/// Unsigned and free: the pending entry already pins the exact bytes accepted, and the
296		/// manager has paid for them on the parachain.
297		#[pallet::call_index(1)]
298		#[pallet::authorize(Self::authorize_apply_authorized_code)]
299		#[pallet::weight_of_authorize(T::WeightInfo::authorize_apply_authorized_code(validation_code.len() as u32))]
300		#[pallet::weight(T::WeightInfo::apply_authorized_code(validation_code.len() as u32))]
301		pub fn apply_authorized_code(
302			origin: OriginFor<T>,
303			para_id: ParaId,
304			validation_code: Vec<u8>,
305		) -> DispatchResultWithPostInfo {
306			ensure_authorized(origin)?;
307
308			let pending = Self::validate_pending_code(para_id, &validation_code)?;
309
310			T::Registrar::register(
311				pending.manager.clone(),
312				para_id,
313				pending.genesis_head.into_inner(),
314				validation_code,
315			)?;
316			PendingRegistrations::<T>::remove(para_id);
317
318			let message_id = pending.message_id;
319			Self::report_registration(para_id, message_id, Ok(()));
320			Self::deposit_event(Event::Registered {
321				para_id,
322				message_id,
323				manager: pending.manager,
324			});
325			Ok(Pays::No.into())
326		}
327	}
328
329	impl<T: Config> Pallet<T> {
330		/// Validate an unsigned [`Pallet::apply_authorized_code`], with the same checks as the
331		/// dispatch so the pool and the block cannot disagree.
332		// `#[pallet::authorize]` passes the call arguments by reference, so the types must mirror
333		// the call's exactly; `&[u8]` would not compile.
334		#[allow(clippy::ptr_arg)]
335		pub fn authorize_apply_authorized_code(
336			_source: TransactionSource,
337			para_id: &ParaId,
338			validation_code: &Vec<u8>,
339		) -> TransactionValidityWithRefund {
340			let pending = Self::validate_pending_code(*para_id, validation_code)
341				.map_err(|e| InvalidTransaction::Custom(Self::err_to_code(e)))?;
342
343			// No longevity bound: an authorization does not expire, so the transaction stays valid
344			// until the code is applied or the parachain cancels, and revalidation drops it then.
345			let validity = ValidTransaction::with_tag_prefix("RegistrarApplyAuthorizedCode")
346				.priority(T::UnsignedPriority::get())
347				.and_provides((*para_id, pending.code_hash))
348				.propagate(true)
349				.build()?;
350
351			Ok((validity, Weight::zero()))
352		}
353
354		/// Apply a registration request from the parachain, accepting or rejecting it.
355		fn on_register_request(
356			para_id: ParaId,
357			message_id: u64,
358			manager: T::AccountId,
359			genesis_head: Vec<u8>,
360			code_hash: H256,
361			code_len: u32,
362		) {
363			let Ok(head_len) = u32::try_from(genesis_head.len()) else {
364				return Self::reject(para_id, message_id, FailureReason::InvalidOnboardingData);
365			};
366
367			if T::Registrar::is_registered(para_id) ||
368				PendingRegistrations::<T>::contains_key(para_id)
369			{
370				return Self::reject(para_id, message_id, FailureReason::AlreadyRegistered);
371			}
372			if PendingRegistrations::<T>::count() >= T::MaxPendingRegistrations::get() {
373				return Self::reject(para_id, message_id, FailureReason::TooManyPending);
374			}
375			if code_len > T::MaxCodeSize::get() ||
376				T::Registrar::check_onboarding(head_len, code_len).is_err()
377			{
378				return Self::reject(para_id, message_id, FailureReason::InvalidOnboardingData);
379			}
380			let Ok(genesis_head) = BoundedVec::try_from(genesis_head) else {
381				return Self::reject(para_id, message_id, FailureReason::InvalidOnboardingData);
382			};
383
384			PendingRegistrations::<T>::insert(
385				para_id,
386				PendingRegistration { message_id, manager, genesis_head, code_hash, code_len },
387			);
388
389			AwaitingFirstHead::<T>::insert(para_id, ());
390
391			Self::deposit_event(Event::RegistrationPending { para_id, message_id, code_hash });
392		}
393
394		/// Turn a request away and tell the parachain to release the deposit.
395		fn reject(para_id: ParaId, message_id: u64, reason: FailureReason) {
396			Self::report_registration(para_id, message_id, Err(reason.clone()));
397			Self::deposit_event(Event::RegistrationRejected { para_id, message_id, reason });
398		}
399
400		/// Drop the authorization for `para_id` and tell the parachain to release the deposit.
401		///
402		/// If the code already landed the cancellation is refused and the deposit is kept. Nothing
403		/// pending still gets an `Ok`: the request may have been rejected here and the report lost.
404		fn on_cancel_request(para_id: ParaId, message_id: u64) {
405			PendingRegistrations::<T>::remove(para_id);
406
407			if T::Registrar::is_registered(para_id) {
408				Self::report_cancellation(
409					para_id,
410					message_id,
411					Err(FailureReason::AlreadyRegistered),
412				);
413				return Self::deposit_event(Event::CancellationRefused { para_id, message_id });
414			}
415
416			AwaitingFirstHead::<T>::remove(para_id);
417
418			Self::report_cancellation(para_id, message_id, Ok(()));
419			Self::deposit_event(Event::AuthorizationCancelled { para_id, message_id });
420		}
421
422		/// Check `validation_code` against the pending entry for `para_id`.
423		fn validate_pending_code(
424			para_id: ParaId,
425			validation_code: &[u8],
426		) -> Result<PendingRegistrationOf<T>, Error<T>> {
427			// Bound the work before hashing, so an oversized blob is rejected cheaply.
428			let code_len =
429				u32::try_from(validation_code.len()).map_err(|_| Error::<T>::CodeTooLarge)?;
430			ensure!(code_len <= T::MaxCodeSize::get(), Error::<T>::CodeTooLarge);
431
432			let pending =
433				PendingRegistrations::<T>::get(para_id).ok_or(Error::<T>::NothingPending)?;
434			ensure!(code_len == pending.code_len, Error::<T>::CodeLenMismatch);
435			ensure!(
436				BlakeTwo256::hash(validation_code) == pending.code_hash,
437				Error::<T>::CodeHashMismatch
438			);
439
440			Ok(pending)
441		}
442
443		/// Map a validation failure onto the `InvalidTransaction::Custom` code it reports.
444		pub fn err_to_code(error: Error<T>) -> u8 {
445			match error {
446				Error::<T>::NothingPending => 0,
447				Error::<T>::CodeHashMismatch => 1,
448				Error::<T>::CodeLenMismatch => 2,
449				Error::<T>::CodeTooLarge => 3,
450			}
451		}
452
453		/// Tell the parachain how a registration ended.
454		fn report_registration(para_id: ParaId, message_id: u64, outcome: Outcome) {
455			Self::report(
456				para_id,
457				message_id,
458				MessageToParaV1::RegisterResponse { para_id, message_id, outcome },
459			);
460		}
461
462		/// Tell the parachain what became of its cancellation.
463		fn report_cancellation(para_id: ParaId, message_id: u64, outcome: Outcome) {
464			Self::report(
465				para_id,
466				message_id,
467				MessageToParaV1::CancelResponse { para_id, message_id, outcome },
468			);
469		}
470
471		/// Drop `para_id` at the parachain's request.
472		///
473		/// A para already gone is confirmed rather than refused, so a retried request settles.
474		fn on_deregister_request(para_id: ParaId, message_id: u64) {
475			if let Err(e) = T::Registrar::deregister(para_id) {
476				log::debug!(
477					target: LOG_TARGET,
478					"registry refused to drop para {para_id}: {e:?}",
479				);
480				Self::report_deregistration(
481					para_id,
482					message_id,
483					Err(FailureReason::NotDeregisterable),
484				);
485				return Self::deposit_event(Event::DeregistrationRejected { para_id, message_id });
486			}
487
488			AwaitingFirstHead::<T>::remove(para_id);
489			Self::report_deregistration(para_id, message_id, Ok(()));
490			Self::deposit_event(Event::Deregistered { para_id, message_id });
491		}
492
493		fn on_authorize_code_upgrade_request(
494			para_id: ParaId,
495			message_id: u64,
496			code_hash: H256,
497			code_len: u32,
498		) {
499			let _ = (para_id, message_id, code_hash, code_len);
500			// TODO(ahm-v2): authorize the code upgrade and report the outcome back.
501		}
502
503		fn on_set_current_head_request(para_id: ParaId, message_id: u64, head: Vec<u8>) {
504			let _ = (para_id, message_id, head);
505			// TODO(ahm-v2): set the para's head and report the outcome back.
506		}
507
508		/// Tell the parachain how a deregistration ended.
509		fn report_deregistration(para_id: ParaId, message_id: u64, outcome: Outcome) {
510			Self::report(
511				para_id,
512				message_id,
513				MessageToParaV1::DeregisterResponse { para_id, message_id, outcome },
514			);
515		}
516
517		#[allow(dead_code)]
518		fn report_code_upgrade(
519			para_id: ParaId,
520			message_id: u64,
521			outcome: Result<u32, FailureReason>,
522		) {
523			Self::report(
524				para_id,
525				message_id,
526				MessageToParaV1::CodeUpgradeResponse { para_id, message_id, outcome },
527			);
528		}
529
530		#[allow(dead_code)]
531		fn report_code_upgrade_scheduled(para_id: ParaId, message_id: u64) {
532			Self::report(
533				para_id,
534				message_id,
535				MessageToParaV1::CodeUpgradeScheduled { para_id, message_id },
536			);
537		}
538
539		#[allow(dead_code)]
540		fn report_set_head(para_id: ParaId, message_id: u64, outcome: Outcome) {
541			Self::report(
542				para_id,
543				message_id,
544				MessageToParaV1::SetHeadResponse { para_id, message_id, outcome },
545			);
546		}
547
548		/// Hand a report to the transport.
549		///
550		/// A failure is only logged and evented: callers have committed relay-chain state that must
551		/// not unwind because a report bounced.
552		fn report(para_id: ParaId, message_id: u64, message: MessageToParaV1) {
553			if T::SendToPara::send(MessageToPara::V1(message)).is_err() {
554				log::error!(
555					target: LOG_TARGET,
556					"failed to report the outcome for para {para_id} back to the parachain",
557				);
558				Self::deposit_event(Event::ReportFailed { para_id, message_id });
559			}
560		}
561	}
562}
563
564/// Tells the parachain the first time a para produces a head, so it can lock the para.
565///
566/// The caller is a hook that cannot fail, so a send failure is only logged and evented. Nothing is
567/// recorded in that case, so the next head retries.
568impl<T: Config> OnNewHead for Pallet<T> {
569	fn on_new_head(id: Id, _head: &HeadData) -> Weight {
570		let para_id: ParaId = id.into();
571		if !AwaitingFirstHead::<T>::contains_key(para_id) {
572			return T::WeightInfo::on_new_head_already_noted();
573		}
574
575		if T::SendToPara::send(MessageToPara::V1(MessageToParaV1::HeadNoted { para_id })).is_err() {
576			log::error!(
577				target: LOG_TARGET,
578				"failed to tell the parachain about a new head for para {para_id}",
579			);
580			Self::deposit_event(Event::HeadNoteFailed { para_id });
581		} else {
582			AwaitingFirstHead::<T>::remove(para_id);
583			Self::deposit_event(Event::HeadNoted { para_id });
584		}
585
586		T::WeightInfo::on_new_head()
587	}
588}