pallet_transaction_storage/
lib.rs1#![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
50type 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
55pub use pallet::*;
57pub use weights::WeightInfo;
58
59pub const DEFAULT_MAX_TRANSACTION_SIZE: u32 = 8 * 1024 * 1024;
62pub const DEFAULT_MAX_BLOCK_TRANSACTIONS: u32 = 512;
63
64#[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_root: <BlakeTwo256 as Hash>::Output,
78 content_hash: <BlakeTwo256 as Hash>::Output,
80 size: u32,
82 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 #[pallet::composite_enum]
99 pub enum HoldReason {
100 StorageFeeHold,
102 }
103
104 #[pallet::config]
105 pub trait Config: frame_system::Config {
106 #[allow(deprecated)]
108 type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
109 type RuntimeCall: Parameter
111 + Dispatchable<RuntimeOrigin = Self::RuntimeOrigin>
112 + GetDispatchInfo
113 + From<frame_system::Call<Self>>;
114 type Currency: Mutate<Self::AccountId>
116 + MutateHold<Self::AccountId, Reason = Self::RuntimeHoldReason>
117 + Balanced<Self::AccountId>;
118 type RuntimeHoldReason: From<HoldReason>;
120 type FeeDestination: OnUnbalanced<CreditOf<Self>>;
122 type WeightInfo: WeightInfo;
124 type MaxBlockTransactions: Get<u32>;
126 type MaxTransactionSize: Get<u32>;
128 }
129
130 #[pallet::error]
131 pub enum Error<T> {
132 NotConfigured,
134 RenewedNotFound,
136 EmptyTransaction,
138 UnexpectedProof,
140 InvalidProof,
142 MissingProof,
144 MissingStateData,
146 DoubleCheck,
148 ProofNotChecked,
150 TransactionTooLarge,
152 TooManyTransactions,
154 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 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 T::DbWeight::get().reads_writes(2, 4)
174 }
175
176 fn on_finalize(n: BlockNumberFor<T>) {
177 assert!(
178 ProofChecked::<T>::take() || {
179 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 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 #[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 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 #[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 #[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 { index: u32 },
348 Renewed { index: u32 },
350 ProofChecked,
352 }
353
354 #[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 #[pallet::storage]
366 pub type ChunkCount<T: Config> =
367 StorageMap<_, Blake2_128Concat, BlockNumberFor<T>, u32, ValueQuery>;
368
369 #[pallet::storage]
370 pub type ByteFee<T: Config> = StorageValue<_, BalanceOf<T>>;
372
373 #[pallet::storage]
374 pub type EntryFee<T: Config> = StorageValue<_, BalanceOf<T>>;
376
377 #[pallet::storage]
380 pub type StoragePeriod<T: Config> = StorageValue<_, BlockNumberFor<T>, ValueQuery>;
381
382 #[pallet::storage]
384 pub type BlockTransactions<T: Config> =
385 StorageValue<_, BoundedVec<TransactionInfo, T::MaxBlockTransactions>, ValueQuery>;
386
387 #[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 pub fn transaction_roots(
445 block: BlockNumberFor<T>,
446 ) -> Option<BoundedVec<TransactionInfo, T::MaxBlockTransactions>> {
447 Transactions::<T>::get(block)
448 }
449 pub fn byte_fee() -> Option<BalanceOf<T>> {
451 ByteFee::<T>::get()
452 }
453 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}