diff --git a/Cargo.lock b/Cargo.lock index 0091223b09e6a..6b0101c5aa5f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5886,6 +5886,20 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-preimage" +version = "4.0.0-dev" +dependencies = [ + "frame-support", + "frame-system", + "parity-scale-codec", + "scale-info", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", +] + [[package]] name = "pallet-proxy" version = "4.0.0-dev" diff --git a/Cargo.toml b/Cargo.toml index f30b223a9b205..bfa5ce7b1a27c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -101,6 +101,7 @@ members = [ "frame/nicks", "frame/node-authorization", "frame/offences", + "frame/preimage", "frame/proxy", "frame/randomness-collective-flip", "frame/recovery", diff --git a/frame/preimage/Cargo.toml b/frame/preimage/Cargo.toml new file mode 100644 index 0000000000000..35b99c6335b16 --- /dev/null +++ b/frame/preimage/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "pallet-preimage" +version = "4.0.0-dev" +authors = ["Parity Technologies "] +edition = "2018" +license = "Apache-2.0" +homepage = "https://substrate.io" +repository = "https://github.com/paritytech/substrate/" +description = "FRAME pallet for storing preimages of hashes" +readme = "README.md" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +codec = { package = "parity-scale-codec", version = "2.0.0", default-features = false, features = ["derive"] } +scale-info = { version = "1.0", default-features = false, features = ["derive"] } +sp-std = { version = "4.0.0-dev", default-features = false, path = "../../primitives/std" } +sp-io = { version = "4.0.0-dev", default-features = false, path = "../../primitives/io" } +sp-runtime = { version = "4.0.0-dev", default-features = false, path = "../../primitives/runtime" } +frame-support = { version = "4.0.0-dev", default-features = false, path = "../support" } +frame-system = { version = "4.0.0-dev", default-features = false, path = "../system" } + +[dev-dependencies] +sp-core = { version = "4.0.0-dev", path = "../../primitives/core" } + +[features] +default = ["std"] +std = [ + "codec/std", + "scale-info/std", + "sp-std/std", + "sp-io/std", + "sp-runtime/std", + "frame-support/std", + "frame-system/std", +] +try-runtime = ["frame-support/try-runtime"] diff --git a/frame/preimage/src/lib.rs b/frame/preimage/src/lib.rs new file mode 100644 index 0000000000000..aa263ee269972 --- /dev/null +++ b/frame/preimage/src/lib.rs @@ -0,0 +1,331 @@ +// This file is part of Substrate. + +// Copyright (C) 2017-2021 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! # Preimage Pallet +//! +//! - [`Config`] +//! - [`Call`] +//! +//! ## Overview +//! +//! The Preimage pallet allows for the users and the runtime to store the preimage +//! of a hash on chain. This can be used by other pallets where storing and managing +//! large byte-blobs. + +#![cfg_attr(not(feature = "std"), no_std)] + +use sp_runtime::traits::{BadOrigin, Hash, Saturating}; +use sp_std::{convert::TryFrom, prelude::*}; + +use codec::{Decode, Encode, MaxEncodedLen}; +use frame_support::{ + ensure, + pallet_prelude::Get, + traits::{Currency, PreimageProvider, PreimageRecipient, ReservableCurrency}, + weights::Pays, + BoundedVec, +}; +use scale_info::TypeInfo; + +use frame_support::pallet_prelude::*; +use frame_system::pallet_prelude::*; + +pub use pallet::*; + +/// A type to note whether a preimage is owned by a user or the system. +#[derive(Clone, Encode, Decode, TypeInfo, MaxEncodedLen)] +pub enum RequestStatus { + /// The associated preimage has not yet been requested by the system. The given deposit (if + /// some) is being held until either it becomes requested or the user retracts the primage. + Unrequested(Option<(AccountId, Balance)>), + /// There are a non-zero number of outstanding requests for this hash by this chain. If there + /// is a preimage registered, then it may be removed iff this counter becomes zero. + Requested(u32), +} + +type BalanceOf = + <::Currency as Currency<::AccountId>>::Balance; + +#[frame_support::pallet] +pub mod pallet { + use super::*; + + #[pallet::config] + pub trait Config: frame_system::Config { + /// The overarching event type. + type Event: From> + IsType<::Event>; + + /// Currency type for this pallet. + type Currency: ReservableCurrency; + + /// An origin that can request a preimage be placed on-chain without a deposit or fee, or + /// manage existing preimages. + type ManagerOrigin: EnsureOrigin; + + /// Max size allowed for a preimage. + type MaxSize: Get; + + /// The base deposit for placing a preimage on chain. + type BaseDeposit: Get>; + + /// The per-byte deposit for placing a preimage on chain. + type ByteDeposit: Get>; + } + + #[pallet::pallet] + #[pallet::generate_store(pub(super) trait Store)] + #[pallet::generate_storage_info] + pub struct Pallet(PhantomData); + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// A preimage has been noted. + Noted { hash: T::Hash }, + /// A preimage has been requested. + Requested { hash: T::Hash }, + /// A preimage has ben cleared. + Cleared { hash: T::Hash }, + } + + #[pallet::error] + pub enum Error { + /// Preimage is too large to store on-chain. + TooLarge, + /// Preimage has already been noted on-chain. + AlreadyNoted, + /// The user is not authorized to perform this action. + NotAuthorized, + /// The preimage cannot be removed since it has not yet been noted. + NotNoted, + /// A preimage may not be removed when there are outstanding requests. + Requested, + /// The preimage request cannot be removed since no outstanding requests exist. + NotRequested, + } + + /// The request status of a given hash. + #[pallet::storage] + pub(super) type StatusFor = + StorageMap<_, Identity, T::Hash, RequestStatus>>; + + /// The preimages stored by this pallet. + #[pallet::storage] + pub(super) type PreimageFor = + StorageMap<_, Identity, T::Hash, BoundedVec>; + + #[pallet::call] + impl Pallet { + /// Register a preimage on-chain. + /// + /// If the preimage was previously requested, no fees or deposits are taken for providing + /// the preimage. Otherwise, a deposit is taken proportional to the size of the preimage. + #[pallet::weight(0)] + pub fn note_preimage(origin: OriginFor, bytes: Vec) -> DispatchResultWithPostInfo { + // We accept a signed origin which will pay a deposit, or a root origin where a deposit + // is not taken. + let maybe_sender = Self::ensure_signed_or_manager(origin)?; + let bounded_vec = + BoundedVec::::try_from(bytes).map_err(|()| Error::::TooLarge)?; + let system_requested = Self::note_bytes(bounded_vec, maybe_sender.as_ref())?; + if system_requested || maybe_sender.is_none() { + Ok(Pays::No.into()) + } else { + Ok(().into()) + } + } + + /// Clear an unrequested preimage from the runtime storage. + #[pallet::weight(0)] + pub fn unnote_preimage(origin: OriginFor, hash: T::Hash) -> DispatchResult { + let maybe_sender = Self::ensure_signed_or_manager(origin)?; + Self::unnote_preimage(hash, maybe_sender) + } + + /// Request a preimage be uploaded to the chain without paying any fees or deposits. + /// + /// If the preimage requests has already been provided on-chain, we unreserve any deposit + /// a user may have paid, and take the control of the preimage out of their hands. + #[pallet::weight(0)] + pub fn request_preimage(origin: OriginFor, hash: T::Hash) -> DispatchResult { + T::ManagerOrigin::ensure_origin(origin)?; + Self::do_request_preimage(hash); + Ok(()) + } + + /// Clear a previously made request for a preimage. + /// + /// NOTE: THIS MUST NOT BE CALLED ON `hash` MORE TIMES THAN `request_preimage`. + #[pallet::weight(0)] + pub fn clear_request(origin: OriginFor, hash: T::Hash) -> DispatchResult { + T::ManagerOrigin::ensure_origin(origin)?; + Self::do_clear_request(hash) + } + } +} + +impl Pallet { + /// Ensure that the origin is either the `ManagerOrigin` or a signed origin. + fn ensure_signed_or_manager(origin: T::Origin) -> Result, BadOrigin> { + if T::ManagerOrigin::ensure_origin(origin.clone()).is_ok() { + return Ok(None) + } + let who = ensure_signed(origin)?; + Ok(Some(who)) + } + + /// Store some preimage on chain. + /// + /// We verify that the preimage is within the bounds of what the pallet supports. + /// + /// If the preimage was requested to be uploaded, then the user pays no deposits or tx fees. + fn note_bytes( + preimage: BoundedVec, + maybe_depositor: Option<&T::AccountId>, + ) -> Result { + let hash = T::Hashing::hash(&preimage); + ensure!(!PreimageFor::::contains_key(hash), Error::::AlreadyNoted); + + // We take a deposit only if there is a provided depositor, and the preimage was not + // previously requested. This also allows the tx to pay no fee. + let was_requested = match (StatusFor::::get(hash), maybe_depositor) { + (Some(RequestStatus::Requested(_)), _) => true, + (Some(RequestStatus::Unrequested(..)), _) => Err(Error::::AlreadyNoted)?, + (_, None) => true, + (None, Some(depositor)) => { + let length = preimage.len() as u32; + let deposit = T::BaseDeposit::get() + .saturating_add(T::ByteDeposit::get().saturating_mul(length.into())); + T::Currency::reserve(depositor, deposit)?; + let status = RequestStatus::Unrequested(Some((depositor.clone(), deposit))); + StatusFor::::insert(hash, status); + false + }, + }; + + PreimageFor::::insert(hash, preimage); + Self::deposit_event(Event::Noted { hash }); + + Ok(was_requested) + } + + // This function will add a hash to the list of requested preimages. + // + // If the preimage already exists before the request is made, the deposit for the preimage is + // returned to the user, and removed from their management. + fn do_request_preimage(hash: T::Hash) { + let count = StatusFor::::get(hash).map_or(1, |x| match x { + RequestStatus::Requested(mut count) => { + count.saturating_inc(); + count + }, + RequestStatus::Unrequested(None) => 1, + RequestStatus::Unrequested(Some((owner, deposit))) => { + // Return the deposit - the preimage now has outstanding requests. + T::Currency::unreserve(&owner, deposit); + 1 + }, + }); + StatusFor::::insert(&hash, RequestStatus::Requested(count)); + if count == 1 { + Self::deposit_event(Event::Requested { hash }); + } + } + + // Clear a preimage from the storage of the chain, returning any deposit that may be reserved. + // + // If `maybe_owner` is provided, we verify that it is the correct owner before clearing the + // data. + // + // If `maybe_owner` is not provided, this function cannot return an error. + fn unnote_preimage(hash: T::Hash, maybe_check_owner: Option) -> DispatchResult { + match StatusFor::::get(&hash).ok_or(Error::::NotNoted)? { + RequestStatus::Unrequested(Some((owner, deposit))) => { + ensure!( + maybe_check_owner.map_or(true, |c| &c == &owner), + Error::::NotAuthorized + ); + T::Currency::unreserve(&owner, deposit); + }, + RequestStatus::Unrequested(None) => { + ensure!(maybe_check_owner.is_none(), Error::::NotAuthorized); + }, + RequestStatus::Requested(_) => Err(Error::::Requested)?, + } + StatusFor::::remove(&hash); + PreimageFor::::remove(&hash); + Self::deposit_event(Event::Cleared { hash }); + Ok(()) + } + + /// Clear a preimage request. + fn do_clear_request(hash: T::Hash) -> DispatchResult { + match StatusFor::::get(hash).ok_or(Error::::NotRequested)? { + RequestStatus::Requested(mut count) if count > 1 => { + count.saturating_dec(); + StatusFor::::insert(&hash, RequestStatus::Requested(count)); + }, + RequestStatus::Requested(count) => { + debug_assert!(count == 1, "preimage request counter at zero?"); + PreimageFor::::remove(&hash); + StatusFor::::remove(&hash); + Self::deposit_event(Event::Cleared { hash }); + }, + RequestStatus::Unrequested(_) => Err(Error::::NotRequested)?, + } + Ok(()) + } +} + +impl PreimageProvider for Pallet { + fn preimage_exists(hash: T::Hash) -> bool { + PreimageFor::::contains_key(hash) + } + + fn preimage_requested(hash: T::Hash) -> bool { + matches!(StatusFor::::get(hash), Some(RequestStatus::Requested(..))) + } + + fn get_preimage(hash: T::Hash) -> Option> { + PreimageFor::::get(hash).map(|preimage| preimage.to_vec()) + } + + fn request_preimage(hash: T::Hash) { + Self::do_request_preimage(hash) + } + + fn clear_request(hash: T::Hash) { + let res = Self::do_clear_request(hash); + debug_assert!(res.is_ok(), "do_clear_request failed - counter underflow?"); + } +} + +impl PreimageRecipient for Pallet { + type MaxSize = T::MaxSize; + + fn note_preimage(bytes: BoundedVec) { + // We don't really care if this fails, since that's only the case if someone else has + // already noted it. + let _ = Self::note_bytes(bytes, None); + } + + fn unnote_preimage(hash: T::Hash) { + // Should never fail if authorization check is skipped. + let res = Self::unnote_preimage(hash, None); + debug_assert!(res.is_ok(), "unnote_preimage failed - request outstanding?"); + } +} diff --git a/frame/support/src/traits.rs b/frame/support/src/traits.rs index bb990e25646db..dd03ed75d3ccb 100644 --- a/frame/support/src/traits.rs +++ b/frame/support/src/traits.rs @@ -52,8 +52,8 @@ mod misc; pub use misc::{ Backing, ConstU32, EnsureInherentsAreFirst, EqualPrivilegeOnly, EstimateCallFee, ExecuteBlock, ExtrinsicCall, Get, GetBacking, GetDefault, HandleLifetime, IsSubType, IsType, Len, - OffchainWorker, OnKilledAccount, OnNewAccount, PrivilegeCmp, SameOrOther, Time, TryDrop, - UnixTime, WrapperKeepOpaque, WrapperOpaque, + OffchainWorker, OnKilledAccount, OnNewAccount, PreimageHandler, PrivilegeCmp, SameOrOther, + Time, TryDrop, UnixTime, WrapperKeepOpaque, WrapperOpaque, }; mod stored_map; diff --git a/frame/support/src/traits/misc.rs b/frame/support/src/traits/misc.rs index 153c3804bd599..1c2b8d59400d2 100644 --- a/frame/support/src/traits/misc.rs +++ b/frame/support/src/traits/misc.rs @@ -564,6 +564,45 @@ impl TypeInfo for WrapperKeepOpaque { } } +/// A interface for looking up preimages from their hash on chain. +pub trait PreimageProvider { + /// Returns whether a preimage exists for a given hash. + /// + /// A value of `true` implies that `get_preimage` is `Some`. + fn preimage_exists(hash: Hash) -> bool; + + /// Returns the preimage for a given hash. + fn get_preimage(hash: Hash) -> Option>; + + /// Returns whether a preimage request exists for a given hash. + fn preimage_requested(hash: Hash) -> bool; + + /// Request that someone report a preimage. Providers use this to optimise the economics for + /// preimage reporting. + fn request_preimage(hash: Hash); + + /// Clear a preimage request. + fn clear_request(hash: Hash); +} + +/// A interface for managing preimages to hashes on chain. +/// +/// Note that this API does not assume any underlying user is calling, and thus +/// does not handle any preimage ownership or fees. Other system level logic that +/// uses this API should implement that on their own side. +pub trait PreimageRecipient { + /// Maximum size of a preimage. + type MaxSize: Get; + + /// Store the bytes of a preimage on chain. + fn note_preimage(bytes: crate::BoundedVec); + + /// Clear a previously noted preimage. This is infallible and should be treated more like a + /// hint - if it was not previously noted or if it is now requested, then this will not do + /// anything. + fn unnote_preimage(hash: Hash); +} + #[cfg(test)] mod test { use super::*; diff --git a/frame/support/src/traits/schedule.rs b/frame/support/src/traits/schedule.rs index 19f50a93c0681..314e480e1332e 100644 --- a/frame/support/src/traits/schedule.rs +++ b/frame/support/src/traits/schedule.rs @@ -18,6 +18,7 @@ //! Traits and associated utilities for scheduling dispatchables in FRAME. use codec::{Codec, Decode, Encode, EncodeLike}; +use frame_support::traits::PreimageProvider; use scale_info::TypeInfo; use sp_runtime::{DispatchError, RuntimeDebug}; use sp_std::{fmt::Debug, prelude::*};