// 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. //! Transaction storage pallet. Indexes transactions and manages storage proofs. // Ensure we're `no_std` when compiling for Wasm. #![cfg_attr(not(feature = "std"), no_std)] mod benchmarking; pub mod weights; #[cfg(test)] mod mock; #[cfg(test)] mod tests; use codec::{Decode, Encode}; use frame_support::{ dispatch::{Dispatchable, GetDispatchInfo}, traits::{Currency, OnUnbalanced, ReservableCurrency}, }; use sp_runtime::traits::{BlakeTwo256, Hash, One, Saturating, Zero}; use sp_std::{prelude::*, result}; use sp_transaction_storage_proof::{ encode_index, random_chunk, InherentError, TransactionStorageProof, CHUNK_SIZE, INHERENT_IDENTIFIER, }; /// A type alias for the balance type from this pallet's point of view. type BalanceOf = <::Currency as Currency<::AccountId>>::Balance; type NegativeImbalanceOf = <::Currency as Currency< ::AccountId, >>::NegativeImbalance; // Re-export pallet items so that they can be accessed from the crate namespace. pub use pallet::*; pub use weights::WeightInfo; /// Maximum bytes that can be stored in one transaction. // Setting higher limit also requires raising the allocator limit. pub const DEFAULT_MAX_TRANSACTION_SIZE: u32 = 8 * 1024 * 1024; pub const DEFAULT_MAX_BLOCK_TRANSACTIONS: u32 = 512; /// State data for a stored transaction. #[derive(Encode, Decode, Clone, sp_runtime::RuntimeDebug, PartialEq, Eq, scale_info::TypeInfo)] pub struct TransactionInfo { /// Chunk trie root. chunk_root: ::Output, /// Plain hash of indexed data. content_hash: ::Output, /// Size of indexed data in bytes. size: u32, /// Total number of chunks added in the block with this transaction. This /// is used find transaction info by block chunk index using binary search. block_chunks: u32, } fn num_chunks(bytes: u32) -> u32 { ((bytes as u64 + CHUNK_SIZE as u64 - 1) / CHUNK_SIZE as u64) as u32 } #[frame_support::pallet] pub mod pallet { use super::*; use frame_support::pallet_prelude::*; use frame_system::pallet_prelude::*; #[pallet::config] pub trait Config: frame_system::Config { /// The overarching event type. type Event: From> + IsType<::Event>; /// A dispatchable call. type Call: Parameter + Dispatchable + GetDispatchInfo + From>; /// The currency trait. type Currency: ReservableCurrency; /// Handler for the unbalanced decrease when fees are burned. type FeeDestination: OnUnbalanced>; /// Weight information for extrinsics in this pallet. type WeightInfo: WeightInfo; } #[pallet::error] pub enum Error { /// Insufficient account balance. InsufficientFunds, /// Invalid configuration. NotConfigured, /// Renewed extrinsic is not found. RenewedNotFound, /// Attempting to store empty transaction EmptyTransaction, /// Proof was not expected in this block. UnexpectedProof, /// Proof failed verification. InvalidProof, /// Missing storage proof. MissingProof, /// Unable to verify proof becasue state data is missing. MissingStateData, /// Double proof check in the block. DoubleCheck, /// Storage proof was not checked in the block. ProofNotChecked, /// Transaction is too large. TransactionTooLarge, /// Too many transactions in the block. TooManyTransactions, /// Attempted to call `store` outside of block execution. BadContext, } #[pallet::pallet] #[pallet::generate_store(pub(super) trait Store)] pub struct Pallet(_); #[pallet::hooks] impl Hooks> for Pallet { fn on_initialize(n: T::BlockNumber) -> Weight { // Drop obsolete roots. The proof for `obsolete` will be checked later // in this block, so we drop `obsolete` - 1. let period = >::get(); let obsolete = n.saturating_sub(period.saturating_add(One::one())); if obsolete > Zero::zero() { >::remove(obsolete); >::remove(obsolete); } // 2 writes in `on_initialize` and 2 writes + 2 reads in `on_finalize` T::DbWeight::get().reads_writes(2, 4) } fn on_finalize(n: T::BlockNumber) { assert!( >::take() || { // Proof is not required for early or empty blocks. let number = >::block_number(); let period = >::get(); let target_number = number.saturating_sub(period); target_number.is_zero() || >::get(target_number) == 0 }, "Storage proof must be checked once in the block" ); // Insert new transactions let transactions = >::take(); let total_chunks = transactions.last().map_or(0, |t| t.block_chunks); if total_chunks != 0 { >::insert(n, total_chunks); >::insert(n, transactions); } } } #[pallet::call] impl Pallet { /// Index and store data on chain. Minimum data size is 1 bytes, maximum is /// `MaxTransactionSize`. Data will be removed after `STORAGE_PERIOD` blocks, unless `renew` /// is called. # /// - n*log(n) of data size, as all data is pushed to an in-memory trie. /// Additionally contains a DB write. /// # #[pallet::weight(T::WeightInfo::store(data.len() as u32))] pub fn store(origin: OriginFor, data: Vec) -> DispatchResult { ensure!(data.len() > 0, Error::::EmptyTransaction); ensure!( data.len() <= MaxTransactionSize::::get() as usize, Error::::TransactionTooLarge ); let sender = ensure_signed(origin)?; Self::apply_fee(sender, data.len() as u32)?; // Chunk data and compute storage root let chunk_count = num_chunks(data.len() as u32); let chunks = data.chunks(CHUNK_SIZE).map(|c| c.to_vec()).collect(); let root = sp_io::trie::blake2_256_ordered_root(chunks); let content_hash = sp_io::hashing::blake2_256(&data); let extrinsic_index = >::extrinsic_index() .ok_or_else(|| Error::::BadContext)?; sp_io::transaction_index::index(extrinsic_index, data.len() as u32, content_hash); let mut index = 0; >::mutate(|transactions| { if transactions.len() + 1 > MaxBlockTransactions::::get() as usize { return Err(Error::::TooManyTransactions) } let total_chunks = transactions.last().map_or(0, |t| t.block_chunks) + chunk_count; index = transactions.len() as u32; transactions.push(TransactionInfo { chunk_root: root, size: data.len() as u32, content_hash: content_hash.into(), block_chunks: total_chunks, }); Ok(()) })?; Self::deposit_event(Event::Stored(index)); Ok(()) } /// Renew previously stored data. Parameters are the block number that contains /// previous `store` or `renew` call and transaction index within that block. /// Transaction index is emitted in the `Stored` or `Renewed` event. /// Applies same fees as `store`. /// # /// - Constant. /// # #[pallet::weight(T::WeightInfo::renew())] pub fn renew( origin: OriginFor, block: T::BlockNumber, index: u32, ) -> DispatchResultWithPostInfo { let sender = ensure_signed(origin)?; let transactions = >::get(block).ok_or(Error::::RenewedNotFound)?; let info = transactions.get(index as usize).ok_or(Error::::RenewedNotFound)?; Self::apply_fee(sender, info.size)?; let extrinsic_index = >::extrinsic_index().unwrap(); sp_io::transaction_index::renew(extrinsic_index, info.content_hash.into()); let mut index = 0; >::mutate(|transactions| { if transactions.len() + 1 > MaxBlockTransactions::::get() as usize { return Err(Error::::TooManyTransactions) } let chunks = num_chunks(info.size); let total_chunks = transactions.last().map_or(0, |t| t.block_chunks) + chunks; index = transactions.len() as u32; transactions.push(TransactionInfo { chunk_root: info.chunk_root, size: info.size, content_hash: info.content_hash, block_chunks: total_chunks, }); Ok(()) })?; Self::deposit_event(Event::Renewed(index)); Ok(().into()) } /// Check storage proof for block number `block_number() - StoragePeriod`. /// If such block does not exist the proof is expected to be `None`. /// # /// - Linear w.r.t the number of indexed transactions in the proved block for random /// probing. /// There's a DB read for each transaction. /// Here we assume a maximum of 100 probed transactions. /// # #[pallet::weight((T::WeightInfo::check_proof_max(), DispatchClass::Mandatory))] pub fn check_proof( origin: OriginFor, proof: TransactionStorageProof, ) -> DispatchResultWithPostInfo { ensure_none(origin)?; ensure!(!ProofChecked::::get(), Error::::DoubleCheck); let number = >::block_number(); let period = >::get(); let target_number = number.saturating_sub(period); ensure!(!target_number.is_zero(), Error::::UnexpectedProof); let total_chunks = >::get(target_number); ensure!(total_chunks != 0, Error::::UnexpectedProof); let parent_hash = >::parent_hash(); let selected_chunk_index = random_chunk(parent_hash.as_ref(), total_chunks); let (info, chunk_index) = match >::get(target_number) { Some(infos) => { let index = match infos .binary_search_by_key(&selected_chunk_index, |info| info.block_chunks) { Ok(index) => index, Err(index) => index, }; let info = infos.get(index).ok_or_else(|| Error::::MissingStateData)?.clone(); let chunks = num_chunks(info.size); let prev_chunks = info.block_chunks - chunks; (info, selected_chunk_index - prev_chunks) }, None => Err(Error::::MissingStateData)?, }; ensure!( sp_io::trie::blake2_256_verify_proof( info.chunk_root, &proof.proof, &encode_index(chunk_index), &proof.chunk, ), Error::::InvalidProof ); ProofChecked::::put(true); Self::deposit_event(Event::ProofChecked); Ok(().into()) } } #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { /// Stored data under specified index. Stored(u32), /// Renewed data under specified index. Renewed(u32), /// Storage proof was successfully checked. ProofChecked, } /// Collection of transaction metadata by block number. #[pallet::storage] #[pallet::getter(fn transaction_roots)] pub(super) type Transactions = StorageMap<_, Blake2_128Concat, T::BlockNumber, Vec, OptionQuery>; /// Count indexed chunks for each block. #[pallet::storage] pub(super) type ChunkCount = StorageMap<_, Blake2_128Concat, T::BlockNumber, u32, ValueQuery>; #[pallet::storage] #[pallet::getter(fn byte_fee)] /// Storage fee per byte. pub(super) type ByteFee = StorageValue<_, BalanceOf>; #[pallet::storage] #[pallet::getter(fn entry_fee)] /// Storage fee per transaction. pub(super) type EntryFee = StorageValue<_, BalanceOf>; #[pallet::storage] #[pallet::getter(fn max_transaction_size)] /// Maximum data set in a single transaction in bytes. pub(super) type MaxTransactionSize = StorageValue<_, u32, ValueQuery>; #[pallet::storage] #[pallet::getter(fn max_block_transactions)] /// Maximum number of indexed transactions in the block. pub(super) type MaxBlockTransactions = StorageValue<_, u32, ValueQuery>; /// Storage period for data in blocks. Should match `sp_storage_proof::DEFAULT_STORAGE_PERIOD` /// for block authoring. #[pallet::storage] pub(super) type StoragePeriod = StorageValue<_, T::BlockNumber, ValueQuery>; // Intermediates #[pallet::storage] pub(super) type BlockTransactions = StorageValue<_, Vec, ValueQuery>; /// Was the proof checked in this block? #[pallet::storage] pub(super) type ProofChecked = StorageValue<_, bool, ValueQuery>; #[pallet::genesis_config] pub struct GenesisConfig { pub byte_fee: BalanceOf, pub entry_fee: BalanceOf, pub storage_period: T::BlockNumber, pub max_block_transactions: u32, pub max_transaction_size: u32, } #[cfg(feature = "std")] impl Default for GenesisConfig { fn default() -> Self { Self { byte_fee: 10u32.into(), entry_fee: 1000u32.into(), storage_period: sp_transaction_storage_proof::DEFAULT_STORAGE_PERIOD.into(), max_block_transactions: DEFAULT_MAX_BLOCK_TRANSACTIONS, max_transaction_size: DEFAULT_MAX_TRANSACTION_SIZE, } } } #[pallet::genesis_build] impl GenesisBuild for GenesisConfig { fn build(&self) { >::put(&self.byte_fee); >::put(&self.entry_fee); >::put(&self.max_transaction_size); >::put(&self.max_block_transactions); >::put(&self.storage_period); } } #[pallet::inherent] impl ProvideInherent for Pallet { type Call = Call; type Error = InherentError; const INHERENT_IDENTIFIER: InherentIdentifier = INHERENT_IDENTIFIER; fn create_inherent(data: &InherentData) -> Option { let proof = data .get_data::(&Self::INHERENT_IDENTIFIER) .unwrap_or(None); proof.map(|proof| Call::check_proof { proof }) } fn check_inherent( _call: &Self::Call, _data: &InherentData, ) -> result::Result<(), Self::Error> { Ok(()) } fn is_inherent(call: &Self::Call) -> bool { matches!(call, Call::check_proof { .. }) } } impl Pallet { fn apply_fee(sender: T::AccountId, size: u32) -> DispatchResult { let byte_fee = ByteFee::::get().ok_or(Error::::NotConfigured)?; let entry_fee = EntryFee::::get().ok_or(Error::::NotConfigured)?; let fee = byte_fee.saturating_mul(size.into()).saturating_add(entry_fee); ensure!(T::Currency::can_slash(&sender, fee), Error::::InsufficientFunds); let (credit, _) = T::Currency::slash(&sender, fee); T::FeeDestination::on_unbalanced(credit); Ok(()) } } }