referrerpolicy=no-referrer-when-downgrade

pallet_whitelist/
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//! # Whitelist Pallet
19//!
20//! - [`Config`]
21//! - [`Call`]
22//!
23//! ## Overview
24//!
25//! Allow some configurable origin: [`Config::WhitelistOrigin`] to whitelist some hash of a call,
26//! and allow another configurable origin: [`Config::DispatchWhitelistedOrigin`] to dispatch them
27//! with the root origin.
28//!
29//! In the meantime the call corresponding to the hash must have been submitted to the pre-image
30//! handler [`pallet::Config::Preimages`].
31//!
32//! ## Authorized dispatch
33//!
34//! If [`Config::DispatchWhitelistedOrigin`] accepts `Authorized` (e.g. via
35//! [`frame_system::EnsureAuthorized`]), whitelisted calls can also be dispatched
36//! permissionlessly as unsigned transactions.
37//!
38//! Validating [`Pallet::dispatch_whitelisted_call`] decodes the whole preimage on every
39//! submission; prefer [`Pallet::dispatch_whitelisted_call_with_preimage`] for large calls.
40//!
41//! ## Deferred dispatch
42//!
43//! A dispatch by [`Config::DispatchWhitelistedOrigin`] of a not-yet-whitelisted hash is deferred
44//! into [`DeferredDispatch`] with an expiration. Once the hash is whitelisted, any signed account
45//! can relay it as root, fee-free.
46//!
47//! [`WhitelistedCall`] and [`DeferredDispatch`] are independent: each is written only by its own
48//! origin and neither removes the other. Unwhitelisting therefore *pauses* a live deferral rather
49//! than cancelling it. Relayers are blocked while the hash is off the whitelist, but
50//! re-whitelisting it before expiry re-enables relayed execution without a new governance act.
51
52#![cfg_attr(not(feature = "std"), no_std)]
53
54#[cfg(feature = "runtime-benchmarks")]
55mod benchmarking;
56#[cfg(test)]
57mod mock;
58#[cfg(test)]
59mod tests;
60pub mod weights;
61pub use weights::WeightInfo;
62
63extern crate alloc;
64
65use alloc::{boxed::Box, vec};
66use codec::{DecodeLimit, Encode, FullCodec};
67use frame::{
68	deps::sp_runtime::transaction_validity::{
69		TransactionSource, TransactionValidityError, TransactionValidityWithRefund,
70	},
71	prelude::*,
72	traits::{QueryPreimage, StorePreimage},
73};
74use scale_info::TypeInfo;
75
76pub use pallet::*;
77
78/// The Block number that we use to measure time.
79///
80/// Deferral expirations are tracked against this provider rather than the local system block,
81/// so on a parachain it can be the relay chain block number. All `DeferredDispatch` `expire_at`
82/// values and the [`Config::DeferredDispatchExpiration`] window are denominated in it.
83pub type ProvidedBlockNumberFor<T> =
84	<<T as Config>::BlockNumberProvider as BlockNumberProvider>::BlockNumber;
85
86#[frame::pallet]
87pub mod pallet {
88	use super::*;
89
90	#[pallet::config]
91	pub trait Config: frame_system::Config {
92		/// The overarching event type.
93		#[allow(deprecated)]
94		type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
95
96		/// The overarching call type.
97		type RuntimeCall: IsType<<Self as frame_system::Config>::RuntimeCall>
98			+ Dispatchable<RuntimeOrigin = Self::RuntimeOrigin, PostInfo = PostDispatchInfo>
99			+ GetDispatchInfo
100			+ FullCodec
101			+ TypeInfo
102			+ From<frame_system::Call<Self>>
103			+ Parameter;
104
105		/// Required origin for whitelisting a call.
106		type WhitelistOrigin: EnsureOrigin<Self::RuntimeOrigin>;
107
108		/// Required origin for dispatching whitelisted call with root origin.
109		///
110		/// If this origin accepts `Authorized` (e.g. via [`frame_system::EnsureAuthorized`]),
111		/// whitelisted calls can additionally be dispatched permissionlessly as unsigned
112		/// transactions.
113		type DispatchWhitelistedOrigin: EnsureOrigin<Self::RuntimeOrigin>;
114
115		/// The handler of pre-images.
116		type Preimages: QueryPreimage<H = Self::Hashing> + StorePreimage;
117
118		/// The number of provided blocks after which a deferred dispatch expires.
119		type DeferredDispatchExpiration: Get<ProvidedBlockNumberFor<Self>>;
120
121		/// Provider for the block number.
122		type BlockNumberProvider: BlockNumberProvider;
123
124		/// The weight information for this pallet.
125		type WeightInfo: WeightInfo;
126	}
127
128	#[pallet::pallet]
129	pub struct Pallet<T>(_);
130
131	#[pallet::event]
132	#[pallet::generate_deposit(pub(super) fn deposit_event)]
133	pub enum Event<T: Config> {
134		CallWhitelisted {
135			call_hash: T::Hash,
136		},
137		WhitelistedCallRemoved {
138			call_hash: T::Hash,
139		},
140		WhitelistedCallDispatched {
141			call_hash: T::Hash,
142			result: DispatchResultWithPostInfo,
143		},
144		/// A call dispatch has been deferred to a future provided block.
145		DispatchDeferred {
146			call_hash: T::Hash,
147		},
148		/// A deferred dispatch entry has been removed after expiration.
149		DeferredDispatchRemoved {
150			call_hash: T::Hash,
151		},
152		/// A relayer (signed origin) executed a deferred dispatch.
153		///
154		/// Emitted whenever the deferred entry is consumed by a relayer, regardless of whether the
155		/// inner call itself succeeded; the inner call's outcome is reported separately by
156		/// [`Event::WhitelistedCallDispatched`].
157		DeferredDispatchExecuted {
158			call_hash: T::Hash,
159			who: T::AccountId,
160		},
161	}
162
163	#[pallet::error]
164	pub enum Error<T> {
165		/// The preimage of the call hash could not be loaded.
166		UnavailablePreImage,
167		/// The call could not be decoded.
168		UndecodableCall,
169		/// The weight of the decoded call was higher than the witness.
170		InvalidCallWeightWitness,
171		/// The call was not whitelisted.
172		CallIsNotWhitelisted,
173		/// The call was already whitelisted; No-Op.
174		CallAlreadyWhitelisted,
175		/// No deferred dispatch entry exists for this call hash.
176		DeferredDispatchNotFound,
177		/// The deferred dispatch entry has not yet expired.
178		DeferredDispatchNotExpired,
179		/// The dispatch has already been deferred.
180		AlreadyDeferred,
181		/// The deferred dispatch has expired.
182		DeferredDispatchExpired,
183	}
184
185	#[pallet::storage]
186	pub type WhitelistedCall<T: Config> = StorageMap<_, Twox64Concat, T::Hash, (), OptionQuery>;
187
188	/// Deferred dispatches, mapping a call hash to the provided block number at which the deferral
189	/// expires and the entry can be permissionlessly removed.
190	#[pallet::storage]
191	pub type DeferredDispatch<T: Config> =
192		StorageMap<_, Twox64Concat, T::Hash, ProvidedBlockNumberFor<T>, OptionQuery>;
193
194	#[pallet::call]
195	impl<T: Config> Pallet<T> {
196		#[pallet::call_index(0)]
197		#[pallet::weight(T::WeightInfo::whitelist_call())]
198		pub fn whitelist_call(origin: OriginFor<T>, call_hash: T::Hash) -> DispatchResult {
199			T::WhitelistOrigin::ensure_origin(origin)?;
200
201			ensure!(
202				!WhitelistedCall::<T>::contains_key(call_hash),
203				Error::<T>::CallAlreadyWhitelisted,
204			);
205
206			WhitelistedCall::<T>::insert(call_hash, ());
207			T::Preimages::request(&call_hash);
208
209			Self::deposit_event(Event::<T>::CallWhitelisted { call_hash });
210			Ok(())
211		}
212
213		/// Remove a call hash from the whitelist.
214		///
215		/// Leaves any live [`DeferredDispatch`] entry in place, so re-whitelisting the hash before
216		/// it expires re-enables relayed execution.
217		#[pallet::call_index(1)]
218		#[pallet::weight(T::WeightInfo::remove_whitelisted_call())]
219		pub fn remove_whitelisted_call(origin: OriginFor<T>, call_hash: T::Hash) -> DispatchResult {
220			T::WhitelistOrigin::ensure_origin(origin)?;
221
222			WhitelistedCall::<T>::take(call_hash).ok_or(Error::<T>::CallIsNotWhitelisted)?;
223
224			T::Preimages::unrequest(&call_hash);
225
226			Self::deposit_event(Event::<T>::WhitelistedCallRemoved { call_hash });
227
228			Ok(())
229		}
230
231		#[pallet::authorize(Self::authorize_dispatch_whitelisted_call)]
232		#[pallet::weight_of_authorize(
233			T::WeightInfo::authorize_dispatch_whitelisted_call(*call_encoded_len)
234		)]
235		#[pallet::call_index(2)]
236		#[pallet::weight(
237			T::WeightInfo::dispatch_whitelisted_call(*call_encoded_len)
238				.saturating_add(*call_weight_witness)
239		)]
240		pub fn dispatch_whitelisted_call(
241			origin: OriginFor<T>,
242			call_hash: T::Hash,
243			call_encoded_len: u32,
244			call_weight_witness: Weight,
245		) -> DispatchResultWithPostInfo {
246			let relayer = match T::DispatchWhitelistedOrigin::try_origin(origin) {
247				Ok(_) if WhitelistedCall::<T>::contains_key(call_hash) => None,
248				Ok(_) => {
249					Self::defer_dispatch(call_hash)?;
250					return Ok(Some(T::WeightInfo::defer_dispatch(0)).into());
251				},
252				Err(dispatch_origin) => {
253					Some(Self::ensure_signed_deferred_dispatch(dispatch_origin, call_hash)?)
254				},
255			};
256
257			let call_data = T::Preimages::fetch(&call_hash, Some(call_encoded_len))
258				.map_err(|_| Error::<T>::UnavailablePreImage)?;
259
260			let call = <T as Config>::RuntimeCall::decode_all_with_depth_limit(
261				frame::deps::frame_support::MAX_EXTRINSIC_DEPTH,
262				&mut &call_data[..],
263			)
264			.map_err(|_| Error::<T>::UndecodableCall)?;
265
266			ensure!(
267				call.get_dispatch_info().call_weight.all_lte(call_weight_witness),
268				Error::<T>::InvalidCallWeightWitness
269			);
270
271			// Relayer isn't charged; the privileged direct path still pays.
272			let pays_fee = if relayer.is_some() { Pays::No } else { Pays::Yes };
273
274			let call_actual_weight = Self::clean_and_dispatch(call_hash, call);
275			if let Some(who) = relayer {
276				Self::deposit_event(Event::<T>::DeferredDispatchExecuted { call_hash, who });
277			}
278
279			let actual_weight = call_actual_weight.map(|w| {
280				w.saturating_add(T::WeightInfo::dispatch_whitelisted_call(call_encoded_len))
281			});
282			Ok(PostDispatchInfo { actual_weight, pays_fee })
283		}
284
285		#[pallet::authorize(Self::authorize_dispatch_whitelisted_call_with_preimage)]
286		#[pallet::weight_of_authorize(
287			T::WeightInfo::authorize_dispatch_whitelisted_call_with_preimage(
288				call.encoded_size() as u32
289			)
290		)]
291		#[pallet::call_index(3)]
292		#[pallet::weight({
293			let call_weight = call.get_dispatch_info().call_weight;
294			let call_len = call.encoded_size() as u32;
295			T::WeightInfo::dispatch_whitelisted_call_with_preimage(call_len)
296				.saturating_add(call_weight)
297		})]
298		pub fn dispatch_whitelisted_call_with_preimage(
299			origin: OriginFor<T>,
300			call: Box<<T as Config>::RuntimeCall>,
301		) -> DispatchResultWithPostInfo {
302			let call_hash = T::Hashing::hash_of(&call).into();
303			let call_len = call.encoded_size() as u32;
304
305			let relayer = match T::DispatchWhitelistedOrigin::try_origin(origin) {
306				Ok(_) if WhitelistedCall::<T>::contains_key(call_hash) => None,
307				Ok(_) => {
308					Self::defer_dispatch(call_hash)?;
309					return Ok(Some(T::WeightInfo::defer_dispatch(call_len)).into());
310				},
311				Err(dispatch_origin) => {
312					Some(Self::ensure_signed_deferred_dispatch(dispatch_origin, call_hash)?)
313				},
314			};
315
316			// Relayer isn't charged; the privileged direct path still pays.
317			let pays_fee = if relayer.is_some() { Pays::No } else { Pays::Yes };
318
319			let call_actual_weight = Self::clean_and_dispatch(call_hash, *call);
320			if let Some(who) = relayer {
321				Self::deposit_event(Event::<T>::DeferredDispatchExecuted { call_hash, who });
322			}
323
324			let actual_weight = call_actual_weight.map(|w| {
325				w.saturating_add(T::WeightInfo::dispatch_whitelisted_call_with_preimage(call_len))
326			});
327			Ok(PostDispatchInfo { actual_weight, pays_fee })
328		}
329
330		#[pallet::call_index(4)]
331		#[pallet::weight(T::WeightInfo::remove_deferred_dispatch())]
332		pub fn remove_deferred_dispatch(
333			origin: OriginFor<T>,
334			call_hash: T::Hash,
335		) -> DispatchResultWithPostInfo {
336			ensure_signed(origin)?;
337
338			let expire_at = DeferredDispatch::<T>::get(call_hash)
339				.ok_or(Error::<T>::DeferredDispatchNotFound)?;
340
341			let now = T::BlockNumberProvider::current_block_number();
342
343			ensure!(now >= expire_at, Error::<T>::DeferredDispatchNotExpired);
344
345			DeferredDispatch::<T>::remove(call_hash);
346
347			Self::deposit_event(Event::<T>::DeferredDispatchRemoved { call_hash });
348
349			Ok(Pays::No.into())
350		}
351	}
352}
353
354impl<T: Config> Pallet<T> {
355	/// Passes when [`Config::DispatchWhitelistedOrigin`] accepts the `Authorized` system origin.
356	/// Runs before any hashing or preimage read, so runtimes that don't opt in reject for free.
357	fn ensure_authorized_origin() -> Result<(), TransactionValidityError> {
358		let authorized: T::RuntimeOrigin =
359			frame_system::RawOrigin::<T::AccountId>::Authorized.into();
360		T::DispatchWhitelistedOrigin::try_origin(authorized)
361			.map_err(|_| TransactionValidityError::Invalid(InvalidTransaction::Call))?;
362
363		Ok(())
364	}
365
366	/// Passes when `call_hash` is present in [`WhitelistedCall`].
367	fn ensure_whitelisted(call_hash: T::Hash) -> Result<(), TransactionValidityError> {
368		if !WhitelistedCall::<T>::contains_key(call_hash) {
369			return Err(TransactionValidityError::Invalid(InvalidTransaction::Call));
370		}
371
372		Ok(())
373	}
374
375	/// Origin and whitelist gate for the permissionless witness dispatch.
376	///
377	/// Runs before any preimage read, so an unwhitelisted submission costs one map read.
378	fn ensure_authorized_whitelisted_dispatch(
379		call_hash: T::Hash,
380	) -> Result<(), TransactionValidityError> {
381		Self::ensure_authorized_origin()?;
382
383		Self::ensure_whitelisted(call_hash)
384	}
385
386	/// Pool validity for an authorized dispatch of `call_hash`.
387	///
388	/// Tagged by `(call_hash, call_encoded_len, call_weight)`, all three read off the call
389	/// itself, so every submission dispatching it shares one slot.
390	fn whitelisted_dispatch_validity(
391		call_hash: T::Hash,
392		call_encoded_len: u32,
393		call_weight: Weight,
394	) -> TransactionValidityWithRefund {
395		Ok((
396			ValidTransaction {
397				provides: vec![(call_hash, call_encoded_len, call_weight).encode()],
398				..Default::default()
399			},
400			Weight::zero(),
401		))
402	}
403
404	/// [`pallet::authorize`] callback for [`Pallet::dispatch_whitelisted_call`].
405	fn authorize_dispatch_whitelisted_call(
406		_source: TransactionSource,
407		call_hash: &T::Hash,
408		call_encoded_len: &u32,
409		call_weight_witness: &Weight,
410	) -> TransactionValidityWithRefund {
411		Self::ensure_authorized_whitelisted_dispatch(*call_hash)?;
412
413		// Every way this call can fail at dispatch fails with nobody charged, so both witnesses
414		// are checked against the preimage here. `fetch` covers the length witness.
415		let call_data = T::Preimages::fetch(call_hash, Some(*call_encoded_len))
416			.map_err(|_| TransactionValidityError::Invalid(InvalidTransaction::Call))?;
417
418		let call = <T as Config>::RuntimeCall::decode_all_with_depth_limit(
419			frame::deps::frame_support::MAX_EXTRINSIC_DEPTH,
420			&mut &call_data[..],
421		)
422		.map_err(|_| TransactionValidityError::Invalid(InvalidTransaction::Call))?;
423
424		let call_weight = call.get_dispatch_info().call_weight;
425		if call_weight != *call_weight_witness {
426			return Err(TransactionValidityError::Invalid(InvalidTransaction::Call));
427		}
428
429		Self::whitelisted_dispatch_validity(*call_hash, *call_encoded_len, call_weight)
430	}
431
432	/// [`pallet::authorize`] callback for [`Pallet::dispatch_whitelisted_call_with_preimage`].
433	fn authorize_dispatch_whitelisted_call_with_preimage(
434		_source: TransactionSource,
435		call: &Box<<T as Config>::RuntimeCall>,
436	) -> TransactionValidityWithRefund {
437		// Probe before hashing: non-opted-in runtimes reject without the wasted hash.
438		Self::ensure_authorized_origin()?;
439
440		let call_hash = T::Hashing::hash_of(call).into();
441		Self::ensure_whitelisted(call_hash)?;
442
443		// The call travels with the transaction, so there is no witness to check.
444		Self::whitelisted_dispatch_validity(
445			call_hash,
446			call.encoded_size() as u32,
447			call.get_dispatch_info().call_weight,
448		)
449	}
450
451	/// Defer the dispatch of a whitelisted call to a future block.
452	///
453	/// This function stores the call hash for later execution by any signed origin
454	/// before the expiration block.
455	fn defer_dispatch(call_hash: T::Hash) -> DispatchResult {
456		let now = T::BlockNumberProvider::current_block_number();
457
458		let expire_at = now.saturating_add(T::DeferredDispatchExpiration::get());
459
460		ensure!(!DeferredDispatch::<T>::contains_key(call_hash), Error::<T>::AlreadyDeferred);
461
462		DeferredDispatch::<T>::insert(call_hash, expire_at);
463
464		Self::deposit_event(Event::<T>::DispatchDeferred { call_hash });
465
466		Ok(())
467	}
468
469	/// Deferred dispatch sanity check.
470	///
471	/// Validates that:
472	/// - The origin is a signed account.
473	/// - A deferred dispatch entry exists for the call hash.
474	/// - The deferred dispatch has not yet expired.
475	/// - The call is still whitelisted.
476	///
477	/// The whitelist is always re-checked so that revoking the whitelist (via
478	/// [`Pallet::remove_whitelisted_call`]) prevents a relayer from executing a still-deferred
479	/// call.
480	///
481	/// Returns the signed account ID if all checks pass.
482	fn ensure_signed_deferred_dispatch(
483		origin: T::RuntimeOrigin,
484		call_hash: T::Hash,
485	) -> Result<T::AccountId, DispatchError> {
486		let who = ensure_signed(origin)?;
487
488		let expire_at =
489			DeferredDispatch::<T>::get(call_hash).ok_or(Error::<T>::DeferredDispatchNotFound)?;
490
491		ensure!(
492			T::BlockNumberProvider::current_block_number() < expire_at,
493			Error::<T>::DeferredDispatchExpired
494		);
495
496		ensure!(WhitelistedCall::<T>::contains_key(call_hash), Error::<T>::CallIsNotWhitelisted);
497
498		Ok(who)
499	}
500
501	/// Clean whitelisting/preimage and dispatch call.
502	///
503	/// Returns the inner call's actual weight.
504	fn clean_and_dispatch(call_hash: T::Hash, call: <T as Config>::RuntimeCall) -> Option<Weight> {
505		WhitelistedCall::<T>::remove(call_hash);
506		T::Preimages::unrequest(&call_hash);
507		DeferredDispatch::<T>::remove(call_hash);
508
509		let result = call.dispatch(frame_system::Origin::<T>::Root.into());
510
511		let call_actual_weight = match result {
512			Ok(call_post_info) => call_post_info.actual_weight,
513			Err(call_err) => call_err.post_info.actual_weight,
514		};
515		Self::deposit_event(Event::<T>::WhitelistedCallDispatched { call_hash, result });
516
517		call_actual_weight
518	}
519}