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::{fmt::Debug, result};
36use frame_support::{
37	dispatch::GetDispatchInfo,
38	pallet_prelude::InvalidTransaction,
39	traits::{
40		fungible::{hold::Balanced, Credit, Inspect, Mutate, MutateHold},
41		OnUnbalanced,
42	},
43};
44use frame_system::pallet_prelude::BlockNumberFor;
45use sp_runtime::traits::{BlakeTwo256, Dispatchable, Hash, One, Saturating, Zero};
46use sp_transaction_storage_proof::{
47	encode_index, num_chunks, random_chunk, ChunkIndex, InherentError, TransactionStorageProof,
48	CHUNK_SIZE, INHERENT_IDENTIFIER,
49};
50
51/// A type alias for the balance type from this pallet's point of view.
52type BalanceOf<T> =
53	<<T as Config>::Currency as Inspect<<T as frame_system::Config>::AccountId>>::Balance;
54pub type CreditOf<T> = Credit<<T as frame_system::Config>::AccountId, <T as Config>::Currency>;
55
56// Re-export pallet items so that they can be accessed from the crate namespace.
57pub use pallet::*;
58pub use weights::WeightInfo;
59
60// TODO: https://github.com/paritytech/polkadot-sdk/issues/10591 - Clarify purpose of allocator limits and decide whether to remove or use these constants.
61/// Maximum bytes that can be stored in one transaction.
62// Setting higher limit also requires raising the allocator limit.
63pub const DEFAULT_MAX_TRANSACTION_SIZE: u32 = 8 * 1024 * 1024;
64pub const DEFAULT_MAX_BLOCK_TRANSACTIONS: u32 = 512;
65
66/// Encountered an impossible situation, implies a bug.
67pub const IMPOSSIBLE: InvalidTransaction = InvalidTransaction::Custom(0);
68/// Data size is not in the allowed range.
69pub const BAD_DATA_SIZE: InvalidTransaction = InvalidTransaction::Custom(1);
70/// Renewed extrinsic not found.
71pub const RENEWED_NOT_FOUND: InvalidTransaction = InvalidTransaction::Custom(2);
72/// Authorization was not found.
73pub const AUTHORIZATION_NOT_FOUND: InvalidTransaction = InvalidTransaction::Custom(3);
74/// Authorization has not expired.
75pub const AUTHORIZATION_NOT_EXPIRED: InvalidTransaction = InvalidTransaction::Custom(4);
76
77/// Number of transactions and bytes covered by an authorization.
78#[derive(PartialEq, Eq, Debug, Encode, Decode, scale_info::TypeInfo, MaxEncodedLen)]
79pub struct AuthorizationExtent {
80	/// Number of transactions.
81	pub transactions: u32,
82	/// Number of bytes.
83	pub bytes: u64,
84}
85
86/// Hash of a stored blob of data.
87type ContentHash = [u8; 32];
88
89/// The scope of an authorization.
90#[derive(Encode, Decode, scale_info::TypeInfo, MaxEncodedLen)]
91enum AuthorizationScope<AccountId> {
92	/// Authorization for the given account to store arbitrary data.
93	Account(AccountId),
94	/// Authorization for anyone to store data with a specific hash.
95	Preimage(ContentHash),
96}
97
98type AuthorizationScopeFor<T> = AuthorizationScope<<T as frame_system::Config>::AccountId>;
99
100/// An authorization to store data.
101#[derive(Encode, Decode, scale_info::TypeInfo, MaxEncodedLen)]
102struct Authorization<BlockNumber> {
103	/// Extent of the authorization (number of transactions/bytes).
104	extent: AuthorizationExtent,
105	/// The block at which this authorization expires.
106	expiration: BlockNumber,
107}
108
109type AuthorizationFor<T> = Authorization<BlockNumberFor<T>>;
110
111/// State data for a stored transaction.
112#[derive(Encode, Decode, Clone, Debug, PartialEq, Eq, scale_info::TypeInfo, MaxEncodedLen)]
113pub struct TransactionInfo {
114	/// Chunk trie root.
115	chunk_root: <BlakeTwo256 as Hash>::Output,
116	/// Plain hash of indexed data.
117	content_hash: <BlakeTwo256 as Hash>::Output,
118	/// Size of indexed data in bytes.
119	size: u32,
120	/// Total number of chunks added in the block with this transaction. This
121	/// is used to find transaction info by block chunk index using binary search.
122	///
123	/// Cumulative value of all previous transactions in the block; the last transaction holds the
124	/// total chunks.
125	block_chunks: ChunkIndex,
126}
127
128impl TransactionInfo {
129	/// Get the number of total chunks.
130	///
131	/// See the `block_chunks` field of [`TransactionInfo`] for details.
132	pub fn total_chunks(txs: &[TransactionInfo]) -> ChunkIndex {
133		txs.last().map_or(0, |t| t.block_chunks)
134	}
135}
136
137#[frame_support::pallet]
138pub mod pallet {
139	use super::*;
140	use frame_support::pallet_prelude::*;
141	use frame_system::pallet_prelude::*;
142
143	/// A reason for this pallet placing a hold on funds.
144	#[pallet::composite_enum]
145	pub enum HoldReason {
146		/// The funds are held as deposit for the used storage.
147		StorageFeeHold,
148	}
149
150	#[pallet::config]
151	pub trait Config: frame_system::Config {
152		/// The overarching event type.
153		#[allow(deprecated)]
154		type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
155		/// A dispatchable call.
156		type RuntimeCall: Parameter
157			+ Dispatchable<RuntimeOrigin = Self::RuntimeOrigin>
158			+ GetDispatchInfo
159			+ From<frame_system::Call<Self>>;
160		/// The fungible type for this pallet.
161		type Currency: Mutate<Self::AccountId>
162			+ MutateHold<Self::AccountId, Reason = Self::RuntimeHoldReason>
163			+ Balanced<Self::AccountId>;
164		/// The overarching runtime hold reason.
165		type RuntimeHoldReason: From<HoldReason>;
166		/// Handler for the unbalanced decrease when fees are burned.
167		type FeeDestination: OnUnbalanced<CreditOf<Self>>;
168		/// Weight information for extrinsics in this pallet.
169		type WeightInfo: WeightInfo;
170		/// Maximum number of indexed transactions in the block.
171		type MaxBlockTransactions: Get<u32>;
172		/// Maximum data set in a single transaction in bytes.
173		type MaxTransactionSize: Get<u32>;
174	}
175
176	#[pallet::error]
177	pub enum Error<T> {
178		/// Attempted to call `store`/`renew` outside of block execution.
179		BadContext,
180		/// Data size is not in the allowed range.
181		BadDataSize,
182		/// Too many transactions in the block.
183		TooManyTransactions,
184		/// Invalid configuration.
185		NotConfigured,
186		/// Renewed extrinsic is not found.
187		RenewedNotFound,
188		/// Attempting to store an empty transaction
189		EmptyTransaction,
190		/// Proof was not expected in this block.
191		UnexpectedProof,
192		/// Proof failed verification.
193		InvalidProof,
194		/// Missing storage proof.
195		MissingProof,
196		/// Unable to verify proof because state data is missing.
197		MissingStateData,
198		/// Double proof check in the block.
199		DoubleCheck,
200		/// Storage proof was not checked in the block.
201		ProofNotChecked,
202		/// Transaction is too large.
203		TransactionTooLarge,
204		/// Authorization was not found.
205		AuthorizationNotFound,
206		/// Authorization has not expired.
207		AuthorizationNotExpired,
208	}
209
210	#[pallet::pallet]
211	pub struct Pallet<T>(_);
212
213	#[pallet::hooks]
214	impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
215		fn on_initialize(n: BlockNumberFor<T>) -> Weight {
216			// TODO: https://github.com/paritytech/polkadot-sdk/issues/10203 - Replace this with benchmarked weights.
217			let mut weight = Weight::zero();
218			let db_weight = T::DbWeight::get();
219
220			// Drop obsolete roots. The proof for `obsolete` will be checked later
221			// in this block, so we drop `obsolete` - 1.
222			weight.saturating_accrue(db_weight.reads(1));
223			let period = StoragePeriod::<T>::get();
224			let obsolete = n.saturating_sub(period.saturating_add(One::one()));
225			if obsolete > Zero::zero() {
226				weight.saturating_accrue(db_weight.writes(1));
227				Transactions::<T>::remove(obsolete);
228			}
229
230			// For `on_finalize`
231			weight.saturating_accrue(db_weight.reads_writes(3, 1));
232			weight
233		}
234
235		fn on_finalize(n: BlockNumberFor<T>) {
236			assert!(
237				ProofChecked::<T>::take() || {
238					// Proof is not required for early or empty blocks.
239					let number = frame_system::Pallet::<T>::block_number();
240					let period = StoragePeriod::<T>::get();
241					let target_number = number.saturating_sub(period);
242
243					target_number.is_zero() || {
244						// An empty block means no transactions were stored, relying on the fact
245						// below that we store transactions only if they contain chunks.
246						!Transactions::<T>::contains_key(target_number)
247					}
248				},
249				"Storage proof must be checked once in the block"
250			);
251			// Insert new transactions, iff they have chunks.
252			let transactions = BlockTransactions::<T>::take();
253			let total_chunks = TransactionInfo::total_chunks(&transactions);
254			if total_chunks != 0 {
255				Transactions::<T>::insert(n, transactions);
256			}
257		}
258	}
259
260	#[pallet::call]
261	impl<T: Config> Pallet<T> {
262		/// Index and store data off chain. Minimum data size is 1 bytes, maximum is
263		/// `MaxTransactionSize`. Data will be removed after `STORAGE_PERIOD` blocks, unless `renew`
264		/// is called.
265		/// ## Complexity
266		/// - O(n*log(n)) of data size, as all data is pushed to an in-memory trie.
267		#[pallet::call_index(0)]
268		#[pallet::weight(T::WeightInfo::store(data.len() as u32))]
269		pub fn store(origin: OriginFor<T>, data: Vec<u8>) -> DispatchResult {
270			ensure!(data.len() > 0, Error::<T>::EmptyTransaction);
271			ensure!(
272				data.len() <= T::MaxTransactionSize::get() as usize,
273				Error::<T>::TransactionTooLarge
274			);
275			let sender = ensure_signed(origin)?;
276			Self::apply_fee(sender, data.len() as u32)?;
277
278			// Chunk data and compute storage root
279			let chunk_count = num_chunks(data.len() as u32);
280			let chunks = data.chunks(CHUNK_SIZE).map(|c| c.to_vec()).collect();
281			let root = sp_io::trie::blake2_256_ordered_root(chunks, sp_runtime::StateVersion::V1);
282
283			let content_hash = sp_io::hashing::blake2_256(&data);
284			let extrinsic_index =
285				frame_system::Pallet::<T>::extrinsic_index().ok_or(Error::<T>::BadContext)?;
286			sp_io::transaction_index::index(extrinsic_index, data.len() as u32, content_hash);
287
288			let mut index = 0;
289			BlockTransactions::<T>::mutate(|transactions| {
290				if transactions.len() + 1 > T::MaxBlockTransactions::get() as usize {
291					return Err(Error::<T>::TooManyTransactions);
292				}
293				let total_chunks = TransactionInfo::total_chunks(&transactions) + chunk_count;
294				index = transactions.len() as u32;
295				transactions
296					.try_push(TransactionInfo {
297						chunk_root: root,
298						size: data.len() as u32,
299						content_hash: content_hash.into(),
300						block_chunks: total_chunks,
301					})
302					.map_err(|_| Error::<T>::TooManyTransactions)?;
303				Ok(())
304			})?;
305			Self::deposit_event(Event::Stored { index, content_hash });
306			Ok(())
307		}
308
309		/// Renew previously stored data. Parameters are the block number that contains
310		/// previous `store` or `renew` call and transaction index within that block.
311		/// Transaction index is emitted in the `Stored` or `Renewed` event.
312		/// Applies same fees as `store`.
313		/// ## Complexity
314		/// - O(1).
315		#[pallet::call_index(1)]
316		#[pallet::weight(T::WeightInfo::renew())]
317		pub fn renew(
318			origin: OriginFor<T>,
319			block: BlockNumberFor<T>,
320			index: u32,
321		) -> DispatchResultWithPostInfo {
322			let sender = ensure_signed(origin)?;
323			let transactions = Transactions::<T>::get(block).ok_or(Error::<T>::RenewedNotFound)?;
324			let info = transactions.get(index as usize).ok_or(Error::<T>::RenewedNotFound)?;
325			let extrinsic_index =
326				frame_system::Pallet::<T>::extrinsic_index().ok_or(Error::<T>::BadContext)?;
327
328			Self::apply_fee(sender, info.size)?;
329			let content_hash = info.content_hash.into();
330			sp_io::transaction_index::renew(extrinsic_index, content_hash);
331
332			let mut index = 0;
333			BlockTransactions::<T>::mutate(|transactions| {
334				if transactions.len() + 1 > T::MaxBlockTransactions::get() as usize {
335					return Err(Error::<T>::TooManyTransactions);
336				}
337				let chunks = num_chunks(info.size);
338				let total_chunks = TransactionInfo::total_chunks(&transactions) + chunks;
339				index = transactions.len() as u32;
340				transactions
341					.try_push(TransactionInfo {
342						chunk_root: info.chunk_root,
343						size: info.size,
344						content_hash: info.content_hash,
345						block_chunks: total_chunks,
346					})
347					.map_err(|_| Error::<T>::TooManyTransactions)
348			})?;
349			Self::deposit_event(Event::Renewed { index, content_hash });
350			Ok(().into())
351		}
352
353		/// Check storage proof for block number `block_number() - StoragePeriod`.
354		/// If such a block does not exist, the proof is expected to be `None`.
355		///
356		/// ## Complexity
357		/// - Linear w.r.t the number of indexed transactions in the proved block for random
358		///   probing.
359		/// There's a DB read for each transaction.
360		#[pallet::call_index(2)]
361		#[pallet::weight((T::WeightInfo::check_proof_max(), DispatchClass::Mandatory))]
362		pub fn check_proof(
363			origin: OriginFor<T>,
364			proof: TransactionStorageProof,
365		) -> DispatchResultWithPostInfo {
366			ensure_none(origin)?;
367			ensure!(!ProofChecked::<T>::get(), Error::<T>::DoubleCheck);
368
369			// Get the target block metadata.
370			let number = frame_system::Pallet::<T>::block_number();
371			let period = StoragePeriod::<T>::get();
372			let target_number = number.saturating_sub(period);
373			ensure!(!target_number.is_zero(), Error::<T>::UnexpectedProof);
374			let transactions =
375				Transactions::<T>::get(target_number).ok_or(Error::<T>::MissingStateData)?;
376
377			// Verify the proof with a "random" chunk (randomness is based on the parent hash).
378			let parent_hash = frame_system::Pallet::<T>::parent_hash();
379			Self::verify_chunk_proof(proof, parent_hash.as_ref(), transactions.to_vec())?;
380			ProofChecked::<T>::put(true);
381			Self::deposit_event(Event::ProofChecked);
382			Ok(().into())
383		}
384	}
385
386	#[pallet::event]
387	#[pallet::generate_deposit(pub(super) fn deposit_event)]
388	pub enum Event<T: Config> {
389		/// Stored data under specified index.
390		Stored { index: u32, content_hash: ContentHash },
391		/// Renewed data under specified index.
392		Renewed { index: u32, content_hash: ContentHash },
393		/// Storage proof was successfully checked.
394		ProofChecked,
395		/// An account `who` was authorized to store `bytes` bytes in `transactions` transactions.
396		AccountAuthorized { who: T::AccountId, transactions: u32, bytes: u64 },
397		/// An authorization for account `who` was refreshed.
398		AccountAuthorizationRefreshed { who: T::AccountId },
399		/// Authorization was given for a preimage of `content_hash` (not exceeding `max_size`) to
400		/// be stored by anyone.
401		PreimageAuthorized { content_hash: ContentHash, max_size: u64 },
402		/// An authorization for a preimage of `content_hash` was refreshed.
403		PreimageAuthorizationRefreshed { content_hash: ContentHash },
404		/// An expired account authorization was removed.
405		ExpiredAccountAuthorizationRemoved { who: T::AccountId },
406		/// An expired preimage authorization was removed.
407		ExpiredPreimageAuthorizationRemoved { content_hash: ContentHash },
408	}
409
410	/// Authorizations, keyed by scope.
411	#[pallet::storage]
412	pub(super) type Authorizations<T: Config> =
413		StorageMap<_, Blake2_128Concat, AuthorizationScopeFor<T>, AuthorizationFor<T>, OptionQuery>;
414
415	/// Collection of transaction metadata by block number.
416	#[pallet::storage]
417	pub type Transactions<T: Config> = StorageMap<
418		_,
419		Blake2_128Concat,
420		BlockNumberFor<T>,
421		BoundedVec<TransactionInfo, T::MaxBlockTransactions>,
422		OptionQuery,
423	>;
424
425	#[pallet::storage]
426	/// Storage fee per byte.
427	pub type ByteFee<T: Config> = StorageValue<_, BalanceOf<T>>;
428
429	#[pallet::storage]
430	/// Storage fee per transaction.
431	pub type EntryFee<T: Config> = StorageValue<_, BalanceOf<T>>;
432
433	/// Storage period for data in blocks. Should match `sp_storage_proof::DEFAULT_STORAGE_PERIOD`
434	/// for block authoring.
435	#[pallet::storage]
436	pub type StoragePeriod<T: Config> = StorageValue<_, BlockNumberFor<T>, ValueQuery>;
437
438	// Intermediates
439	#[pallet::storage]
440	pub type BlockTransactions<T: Config> =
441		StorageValue<_, BoundedVec<TransactionInfo, T::MaxBlockTransactions>, ValueQuery>;
442
443	/// Was the proof checked in this block?
444	#[pallet::storage]
445	pub type ProofChecked<T: Config> = StorageValue<_, bool, ValueQuery>;
446
447	#[pallet::genesis_config]
448	pub struct GenesisConfig<T: Config> {
449		pub byte_fee: BalanceOf<T>,
450		pub entry_fee: BalanceOf<T>,
451		pub storage_period: BlockNumberFor<T>,
452	}
453
454	impl<T: Config> Default for GenesisConfig<T> {
455		fn default() -> Self {
456			Self {
457				byte_fee: 10u32.into(),
458				entry_fee: 1000u32.into(),
459				storage_period: sp_transaction_storage_proof::DEFAULT_STORAGE_PERIOD.into(),
460			}
461		}
462	}
463
464	#[pallet::genesis_build]
465	impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
466		fn build(&self) {
467			ByteFee::<T>::put(&self.byte_fee);
468			EntryFee::<T>::put(&self.entry_fee);
469			StoragePeriod::<T>::put(&self.storage_period);
470		}
471	}
472
473	#[pallet::inherent]
474	impl<T: Config> ProvideInherent for Pallet<T> {
475		type Call = Call<T>;
476		type Error = InherentError;
477		const INHERENT_IDENTIFIER: InherentIdentifier = INHERENT_IDENTIFIER;
478
479		fn create_inherent(data: &InherentData) -> Option<Self::Call> {
480			let proof = data
481				.get_data::<TransactionStorageProof>(&Self::INHERENT_IDENTIFIER)
482				.unwrap_or(None);
483			proof.map(|proof| Call::check_proof { proof })
484		}
485
486		fn check_inherent(
487			_call: &Self::Call,
488			_data: &InherentData,
489		) -> result::Result<(), Self::Error> {
490			Ok(())
491		}
492
493		fn is_inherent(call: &Self::Call) -> bool {
494			matches!(call, Call::check_proof { .. })
495		}
496	}
497
498	impl<T: Config> Pallet<T> {
499		/// Get transaction storage information from outside of this pallet.
500		pub fn transaction_roots(
501			block: BlockNumberFor<T>,
502		) -> Option<BoundedVec<TransactionInfo, T::MaxBlockTransactions>> {
503			Transactions::<T>::get(block)
504		}
505		/// Get ByteFee storage information from outside of this pallet.
506		pub fn byte_fee() -> Option<BalanceOf<T>> {
507			ByteFee::<T>::get()
508		}
509		/// Get EntryFee storage information from outside of this pallet.
510		pub fn entry_fee() -> Option<BalanceOf<T>> {
511			EntryFee::<T>::get()
512		}
513
514		fn apply_fee(sender: T::AccountId, size: u32) -> DispatchResult {
515			let byte_fee = ByteFee::<T>::get().ok_or(Error::<T>::NotConfigured)?;
516			let entry_fee = EntryFee::<T>::get().ok_or(Error::<T>::NotConfigured)?;
517			let fee = byte_fee.saturating_mul(size.into()).saturating_add(entry_fee);
518			T::Currency::hold(&HoldReason::StorageFeeHold.into(), &sender, fee)?;
519			let (credit, _remainder) =
520				T::Currency::slash(&HoldReason::StorageFeeHold.into(), &sender, fee);
521			debug_assert!(_remainder.is_zero());
522			T::FeeDestination::on_unbalanced(credit);
523			Ok(())
524		}
525
526		/// Verifies that the provided proof corresponds to a randomly selected chunk from a list of
527		/// transactions.
528		pub(crate) fn verify_chunk_proof(
529			proof: TransactionStorageProof,
530			random_hash: &[u8],
531			infos: Vec<TransactionInfo>,
532		) -> Result<(), Error<T>> {
533			// Get the random chunk index - from all transactions in the block = [0..total_chunks).
534			let total_chunks: ChunkIndex = TransactionInfo::total_chunks(&infos);
535			ensure!(total_chunks != 0, Error::<T>::UnexpectedProof);
536			let selected_block_chunk_index = random_chunk(random_hash, total_chunks as _);
537
538			// Let's find the corresponding transaction and its "local" chunk index for "global"
539			// `selected_block_chunk_index`.
540			let (tx_info, tx_chunk_index) = {
541				// Binary search for the transaction that owns this `selected_block_chunk_index`
542				// chunk.
543				let tx_index = infos
544					.binary_search_by_key(&selected_block_chunk_index, |info| {
545						// Each `info.block_chunks` is cumulative count,
546						// so last chunk index = count - 1.
547						info.block_chunks.saturating_sub(1)
548					})
549					.unwrap_or_else(|tx_index| tx_index);
550
551				// Get the transaction and its local chunk index.
552				let tx_info = infos.get(tx_index).ok_or(Error::<T>::MissingStateData)?;
553				// We shouldn't reach this point; we rely on the fact that `fn store` does not allow
554				// empty transactions. Without this check, it would fail anyway below with
555				// `InvalidProof`.
556				ensure!(!tx_info.block_chunks.is_zero(), Error::<T>::EmptyTransaction);
557
558				// Convert a global chunk index into a transaction-local one.
559				let tx_chunks = num_chunks(tx_info.size);
560				let prev_chunks = tx_info.block_chunks - tx_chunks;
561				let tx_chunk_index = selected_block_chunk_index - prev_chunks;
562
563				(tx_info, tx_chunk_index)
564			};
565
566			// Verify the tx chunk proof.
567			ensure!(
568				sp_io::trie::blake2_256_verify_proof(
569					tx_info.chunk_root,
570					&proof.proof,
571					&encode_index(tx_chunk_index),
572					&proof.chunk,
573					sp_runtime::StateVersion::V1,
574				),
575				Error::<T>::InvalidProof
576			);
577
578			Ok(())
579		}
580	}
581}