referrerpolicy=no-referrer-when-downgrade

pallet_preimage/
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//! # Preimage Pallet
19//!
20//! - [`Config`]
21//! - [`Call`]
22//!
23//! ## Overview
24//!
25//! The Preimage pallet allows for the users and the runtime to store the preimage
26//! of a hash on chain. This can be used by other pallets for storing and managing
27//! large byte-blobs.
28
29#![cfg_attr(not(feature = "std"), no_std)]
30
31#[cfg(feature = "runtime-benchmarks")]
32mod benchmarking;
33pub mod migration;
34#[cfg(test)]
35mod mock;
36#[cfg(test)]
37mod tests;
38pub mod weights;
39
40extern crate alloc;
41
42use alloc::{borrow::Cow, vec::Vec};
43use sp_runtime::{
44	traits::{BadOrigin, Hash, Saturating},
45	Perbill,
46};
47
48use codec::{Decode, Encode, MaxEncodedLen};
49use frame_support::{
50	dispatch::Pays,
51	ensure,
52	pallet_prelude::Get,
53	traits::{
54		Consideration, Currency, Defensive, FetchResult, Footprint, PreimageProvider,
55		PreimageRecipient, QueryPreimage, ReservableCurrency, StorePreimage,
56	},
57	BoundedSlice, BoundedVec,
58};
59use scale_info::TypeInfo;
60pub use weights::WeightInfo;
61
62use frame_support::pallet_prelude::*;
63use frame_system::pallet_prelude::*;
64
65pub use pallet::*;
66
67/// A type to note whether a preimage is owned by a user or the system.
68#[derive(
69	Clone,
70	Eq,
71	PartialEq,
72	Encode,
73	Decode,
74	TypeInfo,
75	MaxEncodedLen,
76	RuntimeDebug,
77	DecodeWithMemTracking,
78)]
79pub enum OldRequestStatus<AccountId, Balance> {
80	/// The associated preimage has not yet been requested by the system. The given deposit (if
81	/// some) is being held until either it becomes requested or the user retracts the preimage.
82	Unrequested { deposit: (AccountId, Balance), len: u32 },
83	/// There are a non-zero number of outstanding requests for this hash by this chain. If there
84	/// is a preimage registered, then `len` is `Some` and it may be removed iff this counter
85	/// becomes zero.
86	Requested { deposit: Option<(AccountId, Balance)>, count: u32, len: Option<u32> },
87}
88
89/// A type to note whether a preimage is owned by a user or the system.
90#[derive(
91	Clone,
92	Eq,
93	PartialEq,
94	Encode,
95	Decode,
96	TypeInfo,
97	MaxEncodedLen,
98	RuntimeDebug,
99	DecodeWithMemTracking,
100)]
101pub enum RequestStatus<AccountId, Ticket> {
102	/// The associated preimage has not yet been requested by the system. The given deposit (if
103	/// some) is being held until either it becomes requested or the user retracts the preimage.
104	Unrequested { ticket: (AccountId, Ticket), len: u32 },
105	/// There are a non-zero number of outstanding requests for this hash by this chain. If there
106	/// is a preimage registered, then `len` is `Some` and it may be removed iff this counter
107	/// becomes zero.
108	Requested { maybe_ticket: Option<(AccountId, Ticket)>, count: u32, maybe_len: Option<u32> },
109}
110
111pub type BalanceOf<T> =
112	<<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
113pub type TicketOf<T> = <T as Config>::Consideration;
114
115/// Maximum size of preimage we can store is 4mb.
116pub const MAX_SIZE: u32 = 4 * 1024 * 1024;
117/// Hard-limit on the number of hashes that can be passed to `ensure_updated`.
118///
119/// Exists only for benchmarking purposes.
120pub const MAX_HASH_UPGRADE_BULK_COUNT: u32 = 1024;
121
122#[frame_support::pallet]
123#[allow(deprecated)]
124pub mod pallet {
125	use super::*;
126
127	/// The in-code storage version.
128	const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
129
130	#[pallet::config]
131	pub trait Config: frame_system::Config {
132		/// The overarching event type.
133		#[allow(deprecated)]
134		type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
135
136		/// The Weight information for this pallet.
137		type WeightInfo: weights::WeightInfo;
138
139		/// Currency type for this pallet.
140		// TODO#1569: Remove.
141		type Currency: ReservableCurrency<Self::AccountId>;
142
143		/// An origin that can request a preimage be placed on-chain without a deposit or fee, or
144		/// manage existing preimages.
145		type ManagerOrigin: EnsureOrigin<Self::RuntimeOrigin>;
146
147		/// A means of providing some cost while data is stored on-chain.
148		type Consideration: Consideration<Self::AccountId, Footprint>;
149	}
150
151	#[pallet::pallet]
152	#[pallet::storage_version(STORAGE_VERSION)]
153	pub struct Pallet<T>(_);
154
155	#[pallet::event]
156	#[pallet::generate_deposit(pub fn deposit_event)]
157	pub enum Event<T: Config> {
158		/// A preimage has been noted.
159		Noted { hash: T::Hash },
160		/// A preimage has been requested.
161		Requested { hash: T::Hash },
162		/// A preimage has ben cleared.
163		Cleared { hash: T::Hash },
164	}
165
166	#[pallet::error]
167	pub enum Error<T> {
168		/// Preimage is too large to store on-chain.
169		TooBig,
170		/// Preimage has already been noted on-chain.
171		AlreadyNoted,
172		/// The user is not authorized to perform this action.
173		NotAuthorized,
174		/// The preimage cannot be removed since it has not yet been noted.
175		NotNoted,
176		/// A preimage may not be removed when there are outstanding requests.
177		Requested,
178		/// The preimage request cannot be removed since no outstanding requests exist.
179		NotRequested,
180		/// More than `MAX_HASH_UPGRADE_BULK_COUNT` hashes were requested to be upgraded at once.
181		TooMany,
182		/// Too few hashes were requested to be upgraded (i.e. zero).
183		TooFew,
184	}
185
186	/// A reason for this pallet placing a hold on funds.
187	#[pallet::composite_enum]
188	pub enum HoldReason {
189		/// The funds are held as storage deposit for a preimage.
190		Preimage,
191	}
192
193	/// The request status of a given hash.
194	#[deprecated = "RequestStatusFor"]
195	#[pallet::storage]
196	pub type StatusFor<T: Config> =
197		StorageMap<_, Identity, T::Hash, OldRequestStatus<T::AccountId, BalanceOf<T>>>;
198
199	/// The request status of a given hash.
200	#[pallet::storage]
201	pub type RequestStatusFor<T: Config> =
202		StorageMap<_, Identity, T::Hash, RequestStatus<T::AccountId, TicketOf<T>>>;
203
204	#[pallet::storage]
205	pub type PreimageFor<T: Config> =
206		StorageMap<_, Identity, (T::Hash, u32), BoundedVec<u8, ConstU32<MAX_SIZE>>>;
207
208	#[pallet::call(weight = T::WeightInfo)]
209	impl<T: Config> Pallet<T> {
210		/// Register a preimage on-chain.
211		///
212		/// If the preimage was previously requested, no fees or deposits are taken for providing
213		/// the preimage. Otherwise, a deposit is taken proportional to the size of the preimage.
214		#[pallet::call_index(0)]
215		#[pallet::weight(T::WeightInfo::note_preimage(bytes.len() as u32))]
216		pub fn note_preimage(origin: OriginFor<T>, bytes: Vec<u8>) -> DispatchResultWithPostInfo {
217			// We accept a signed origin which will pay a deposit, or a root origin where a deposit
218			// is not taken.
219			let maybe_sender = Self::ensure_signed_or_manager(origin)?;
220			let (system_requested, _) = Self::note_bytes(bytes.into(), maybe_sender.as_ref())?;
221			if system_requested || maybe_sender.is_none() {
222				Ok(Pays::No.into())
223			} else {
224				Ok(().into())
225			}
226		}
227
228		/// Clear an unrequested preimage from the runtime storage.
229		///
230		/// If `len` is provided, then it will be a much cheaper operation.
231		///
232		/// - `hash`: The hash of the preimage to be removed from the store.
233		/// - `len`: The length of the preimage of `hash`.
234		#[pallet::call_index(1)]
235		pub fn unnote_preimage(origin: OriginFor<T>, hash: T::Hash) -> DispatchResult {
236			let maybe_sender = Self::ensure_signed_or_manager(origin)?;
237			Self::do_unnote_preimage(&hash, maybe_sender)
238		}
239
240		/// Request a preimage be uploaded to the chain without paying any fees or deposits.
241		///
242		/// If the preimage requests has already been provided on-chain, we unreserve any deposit
243		/// a user may have paid, and take the control of the preimage out of their hands.
244		#[pallet::call_index(2)]
245		pub fn request_preimage(origin: OriginFor<T>, hash: T::Hash) -> DispatchResult {
246			T::ManagerOrigin::ensure_origin(origin)?;
247			Self::do_request_preimage(&hash);
248			Ok(())
249		}
250
251		/// Clear a previously made request for a preimage.
252		///
253		/// NOTE: THIS MUST NOT BE CALLED ON `hash` MORE TIMES THAN `request_preimage`.
254		#[pallet::call_index(3)]
255		pub fn unrequest_preimage(origin: OriginFor<T>, hash: T::Hash) -> DispatchResult {
256			T::ManagerOrigin::ensure_origin(origin)?;
257			Self::do_unrequest_preimage(&hash)
258		}
259
260		/// Ensure that the bulk of pre-images is upgraded.
261		///
262		/// The caller pays no fee if at least 90% of pre-images were successfully updated.
263		#[pallet::call_index(4)]
264		#[pallet::weight(T::WeightInfo::ensure_updated(hashes.len() as u32))]
265		pub fn ensure_updated(
266			origin: OriginFor<T>,
267			hashes: Vec<T::Hash>,
268		) -> DispatchResultWithPostInfo {
269			ensure_signed(origin)?;
270			ensure!(hashes.len() > 0, Error::<T>::TooFew);
271			ensure!(hashes.len() <= MAX_HASH_UPGRADE_BULK_COUNT as usize, Error::<T>::TooMany);
272
273			let updated = hashes.iter().map(Self::do_ensure_updated).filter(|b| *b).count() as u32;
274			let ratio = Perbill::from_rational(updated, hashes.len() as u32);
275
276			let pays: Pays = (ratio < Perbill::from_percent(90)).into();
277			Ok(pays.into())
278		}
279	}
280}
281
282impl<T: Config> Pallet<T> {
283	fn do_ensure_updated(h: &T::Hash) -> bool {
284		#[allow(deprecated)]
285		let r = match StatusFor::<T>::take(h) {
286			Some(r) => r,
287			None => return false,
288		};
289		let n = match r {
290			OldRequestStatus::Unrequested { deposit: (who, amount), len } => {
291				// unreserve deposit
292				T::Currency::unreserve(&who, amount);
293				// take consideration
294				let Ok(ticket) =
295					T::Consideration::new(&who, Footprint::from_parts(1, len as usize))
296						.defensive_proof("Unexpected inability to take deposit after unreserved")
297				else {
298					return true
299				};
300				RequestStatus::Unrequested { ticket: (who, ticket), len }
301			},
302			OldRequestStatus::Requested { deposit: maybe_deposit, count, len: maybe_len } => {
303				let maybe_ticket = if let Some((who, deposit)) = maybe_deposit {
304					// unreserve deposit
305					T::Currency::unreserve(&who, deposit);
306					// take consideration
307					if let Some(len) = maybe_len {
308						let Ok(ticket) =
309							T::Consideration::new(&who, Footprint::from_parts(1, len as usize))
310								.defensive_proof(
311									"Unexpected inability to take deposit after unreserved",
312								)
313						else {
314							return true
315						};
316						Some((who, ticket))
317					} else {
318						None
319					}
320				} else {
321					None
322				};
323				RequestStatus::Requested { maybe_ticket, count, maybe_len }
324			},
325		};
326		RequestStatusFor::<T>::insert(h, n);
327		true
328	}
329
330	/// Ensure that the origin is either the `ManagerOrigin` or a signed origin.
331	fn ensure_signed_or_manager(
332		origin: T::RuntimeOrigin,
333	) -> Result<Option<T::AccountId>, BadOrigin> {
334		if T::ManagerOrigin::ensure_origin(origin.clone()).is_ok() {
335			return Ok(None)
336		}
337		let who = ensure_signed(origin)?;
338		Ok(Some(who))
339	}
340
341	/// Store some preimage on chain.
342	///
343	/// If `maybe_depositor` is `None` then it is also requested. If `Some`, then it is not.
344	///
345	/// We verify that the preimage is within the bounds of what the pallet supports.
346	///
347	/// If the preimage was requested to be uploaded, then the user pays no deposits or tx fees.
348	fn note_bytes(
349		preimage: Cow<[u8]>,
350		maybe_depositor: Option<&T::AccountId>,
351	) -> Result<(bool, T::Hash), DispatchError> {
352		let hash = T::Hashing::hash(&preimage);
353		let len = preimage.len() as u32;
354		ensure!(len <= MAX_SIZE, Error::<T>::TooBig);
355
356		Self::do_ensure_updated(&hash);
357		// We take a deposit only if there is a provided depositor and the preimage was not
358		// previously requested. This also allows the tx to pay no fee.
359		let status = match (RequestStatusFor::<T>::get(hash), maybe_depositor) {
360			(Some(RequestStatus::Requested { maybe_ticket, count, .. }), _) =>
361				RequestStatus::Requested { maybe_ticket, count, maybe_len: Some(len) },
362			(Some(RequestStatus::Unrequested { .. }), Some(_)) =>
363				return Err(Error::<T>::AlreadyNoted.into()),
364			(Some(RequestStatus::Unrequested { ticket, len }), None) => RequestStatus::Requested {
365				maybe_ticket: Some(ticket),
366				count: 1,
367				maybe_len: Some(len),
368			},
369			(None, None) =>
370				RequestStatus::Requested { maybe_ticket: None, count: 1, maybe_len: Some(len) },
371			(None, Some(depositor)) => {
372				let ticket =
373					T::Consideration::new(depositor, Footprint::from_parts(1, len as usize))?;
374				RequestStatus::Unrequested { ticket: (depositor.clone(), ticket), len }
375			},
376		};
377		let was_requested = matches!(status, RequestStatus::Requested { .. });
378		RequestStatusFor::<T>::insert(hash, status);
379
380		let _ = Self::insert(&hash, preimage)
381			.defensive_proof("Unable to insert. Logic error in `note_bytes`?");
382
383		Self::deposit_event(Event::Noted { hash });
384
385		Ok((was_requested, hash))
386	}
387
388	// This function will add a hash to the list of requested preimages.
389	//
390	// If the preimage already exists before the request is made, the deposit for the preimage is
391	// returned to the user, and removed from their management.
392	fn do_request_preimage(hash: &T::Hash) {
393		Self::do_ensure_updated(&hash);
394		let (count, maybe_len, maybe_ticket) =
395			RequestStatusFor::<T>::get(hash).map_or((1, None, None), |x| match x {
396				RequestStatus::Requested { maybe_ticket, mut count, maybe_len } => {
397					count.saturating_inc();
398					(count, maybe_len, maybe_ticket)
399				},
400				RequestStatus::Unrequested { ticket, len } => (1, Some(len), Some(ticket)),
401			});
402		RequestStatusFor::<T>::insert(
403			hash,
404			RequestStatus::Requested { maybe_ticket, count, maybe_len },
405		);
406		if count == 1 {
407			Self::deposit_event(Event::Requested { hash: *hash });
408		}
409	}
410
411	// Clear a preimage from the storage of the chain, returning any deposit that may be reserved.
412	//
413	// If `len` is provided, it will be a much cheaper operation.
414	//
415	// If `maybe_owner` is provided, we verify that it is the correct owner before clearing the
416	// data.
417	fn do_unnote_preimage(
418		hash: &T::Hash,
419		maybe_check_owner: Option<T::AccountId>,
420	) -> DispatchResult {
421		Self::do_ensure_updated(&hash);
422		match RequestStatusFor::<T>::get(hash).ok_or(Error::<T>::NotNoted)? {
423			RequestStatus::Requested { maybe_ticket: Some((owner, ticket)), count, maybe_len } => {
424				ensure!(maybe_check_owner.map_or(true, |c| c == owner), Error::<T>::NotAuthorized);
425				let _ = ticket.drop(&owner);
426				RequestStatusFor::<T>::insert(
427					hash,
428					RequestStatus::Requested { maybe_ticket: None, count, maybe_len },
429				);
430				Ok(())
431			},
432			RequestStatus::Requested { maybe_ticket: None, .. } => {
433				ensure!(maybe_check_owner.is_none(), Error::<T>::NotAuthorized);
434				Self::do_unrequest_preimage(hash)
435			},
436			RequestStatus::Unrequested { ticket: (owner, ticket), len } => {
437				ensure!(maybe_check_owner.map_or(true, |c| c == owner), Error::<T>::NotAuthorized);
438				let _ = ticket.drop(&owner);
439				RequestStatusFor::<T>::remove(hash);
440
441				Self::remove(hash, len);
442				Self::deposit_event(Event::Cleared { hash: *hash });
443				Ok(())
444			},
445		}
446	}
447
448	/// Clear a preimage request.
449	fn do_unrequest_preimage(hash: &T::Hash) -> DispatchResult {
450		Self::do_ensure_updated(&hash);
451		match RequestStatusFor::<T>::get(hash).ok_or(Error::<T>::NotRequested)? {
452			RequestStatus::Requested { mut count, maybe_len, maybe_ticket } if count > 1 => {
453				count.saturating_dec();
454				RequestStatusFor::<T>::insert(
455					hash,
456					RequestStatus::Requested { maybe_ticket, count, maybe_len },
457				);
458			},
459			RequestStatus::Requested { count, maybe_len, maybe_ticket } => {
460				debug_assert!(count == 1, "preimage request counter at zero?");
461				match (maybe_len, maybe_ticket) {
462					// Preimage was never noted.
463					(None, _) => RequestStatusFor::<T>::remove(hash),
464					// Preimage was noted without owner - just remove it.
465					(Some(len), None) => {
466						Self::remove(hash, len);
467						RequestStatusFor::<T>::remove(hash);
468						Self::deposit_event(Event::Cleared { hash: *hash });
469					},
470					// Preimage was noted with owner - move to unrequested so they can get refund.
471					(Some(len), Some(ticket)) => {
472						RequestStatusFor::<T>::insert(
473							hash,
474							RequestStatus::Unrequested { ticket, len },
475						);
476					},
477				}
478			},
479			RequestStatus::Unrequested { .. } => return Err(Error::<T>::NotRequested.into()),
480		}
481		Ok(())
482	}
483
484	fn insert(hash: &T::Hash, preimage: Cow<[u8]>) -> Result<(), ()> {
485		BoundedSlice::<u8, ConstU32<MAX_SIZE>>::try_from(preimage.as_ref())
486			.map_err(|_| ())
487			.map(|s| PreimageFor::<T>::insert((hash, s.len() as u32), s))
488	}
489
490	fn remove(hash: &T::Hash, len: u32) {
491		PreimageFor::<T>::remove((hash, len))
492	}
493
494	fn have(hash: &T::Hash) -> bool {
495		Self::len(hash).is_some()
496	}
497
498	fn len(hash: &T::Hash) -> Option<u32> {
499		use RequestStatus::*;
500		Self::do_ensure_updated(&hash);
501		match RequestStatusFor::<T>::get(hash) {
502			Some(Requested { maybe_len: Some(len), .. }) | Some(Unrequested { len, .. }) =>
503				Some(len),
504			_ => None,
505		}
506	}
507
508	fn fetch(hash: &T::Hash, len: Option<u32>) -> FetchResult {
509		let len = len.or_else(|| Self::len(hash)).ok_or(DispatchError::Unavailable)?;
510		PreimageFor::<T>::get((hash, len))
511			.map(|p| p.into_inner())
512			.map(Into::into)
513			.ok_or(DispatchError::Unavailable)
514	}
515}
516
517impl<T: Config> PreimageProvider<T::Hash> for Pallet<T> {
518	fn have_preimage(hash: &T::Hash) -> bool {
519		Self::have(hash)
520	}
521
522	fn preimage_requested(hash: &T::Hash) -> bool {
523		Self::do_ensure_updated(hash);
524		matches!(RequestStatusFor::<T>::get(hash), Some(RequestStatus::Requested { .. }))
525	}
526
527	fn get_preimage(hash: &T::Hash) -> Option<Vec<u8>> {
528		Self::fetch(hash, None).ok().map(Cow::into_owned)
529	}
530
531	fn request_preimage(hash: &T::Hash) {
532		Self::do_request_preimage(hash)
533	}
534
535	fn unrequest_preimage(hash: &T::Hash) {
536		let res = Self::do_unrequest_preimage(hash);
537		debug_assert!(res.is_ok(), "do_unrequest_preimage failed - counter underflow?");
538	}
539}
540
541impl<T: Config> PreimageRecipient<T::Hash> for Pallet<T> {
542	type MaxSize = ConstU32<MAX_SIZE>; // 2**22
543
544	fn note_preimage(bytes: BoundedVec<u8, Self::MaxSize>) {
545		// We don't really care if this fails, since that's only the case if someone else has
546		// already noted it.
547		let _ = Self::note_bytes(bytes.into_inner().into(), None);
548	}
549
550	fn unnote_preimage(hash: &T::Hash) {
551		// Should never fail if authorization check is skipped.
552		let res = Self::do_unrequest_preimage(hash);
553		debug_assert!(res.is_ok(), "unnote_preimage failed - request outstanding?");
554	}
555}
556
557impl<T: Config> QueryPreimage for Pallet<T> {
558	type H = T::Hashing;
559
560	fn len(hash: &T::Hash) -> Option<u32> {
561		Pallet::<T>::len(hash)
562	}
563
564	fn fetch(hash: &T::Hash, len: Option<u32>) -> FetchResult {
565		Pallet::<T>::fetch(hash, len)
566	}
567
568	fn is_requested(hash: &T::Hash) -> bool {
569		Self::do_ensure_updated(&hash);
570		matches!(RequestStatusFor::<T>::get(hash), Some(RequestStatus::Requested { .. }))
571	}
572
573	fn request(hash: &T::Hash) {
574		Self::do_request_preimage(hash)
575	}
576
577	fn unrequest(hash: &T::Hash) {
578		let res = Self::do_unrequest_preimage(hash);
579		debug_assert!(res.is_ok(), "do_unrequest_preimage failed - counter underflow?");
580	}
581}
582
583impl<T: Config> StorePreimage for Pallet<T> {
584	const MAX_LENGTH: usize = MAX_SIZE as usize;
585
586	fn note(bytes: Cow<[u8]>) -> Result<T::Hash, DispatchError> {
587		// We don't really care if this fails, since that's only the case if someone else has
588		// already noted it.
589		let maybe_hash = Self::note_bytes(bytes, None).map(|(_, h)| h);
590		// Map to the correct trait error.
591		if maybe_hash == Err(DispatchError::from(Error::<T>::TooBig)) {
592			Err(DispatchError::Exhausted)
593		} else {
594			maybe_hash
595		}
596	}
597
598	fn unnote(hash: &T::Hash) {
599		// Should never fail if authorization check is skipped.
600		let res = Self::do_unnote_preimage(hash, None);
601		debug_assert!(res.is_ok(), "unnote_preimage failed - request outstanding?");
602	}
603}