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