Skip to main content

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