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::fmt::Debug;
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-bulletin-chain/issues/139 - 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		#[pallet::constant]
172		type MaxBlockTransactions: Get<u32>;
173		/// Maximum data set in a single transaction in bytes.
174		#[pallet::constant]
175		type MaxTransactionSize: Get<u32>;
176	}
177
178	#[pallet::error]
179	pub enum Error<T> {
180		/// Attempted to call `store`/`renew` outside of block execution.
181		BadContext,
182		/// Data size is not in the allowed range.
183		BadDataSize,
184		/// Too many transactions in the block.
185		TooManyTransactions,
186		/// Invalid configuration.
187		NotConfigured,
188		/// Renewed extrinsic is not found.
189		RenewedNotFound,
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		/// Authorization was not found.
203		AuthorizationNotFound,
204		/// Authorization has not expired.
205		AuthorizationNotExpired,
206	}
207
208	#[pallet::pallet]
209	pub struct Pallet<T>(_);
210
211	#[pallet::hooks]
212	impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
213		fn on_initialize(n: BlockNumberFor<T>) -> Weight {
214			// TODO: https://github.com/paritytech/polkadot-sdk/issues/10203 - Replace this with benchmarked weights.
215			let mut weight = Weight::zero();
216			let db_weight = T::DbWeight::get();
217
218			// Drop obsolete roots. The proof for `obsolete` will be checked later
219			// in this block, so we drop `obsolete` - 1.
220			weight.saturating_accrue(db_weight.reads(1));
221			let period = StoragePeriod::<T>::get();
222			let obsolete = n.saturating_sub(period.saturating_add(One::one()));
223			if obsolete > Zero::zero() {
224				weight.saturating_accrue(db_weight.writes(1));
225				Transactions::<T>::remove(obsolete);
226			}
227
228			// For `on_finalize`
229			weight.saturating_accrue(db_weight.reads_writes(3, 1));
230			weight
231		}
232
233		fn on_finalize(n: BlockNumberFor<T>) {
234			assert!(
235				ProofChecked::<T>::take() || {
236					// Proof is not required for early or empty blocks.
237					let number = frame_system::Pallet::<T>::block_number();
238					let period = StoragePeriod::<T>::get();
239					let target_number = number.saturating_sub(period);
240
241					target_number.is_zero() || {
242						// An empty block means no transactions were stored, relying on the fact
243						// below that we store transactions only if they contain chunks.
244						!Transactions::<T>::contains_key(target_number)
245					}
246				},
247				"Storage proof must be checked once in the block"
248			);
249
250			// Insert new transactions, iff they have chunks.
251			let transactions = BlockTransactions::<T>::take();
252			let total_chunks = TransactionInfo::total_chunks(&transactions);
253			if total_chunks != 0 {
254				Transactions::<T>::insert(n, transactions);
255			}
256		}
257
258		fn integrity_test() {
259			assert!(
260				!T::MaxBlockTransactions::get().is_zero(),
261				"MaxTransactionSize must be greater than zero"
262			);
263			assert!(
264				!T::MaxTransactionSize::get().is_zero(),
265				"MaxTransactionSize must be greater than zero"
266			);
267			let default_period = sp_transaction_storage_proof::DEFAULT_STORAGE_PERIOD.into();
268			let storage_period = GenesisConfig::<T>::default().storage_period;
269			assert_eq!(
270				storage_period, default_period,
271				"GenesisConfig.storage_period must match DEFAULT_STORAGE_PERIOD"
272			);
273		}
274	}
275
276	#[pallet::call]
277	impl<T: Config> Pallet<T> {
278		/// Index and store data off chain. Minimum data size is 1 byte, maximum is
279		/// `MaxTransactionSize`. Data will be removed after `StoragePeriod` blocks, unless `renew`
280		/// is called.
281		///
282		/// Emits [`Stored`](Event::Stored) when successful.
283		///
284		/// ## Complexity
285		///
286		/// O(n*log(n)) of data size, as all data is pushed to an in-memory trie.
287		#[pallet::call_index(0)]
288		#[pallet::weight(T::WeightInfo::store(data.len() as u32))]
289		pub fn store(origin: OriginFor<T>, data: Vec<u8>) -> DispatchResult {
290			// In the case of a regular unsigned transaction, this should have been checked by
291			// pre_dispatch. In the case of a regular signed transaction, this should have been
292			// checked by pre_dispatch_signed.
293			Self::ensure_data_size_ok(data.len())?;
294			let sender = ensure_signed(origin)?;
295			Self::apply_fee(sender, data.len() as u32)?;
296
297			// Chunk data and compute storage root
298			let chunks: Vec<_> = data.chunks(CHUNK_SIZE).map(|c| c.to_vec()).collect();
299			let chunk_count = chunks.len() as u32;
300			debug_assert_eq!(chunk_count, num_chunks(data.len() as u32));
301			let root = sp_io::trie::blake2_256_ordered_root(chunks, sp_runtime::StateVersion::V1);
302
303			let content_hash = sp_io::hashing::blake2_256(&data);
304			let extrinsic_index =
305				frame_system::Pallet::<T>::extrinsic_index().ok_or(Error::<T>::BadContext)?;
306			sp_io::transaction_index::index(extrinsic_index, data.len() as u32, content_hash);
307
308			let mut index = 0;
309			BlockTransactions::<T>::mutate(|transactions| {
310				if transactions.len() + 1 > T::MaxBlockTransactions::get() as usize {
311					return Err(Error::<T>::TooManyTransactions);
312				}
313				let total_chunks = TransactionInfo::total_chunks(&transactions) + chunk_count;
314				index = transactions.len() as u32;
315				transactions
316					.try_push(TransactionInfo {
317						chunk_root: root,
318						size: data.len() as u32,
319						content_hash: content_hash.into(),
320						block_chunks: total_chunks,
321					})
322					.map_err(|_| Error::<T>::TooManyTransactions)
323			})?;
324			Self::deposit_event(Event::Stored { index, content_hash });
325			Ok(())
326		}
327
328		/// Renew previously stored data. Parameters are the block number that contains previous
329		/// `store` or `renew` call and transaction index within that block. Transaction index is
330		/// emitted in the `Stored` or `Renewed` event. Applies same fees as `store`.
331		///
332		/// Emits [`Renewed`](Event::Renewed) when successful.
333		///
334		/// ## Complexity
335		///
336		/// O(1).
337		#[pallet::call_index(1)]
338		#[pallet::weight(T::WeightInfo::renew())]
339		pub fn renew(
340			origin: OriginFor<T>,
341			block: BlockNumberFor<T>,
342			index: u32,
343		) -> DispatchResultWithPostInfo {
344			let sender = ensure_signed(origin)?;
345			let info = Self::transaction_info(block, index).ok_or(Error::<T>::RenewedNotFound)?;
346
347			// In the case of a regular unsigned transaction, this should have been checked by
348			// pre_dispatch. In the case of a regular signed transaction, this should have been
349			// checked by pre_dispatch_signed.
350			Self::ensure_data_size_ok(info.size as usize)?;
351
352			let extrinsic_index =
353				frame_system::Pallet::<T>::extrinsic_index().ok_or(Error::<T>::BadContext)?;
354			Self::apply_fee(sender, info.size)?;
355			let content_hash = info.content_hash.into();
356			sp_io::transaction_index::renew(extrinsic_index, content_hash);
357
358			let mut index = 0;
359			BlockTransactions::<T>::mutate(|transactions| {
360				if transactions.len() + 1 > T::MaxBlockTransactions::get() as usize {
361					return Err(Error::<T>::TooManyTransactions);
362				}
363				let chunks = num_chunks(info.size);
364				let total_chunks = TransactionInfo::total_chunks(&transactions) + chunks;
365				index = transactions.len() as u32;
366				transactions
367					.try_push(TransactionInfo {
368						chunk_root: info.chunk_root,
369						size: info.size,
370						content_hash: info.content_hash,
371						block_chunks: total_chunks,
372					})
373					.map_err(|_| Error::<T>::TooManyTransactions)
374			})?;
375			Self::deposit_event(Event::Renewed { index, content_hash });
376			Ok(().into())
377		}
378
379		/// Check storage proof for block number `block_number() - StoragePeriod`. If such a block
380		/// does not exist, the proof is expected to be `None`.
381		///
382		/// ## Complexity
383		///
384		/// Linear w.r.t the number of indexed transactions in the proved block for random probing.
385		/// There's a DB read for each transaction.
386		#[pallet::call_index(2)]
387		#[pallet::weight((T::WeightInfo::check_proof_max(), DispatchClass::Mandatory))]
388		pub fn check_proof(
389			origin: OriginFor<T>,
390			proof: TransactionStorageProof,
391		) -> DispatchResultWithPostInfo {
392			ensure_none(origin)?;
393			ensure!(!ProofChecked::<T>::get(), Error::<T>::DoubleCheck);
394
395			// Get the target block metadata.
396			let number = frame_system::Pallet::<T>::block_number();
397			let period = StoragePeriod::<T>::get();
398			let target_number = number.saturating_sub(period);
399			ensure!(!target_number.is_zero(), Error::<T>::UnexpectedProof);
400			let transactions =
401				Transactions::<T>::get(target_number).ok_or(Error::<T>::MissingStateData)?;
402
403			// Verify the proof with a "random" chunk (randomness is based on the parent hash).
404			let parent_hash = frame_system::Pallet::<T>::parent_hash();
405			Self::verify_chunk_proof(proof, parent_hash.as_ref(), transactions.to_vec())?;
406			ProofChecked::<T>::put(true);
407			Self::deposit_event(Event::ProofChecked);
408			Ok(().into())
409		}
410	}
411
412	#[pallet::event]
413	#[pallet::generate_deposit(pub(super) fn deposit_event)]
414	pub enum Event<T: Config> {
415		/// Stored data under specified index.
416		Stored { index: u32, content_hash: ContentHash },
417		/// Renewed data under specified index.
418		Renewed { index: u32, content_hash: ContentHash },
419		/// Storage proof was successfully checked.
420		ProofChecked,
421		/// An account `who` was authorized to store `bytes` bytes in `transactions` transactions.
422		AccountAuthorized { who: T::AccountId, transactions: u32, bytes: u64 },
423		/// An authorization for account `who` was refreshed.
424		AccountAuthorizationRefreshed { who: T::AccountId },
425		/// Authorization was given for a preimage of `content_hash` (not exceeding `max_size`) to
426		/// be stored by anyone.
427		PreimageAuthorized { content_hash: ContentHash, max_size: u64 },
428		/// An authorization for a preimage of `content_hash` was refreshed.
429		PreimageAuthorizationRefreshed { content_hash: ContentHash },
430		/// An expired account authorization was removed.
431		ExpiredAccountAuthorizationRemoved { who: T::AccountId },
432		/// An expired preimage authorization was removed.
433		ExpiredPreimageAuthorizationRemoved { content_hash: ContentHash },
434	}
435
436	/// Authorizations, keyed by scope.
437	#[pallet::storage]
438	pub(super) type Authorizations<T: Config> =
439		StorageMap<_, Blake2_128Concat, AuthorizationScopeFor<T>, AuthorizationFor<T>, OptionQuery>;
440
441	/// Collection of transaction metadata by block number.
442	#[pallet::storage]
443	pub type Transactions<T: Config> = StorageMap<
444		_,
445		Blake2_128Concat,
446		BlockNumberFor<T>,
447		BoundedVec<TransactionInfo, T::MaxBlockTransactions>,
448		OptionQuery,
449	>;
450
451	#[pallet::storage]
452	/// Storage fee per byte.
453	pub type ByteFee<T: Config> = StorageValue<_, BalanceOf<T>>;
454
455	#[pallet::storage]
456	/// Storage fee per transaction.
457	pub type EntryFee<T: Config> = StorageValue<_, BalanceOf<T>>;
458
459	/// Storage period for data in blocks. Should match `sp_storage_proof::DEFAULT_STORAGE_PERIOD`
460	/// for block authoring.
461	#[pallet::storage]
462	pub type StoragePeriod<T: Config> = StorageValue<_, BlockNumberFor<T>, ValueQuery>;
463
464	// Intermediates
465	#[pallet::storage]
466	pub type BlockTransactions<T: Config> =
467		StorageValue<_, BoundedVec<TransactionInfo, T::MaxBlockTransactions>, ValueQuery>;
468
469	/// Was the proof checked in this block?
470	#[pallet::storage]
471	pub type ProofChecked<T: Config> = StorageValue<_, bool, ValueQuery>;
472
473	#[pallet::genesis_config]
474	pub struct GenesisConfig<T: Config> {
475		pub byte_fee: BalanceOf<T>,
476		pub entry_fee: BalanceOf<T>,
477		pub storage_period: BlockNumberFor<T>,
478	}
479
480	impl<T: Config> Default for GenesisConfig<T> {
481		fn default() -> Self {
482			Self {
483				byte_fee: 10u32.into(),
484				entry_fee: 1000u32.into(),
485				storage_period: sp_transaction_storage_proof::DEFAULT_STORAGE_PERIOD.into(),
486			}
487		}
488	}
489
490	#[pallet::genesis_build]
491	impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
492		fn build(&self) {
493			ByteFee::<T>::put(self.byte_fee);
494			EntryFee::<T>::put(self.entry_fee);
495			StoragePeriod::<T>::put(self.storage_period);
496		}
497	}
498
499	#[pallet::inherent]
500	impl<T: Config> ProvideInherent for Pallet<T> {
501		type Call = Call<T>;
502		type Error = InherentError;
503		const INHERENT_IDENTIFIER: InherentIdentifier = INHERENT_IDENTIFIER;
504
505		fn create_inherent(data: &InherentData) -> Option<Self::Call> {
506			let proof = data
507				.get_data::<TransactionStorageProof>(&Self::INHERENT_IDENTIFIER)
508				.unwrap_or(None);
509			proof.map(|proof| Call::check_proof { proof })
510		}
511
512		fn check_inherent(_call: &Self::Call, _data: &InherentData) -> Result<(), Self::Error> {
513			Ok(())
514		}
515
516		fn is_inherent(call: &Self::Call) -> bool {
517			matches!(call, Call::check_proof { .. })
518		}
519	}
520
521	impl<T: Config> Pallet<T> {
522		/// Get transaction storage information from outside of this pallet.
523		pub fn transaction_roots(
524			block: BlockNumberFor<T>,
525		) -> Option<BoundedVec<TransactionInfo, T::MaxBlockTransactions>> {
526			Transactions::<T>::get(block)
527		}
528		/// Get ByteFee storage information from outside of this pallet.
529		pub fn byte_fee() -> Option<BalanceOf<T>> {
530			ByteFee::<T>::get()
531		}
532		/// Get EntryFee storage information from outside of this pallet.
533		pub fn entry_fee() -> Option<BalanceOf<T>> {
534			EntryFee::<T>::get()
535		}
536
537		fn apply_fee(sender: T::AccountId, size: u32) -> DispatchResult {
538			let byte_fee = ByteFee::<T>::get().ok_or(Error::<T>::NotConfigured)?;
539			let entry_fee = EntryFee::<T>::get().ok_or(Error::<T>::NotConfigured)?;
540			let fee = byte_fee.saturating_mul(size.into()).saturating_add(entry_fee);
541			T::Currency::hold(&HoldReason::StorageFeeHold.into(), &sender, fee)?;
542			let (credit, _remainder) =
543				T::Currency::slash(&HoldReason::StorageFeeHold.into(), &sender, fee);
544			debug_assert!(_remainder.is_zero());
545			T::FeeDestination::on_unbalanced(credit);
546			Ok(())
547		}
548
549		/// Returns `true` if a blob of the given size can be stored.
550		fn data_size_ok(size: usize) -> bool {
551			(size > 0) && (size <= T::MaxTransactionSize::get() as usize)
552		}
553
554		/// Ensures that the given data size is valid for storage.
555		fn ensure_data_size_ok(size: usize) -> Result<(), Error<T>> {
556			ensure!(Self::data_size_ok(size), Error::<T>::BadDataSize);
557			Ok(())
558		}
559
560		/// Returns the [`TransactionInfo`] for the specified store/renew transaction.
561		fn transaction_info(
562			block_number: BlockNumberFor<T>,
563			index: u32,
564		) -> Option<TransactionInfo> {
565			let transactions = Transactions::<T>::get(block_number)?;
566			transactions.into_iter().nth(index as usize)
567		}
568
569		/// Verifies that the provided proof corresponds to a randomly selected chunk from a list of
570		/// transactions.
571		pub(crate) fn verify_chunk_proof(
572			proof: TransactionStorageProof,
573			random_hash: &[u8],
574			infos: Vec<TransactionInfo>,
575		) -> Result<(), Error<T>> {
576			// Get the random chunk index - from all transactions in the block = [0..total_chunks).
577			let total_chunks: ChunkIndex = TransactionInfo::total_chunks(&infos);
578			ensure!(total_chunks != 0, Error::<T>::UnexpectedProof);
579			let selected_block_chunk_index = random_chunk(random_hash, total_chunks);
580
581			// Let's find the corresponding transaction and its "local" chunk index for "global"
582			// `selected_block_chunk_index`.
583			let (tx_info, tx_chunk_index) = {
584				// Binary search for the transaction that owns this `selected_block_chunk_index`
585				// chunk.
586				let tx_index = infos
587					.binary_search_by_key(&selected_block_chunk_index, |info| {
588						// Each `info.block_chunks` is cumulative count,
589						// so last chunk index = count - 1.
590						info.block_chunks.saturating_sub(1)
591					})
592					.unwrap_or_else(|tx_index| tx_index);
593
594				// Get the transaction and its local chunk index.
595				let tx_info = infos.get(tx_index).ok_or(Error::<T>::MissingStateData)?;
596				// We shouldn't reach this point; we rely on the fact that `fn store` does not allow
597				// empty transactions. Without this check, it would fail anyway below with
598				// `InvalidProof`.
599				ensure!(!tx_info.block_chunks.is_zero(), Error::<T>::BadDataSize);
600
601				// Convert a global chunk index into a transaction-local one.
602				let tx_chunks = num_chunks(tx_info.size);
603				let prev_chunks = tx_info.block_chunks - tx_chunks;
604				let tx_chunk_index = selected_block_chunk_index - prev_chunks;
605
606				(tx_info, tx_chunk_index)
607			};
608
609			// Verify the tx chunk proof.
610			ensure!(
611				sp_io::trie::blake2_256_verify_proof(
612					tx_info.chunk_root,
613					&proof.proof,
614					&encode_index(tx_chunk_index),
615					&proof.chunk,
616					sp_runtime::StateVersion::V1,
617				),
618				Error::<T>::InvalidProof
619			);
620
621			Ok(())
622		}
623	}
624}