referrerpolicy=no-referrer-when-downgrade

pallet_transaction_storage/
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//! Transaction storage pallet. Indexes transactions and manages storage proofs.
19
20// Ensure we're `no_std` when compiling for Wasm.
21#![cfg_attr(not(feature = "std"), no_std)]
22
23mod benchmarking;
24pub mod weights;
25
26#[cfg(test)]
27mod mock;
28#[cfg(test)]
29mod tests;
30
31extern crate alloc;
32
33use alloc::vec::Vec;
34use codec::{Decode, Encode, MaxEncodedLen};
35use core::result;
36use frame_support::{
37	dispatch::GetDispatchInfo,
38	traits::{
39		fungible::{hold::Balanced, Inspect, Mutate, MutateHold},
40		tokens::fungible::Credit,
41		OnUnbalanced,
42	},
43};
44use sp_runtime::traits::{BlakeTwo256, Dispatchable, Hash, One, Saturating, Zero};
45use sp_transaction_storage_proof::{
46	encode_index, random_chunk, InherentError, TransactionStorageProof, CHUNK_SIZE,
47	INHERENT_IDENTIFIER,
48};
49
50/// A type alias for the balance type from this pallet's point of view.
51type BalanceOf<T> =
52	<<T as Config>::Currency as Inspect<<T as frame_system::Config>::AccountId>>::Balance;
53pub type CreditOf<T> = Credit<<T as frame_system::Config>::AccountId, <T as Config>::Currency>;
54
55// Re-export pallet items so that they can be accessed from the crate namespace.
56pub use pallet::*;
57pub use weights::WeightInfo;
58
59/// Maximum bytes that can be stored in one transaction.
60// Setting higher limit also requires raising the allocator limit.
61pub const DEFAULT_MAX_TRANSACTION_SIZE: u32 = 8 * 1024 * 1024;
62pub const DEFAULT_MAX_BLOCK_TRANSACTIONS: u32 = 512;
63
64/// State data for a stored transaction.
65#[derive(
66	Encode,
67	Decode,
68	Clone,
69	sp_runtime::RuntimeDebug,
70	PartialEq,
71	Eq,
72	scale_info::TypeInfo,
73	MaxEncodedLen,
74)]
75pub struct TransactionInfo {
76	/// Chunk trie root.
77	chunk_root: <BlakeTwo256 as Hash>::Output,
78	/// Plain hash of indexed data.
79	content_hash: <BlakeTwo256 as Hash>::Output,
80	/// Size of indexed data in bytes.
81	size: u32,
82	/// Total number of chunks added in the block with this transaction. This
83	/// is used find transaction info by block chunk index using binary search.
84	block_chunks: u32,
85}
86
87fn num_chunks(bytes: u32) -> u32 {
88	(bytes as u64).div_ceil(CHUNK_SIZE as u64) as u32
89}
90
91#[frame_support::pallet]
92pub mod pallet {
93	use super::*;
94	use frame_support::pallet_prelude::*;
95	use frame_system::pallet_prelude::*;
96
97	/// A reason for this pallet placing a hold on funds.
98	#[pallet::composite_enum]
99	pub enum HoldReason {
100		/// The funds are held as deposit for the used storage.
101		StorageFeeHold,
102	}
103
104	#[pallet::config]
105	pub trait Config: frame_system::Config {
106		/// The overarching event type.
107		#[allow(deprecated)]
108		type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
109		/// A dispatchable call.
110		type RuntimeCall: Parameter
111			+ Dispatchable<RuntimeOrigin = Self::RuntimeOrigin>
112			+ GetDispatchInfo
113			+ From<frame_system::Call<Self>>;
114		/// The fungible type for this pallet.
115		type Currency: Mutate<Self::AccountId>
116			+ MutateHold<Self::AccountId, Reason = Self::RuntimeHoldReason>
117			+ Balanced<Self::AccountId>;
118		/// The overarching runtime hold reason.
119		type RuntimeHoldReason: From<HoldReason>;
120		/// Handler for the unbalanced decrease when fees are burned.
121		type FeeDestination: OnUnbalanced<CreditOf<Self>>;
122		/// Weight information for extrinsics in this pallet.
123		type WeightInfo: WeightInfo;
124		/// Maximum number of indexed transactions in the block.
125		type MaxBlockTransactions: Get<u32>;
126		/// Maximum data set in a single transaction in bytes.
127		type MaxTransactionSize: Get<u32>;
128	}
129
130	#[pallet::error]
131	pub enum Error<T> {
132		/// Invalid configuration.
133		NotConfigured,
134		/// Renewed extrinsic is not found.
135		RenewedNotFound,
136		/// Attempting to store empty transaction
137		EmptyTransaction,
138		/// Proof was not expected in this block.
139		UnexpectedProof,
140		/// Proof failed verification.
141		InvalidProof,
142		/// Missing storage proof.
143		MissingProof,
144		/// Unable to verify proof because state data is missing.
145		MissingStateData,
146		/// Double proof check in the block.
147		DoubleCheck,
148		/// Storage proof was not checked in the block.
149		ProofNotChecked,
150		/// Transaction is too large.
151		TransactionTooLarge,
152		/// Too many transactions in the block.
153		TooManyTransactions,
154		/// Attempted to call `store` outside of block execution.
155		BadContext,
156	}
157
158	#[pallet::pallet]
159	pub struct Pallet<T>(_);
160
161	#[pallet::hooks]
162	impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
163		fn on_initialize(n: BlockNumberFor<T>) -> Weight {
164			// Drop obsolete roots. The proof for `obsolete` will be checked later
165			// in this block, so we drop `obsolete` - 1.
166			let period = StoragePeriod::<T>::get();
167			let obsolete = n.saturating_sub(period.saturating_add(One::one()));
168			if obsolete > Zero::zero() {
169				Transactions::<T>::remove(obsolete);
170				ChunkCount::<T>::remove(obsolete);
171			}
172			// 2 writes in `on_initialize` and 2 writes + 2 reads in `on_finalize`
173			T::DbWeight::get().reads_writes(2, 4)
174		}
175
176		fn on_finalize(n: BlockNumberFor<T>) {
177			assert!(
178				ProofChecked::<T>::take() || {
179					// Proof is not required for early or empty blocks.
180					let number = frame_system::Pallet::<T>::block_number();
181					let period = StoragePeriod::<T>::get();
182					let target_number = number.saturating_sub(period);
183					target_number.is_zero() || ChunkCount::<T>::get(target_number) == 0
184				},
185				"Storage proof must be checked once in the block"
186			);
187			// Insert new transactions
188			let transactions = BlockTransactions::<T>::take();
189			let total_chunks = transactions.last().map_or(0, |t| t.block_chunks);
190			if total_chunks != 0 {
191				ChunkCount::<T>::insert(n, total_chunks);
192				Transactions::<T>::insert(n, transactions);
193			}
194		}
195	}
196
197	#[pallet::call]
198	impl<T: Config> Pallet<T> {
199		/// Index and store data off chain. Minimum data size is 1 bytes, maximum is
200		/// `MaxTransactionSize`. Data will be removed after `STORAGE_PERIOD` blocks, unless `renew`
201		/// is called.
202		/// ## Complexity
203		/// - O(n*log(n)) of data size, as all data is pushed to an in-memory trie.
204		#[pallet::call_index(0)]
205		#[pallet::weight(T::WeightInfo::store(data.len() as u32))]
206		pub fn store(origin: OriginFor<T>, data: Vec<u8>) -> DispatchResult {
207			ensure!(data.len() > 0, Error::<T>::EmptyTransaction);
208			ensure!(
209				data.len() <= T::MaxTransactionSize::get() as usize,
210				Error::<T>::TransactionTooLarge
211			);
212			let sender = ensure_signed(origin)?;
213			Self::apply_fee(sender, data.len() as u32)?;
214
215			// Chunk data and compute storage root
216			let chunk_count = num_chunks(data.len() as u32);
217			let chunks = data.chunks(CHUNK_SIZE).map(|c| c.to_vec()).collect();
218			let root = sp_io::trie::blake2_256_ordered_root(chunks, sp_runtime::StateVersion::V1);
219
220			let content_hash = sp_io::hashing::blake2_256(&data);
221			let extrinsic_index =
222				frame_system::Pallet::<T>::extrinsic_index().ok_or(Error::<T>::BadContext)?;
223			sp_io::transaction_index::index(extrinsic_index, data.len() as u32, content_hash);
224
225			let mut index = 0;
226			BlockTransactions::<T>::mutate(|transactions| {
227				if transactions.len() + 1 > T::MaxBlockTransactions::get() as usize {
228					return Err(Error::<T>::TooManyTransactions)
229				}
230				let total_chunks = transactions.last().map_or(0, |t| t.block_chunks) + chunk_count;
231				index = transactions.len() as u32;
232				transactions
233					.try_push(TransactionInfo {
234						chunk_root: root,
235						size: data.len() as u32,
236						content_hash: content_hash.into(),
237						block_chunks: total_chunks,
238					})
239					.map_err(|_| Error::<T>::TooManyTransactions)?;
240				Ok(())
241			})?;
242			Self::deposit_event(Event::Stored { index });
243			Ok(())
244		}
245
246		/// Renew previously stored data. Parameters are the block number that contains
247		/// previous `store` or `renew` call and transaction index within that block.
248		/// Transaction index is emitted in the `Stored` or `Renewed` event.
249		/// Applies same fees as `store`.
250		/// ## Complexity
251		/// - O(1).
252		#[pallet::call_index(1)]
253		#[pallet::weight(T::WeightInfo::renew())]
254		pub fn renew(
255			origin: OriginFor<T>,
256			block: BlockNumberFor<T>,
257			index: u32,
258		) -> DispatchResultWithPostInfo {
259			let sender = ensure_signed(origin)?;
260			let transactions = Transactions::<T>::get(block).ok_or(Error::<T>::RenewedNotFound)?;
261			let info = transactions.get(index as usize).ok_or(Error::<T>::RenewedNotFound)?;
262			let extrinsic_index =
263				frame_system::Pallet::<T>::extrinsic_index().ok_or(Error::<T>::BadContext)?;
264
265			Self::apply_fee(sender, info.size)?;
266
267			sp_io::transaction_index::renew(extrinsic_index, info.content_hash.into());
268
269			let mut index = 0;
270			BlockTransactions::<T>::mutate(|transactions| {
271				if transactions.len() + 1 > T::MaxBlockTransactions::get() as usize {
272					return Err(Error::<T>::TooManyTransactions)
273				}
274				let chunks = num_chunks(info.size);
275				let total_chunks = transactions.last().map_or(0, |t| t.block_chunks) + chunks;
276				index = transactions.len() as u32;
277				transactions
278					.try_push(TransactionInfo {
279						chunk_root: info.chunk_root,
280						size: info.size,
281						content_hash: info.content_hash,
282						block_chunks: total_chunks,
283					})
284					.map_err(|_| Error::<T>::TooManyTransactions)
285			})?;
286			Self::deposit_event(Event::Renewed { index });
287			Ok(().into())
288		}
289
290		/// Check storage proof for block number `block_number() - StoragePeriod`.
291		/// If such block does not exist the proof is expected to be `None`.
292		/// ## Complexity
293		/// - Linear w.r.t the number of indexed transactions in the proved block for random
294		///   probing.
295		/// There's a DB read for each transaction.
296		#[pallet::call_index(2)]
297		#[pallet::weight((T::WeightInfo::check_proof_max(), DispatchClass::Mandatory))]
298		pub fn check_proof(
299			origin: OriginFor<T>,
300			proof: TransactionStorageProof,
301		) -> DispatchResultWithPostInfo {
302			ensure_none(origin)?;
303			ensure!(!ProofChecked::<T>::get(), Error::<T>::DoubleCheck);
304			let number = frame_system::Pallet::<T>::block_number();
305			let period = StoragePeriod::<T>::get();
306			let target_number = number.saturating_sub(period);
307			ensure!(!target_number.is_zero(), Error::<T>::UnexpectedProof);
308			let total_chunks = ChunkCount::<T>::get(target_number);
309			ensure!(total_chunks != 0, Error::<T>::UnexpectedProof);
310			let parent_hash = frame_system::Pallet::<T>::parent_hash();
311			let selected_chunk_index = random_chunk(parent_hash.as_ref(), total_chunks);
312			let (info, chunk_index) = match Transactions::<T>::get(target_number) {
313				Some(infos) => {
314					let index = match infos
315						.binary_search_by_key(&selected_chunk_index, |info| info.block_chunks)
316					{
317						Ok(index) => index,
318						Err(index) => index,
319					};
320					let info = infos.get(index).ok_or(Error::<T>::MissingStateData)?.clone();
321					let chunks = num_chunks(info.size);
322					let prev_chunks = info.block_chunks - chunks;
323					(info, selected_chunk_index - prev_chunks)
324				},
325				None => return Err(Error::<T>::MissingStateData.into()),
326			};
327			ensure!(
328				sp_io::trie::blake2_256_verify_proof(
329					info.chunk_root,
330					&proof.proof,
331					&encode_index(chunk_index),
332					&proof.chunk,
333					sp_runtime::StateVersion::V1,
334				),
335				Error::<T>::InvalidProof
336			);
337			ProofChecked::<T>::put(true);
338			Self::deposit_event(Event::ProofChecked);
339			Ok(().into())
340		}
341	}
342
343	#[pallet::event]
344	#[pallet::generate_deposit(pub(super) fn deposit_event)]
345	pub enum Event<T: Config> {
346		/// Stored data under specified index.
347		Stored { index: u32 },
348		/// Renewed data under specified index.
349		Renewed { index: u32 },
350		/// Storage proof was successfully checked.
351		ProofChecked,
352	}
353
354	/// Collection of transaction metadata by block number.
355	#[pallet::storage]
356	pub type Transactions<T: Config> = StorageMap<
357		_,
358		Blake2_128Concat,
359		BlockNumberFor<T>,
360		BoundedVec<TransactionInfo, T::MaxBlockTransactions>,
361		OptionQuery,
362	>;
363
364	/// Count indexed chunks for each block.
365	#[pallet::storage]
366	pub type ChunkCount<T: Config> =
367		StorageMap<_, Blake2_128Concat, BlockNumberFor<T>, u32, ValueQuery>;
368
369	#[pallet::storage]
370	/// Storage fee per byte.
371	pub type ByteFee<T: Config> = StorageValue<_, BalanceOf<T>>;
372
373	#[pallet::storage]
374	/// Storage fee per transaction.
375	pub type EntryFee<T: Config> = StorageValue<_, BalanceOf<T>>;
376
377	/// Storage period for data in blocks. Should match `sp_storage_proof::DEFAULT_STORAGE_PERIOD`
378	/// for block authoring.
379	#[pallet::storage]
380	pub type StoragePeriod<T: Config> = StorageValue<_, BlockNumberFor<T>, ValueQuery>;
381
382	// Intermediates
383	#[pallet::storage]
384	pub type BlockTransactions<T: Config> =
385		StorageValue<_, BoundedVec<TransactionInfo, T::MaxBlockTransactions>, ValueQuery>;
386
387	/// Was the proof checked in this block?
388	#[pallet::storage]
389	pub type ProofChecked<T: Config> = StorageValue<_, bool, ValueQuery>;
390
391	#[pallet::genesis_config]
392	pub struct GenesisConfig<T: Config> {
393		pub byte_fee: BalanceOf<T>,
394		pub entry_fee: BalanceOf<T>,
395		pub storage_period: BlockNumberFor<T>,
396	}
397
398	impl<T: Config> Default for GenesisConfig<T> {
399		fn default() -> Self {
400			Self {
401				byte_fee: 10u32.into(),
402				entry_fee: 1000u32.into(),
403				storage_period: sp_transaction_storage_proof::DEFAULT_STORAGE_PERIOD.into(),
404			}
405		}
406	}
407
408	#[pallet::genesis_build]
409	impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
410		fn build(&self) {
411			ByteFee::<T>::put(&self.byte_fee);
412			EntryFee::<T>::put(&self.entry_fee);
413			StoragePeriod::<T>::put(&self.storage_period);
414		}
415	}
416
417	#[pallet::inherent]
418	impl<T: Config> ProvideInherent for Pallet<T> {
419		type Call = Call<T>;
420		type Error = InherentError;
421		const INHERENT_IDENTIFIER: InherentIdentifier = INHERENT_IDENTIFIER;
422
423		fn create_inherent(data: &InherentData) -> Option<Self::Call> {
424			let proof = data
425				.get_data::<TransactionStorageProof>(&Self::INHERENT_IDENTIFIER)
426				.unwrap_or(None);
427			proof.map(|proof| Call::check_proof { proof })
428		}
429
430		fn check_inherent(
431			_call: &Self::Call,
432			_data: &InherentData,
433		) -> result::Result<(), Self::Error> {
434			Ok(())
435		}
436
437		fn is_inherent(call: &Self::Call) -> bool {
438			matches!(call, Call::check_proof { .. })
439		}
440	}
441
442	impl<T: Config> Pallet<T> {
443		/// Get transaction storage information from outside of this pallet.
444		pub fn transaction_roots(
445			block: BlockNumberFor<T>,
446		) -> Option<BoundedVec<TransactionInfo, T::MaxBlockTransactions>> {
447			Transactions::<T>::get(block)
448		}
449		/// Get ByteFee storage information from outside of this pallet.
450		pub fn byte_fee() -> Option<BalanceOf<T>> {
451			ByteFee::<T>::get()
452		}
453		/// Get EntryFee storage information from outside of this pallet.
454		pub fn entry_fee() -> Option<BalanceOf<T>> {
455			EntryFee::<T>::get()
456		}
457
458		fn apply_fee(sender: T::AccountId, size: u32) -> DispatchResult {
459			let byte_fee = ByteFee::<T>::get().ok_or(Error::<T>::NotConfigured)?;
460			let entry_fee = EntryFee::<T>::get().ok_or(Error::<T>::NotConfigured)?;
461			let fee = byte_fee.saturating_mul(size.into()).saturating_add(entry_fee);
462			T::Currency::hold(&HoldReason::StorageFeeHold.into(), &sender, fee)?;
463			let (credit, _remainder) =
464				T::Currency::slash(&HoldReason::StorageFeeHold.into(), &sender, fee);
465			debug_assert!(_remainder.is_zero());
466			T::FeeDestination::on_unbalanced(credit);
467			Ok(())
468		}
469	}
470}