Skip to main content

pezpallet_preimage/
lib.rs

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