1#![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
51type 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
56pub use pallet::*;
58pub use weights::WeightInfo;
59
60pub const DEFAULT_RETENTION_PERIOD: u32 = 100800;
62
63pub const DEFAULT_MAX_TRANSACTION_SIZE: u32 = 8 * 1024 * 1024;
67pub const DEFAULT_MAX_BLOCK_TRANSACTIONS: u32 = 512;
68
69pub const IMPOSSIBLE: InvalidTransaction = InvalidTransaction::Custom(0);
71pub const BAD_DATA_SIZE: InvalidTransaction = InvalidTransaction::Custom(1);
73pub const RENEWED_NOT_FOUND: InvalidTransaction = InvalidTransaction::Custom(2);
75pub const AUTHORIZATION_NOT_FOUND: InvalidTransaction = InvalidTransaction::Custom(3);
77pub const AUTHORIZATION_NOT_EXPIRED: InvalidTransaction = InvalidTransaction::Custom(4);
79
80#[derive(PartialEq, Eq, Debug, Encode, Decode, scale_info::TypeInfo, MaxEncodedLen)]
82pub struct AuthorizationExtent {
83 pub transactions: u32,
85 pub bytes: u64,
87}
88
89type ContentHash = [u8; 32];
91
92#[derive(Encode, Decode, scale_info::TypeInfo, MaxEncodedLen)]
94enum AuthorizationScope<AccountId> {
95 Account(AccountId),
97 Preimage(ContentHash),
99}
100
101type AuthorizationScopeFor<T> = AuthorizationScope<<T as frame_system::Config>::AccountId>;
102
103#[derive(Encode, Decode, scale_info::TypeInfo, MaxEncodedLen)]
105struct Authorization<BlockNumber> {
106 extent: AuthorizationExtent,
108 expiration: BlockNumber,
110}
111
112type AuthorizationFor<T> = Authorization<BlockNumberFor<T>>;
113
114#[derive(Encode, Decode, Clone, Debug, PartialEq, Eq, scale_info::TypeInfo, MaxEncodedLen)]
116pub struct TransactionInfo {
117 chunk_root: <BlakeTwo256 as Hash>::Output,
119 content_hash: <BlakeTwo256 as Hash>::Output,
121 size: u32,
123 block_chunks: ChunkIndex,
129}
130
131impl TransactionInfo {
132 pub fn total_chunks(txs: &[TransactionInfo]) -> ChunkIndex {
136 txs.last().map_or(0, |t| t.block_chunks)
137 }
138}
139
140#[frame_support::pallet]
141pub mod pallet {
142 use super::*;
143 use frame_support::pallet_prelude::*;
144 use frame_system::pallet_prelude::*;
145
146 #[pallet::composite_enum]
148 pub enum HoldReason {
149 StorageFeeHold,
151 }
152
153 #[pallet::config]
154 pub trait Config: frame_system::Config {
155 #[allow(deprecated)]
157 type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
158 type RuntimeCall: Parameter
160 + Dispatchable<RuntimeOrigin = Self::RuntimeOrigin>
161 + GetDispatchInfo
162 + From<frame_system::Call<Self>>;
163 type Currency: Mutate<Self::AccountId>
165 + MutateHold<Self::AccountId, Reason = Self::RuntimeHoldReason>
166 + Balanced<Self::AccountId>;
167 type RuntimeHoldReason: From<HoldReason>;
169 type FeeDestination: OnUnbalanced<CreditOf<Self>>;
171 type WeightInfo: WeightInfo;
173 #[pallet::constant]
175 type MaxBlockTransactions: Get<u32>;
176 #[pallet::constant]
178 type MaxTransactionSize: Get<u32>;
179 }
180
181 #[pallet::error]
182 pub enum Error<T> {
183 BadContext,
185 BadDataSize,
187 TooManyTransactions,
189 NotConfigured,
191 RenewedNotFound,
193 UnexpectedProof,
195 InvalidProof,
197 MissingProof,
199 MissingStateData,
201 DoubleCheck,
203 ProofNotChecked,
205 AuthorizationNotFound,
207 AuthorizationNotExpired,
209 }
210
211 #[pallet::pallet]
212 pub struct Pallet<T>(_);
213
214 #[pallet::hooks]
215 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
216 fn on_initialize(n: BlockNumberFor<T>) -> Weight {
217 let mut weight = Weight::zero();
219 let db_weight = T::DbWeight::get();
220
221 weight.saturating_accrue(db_weight.reads(1));
224 let period = Self::retention_period();
225 let obsolete = n.saturating_sub(period.saturating_add(One::one()));
226 if obsolete > Zero::zero() {
227 weight.saturating_accrue(db_weight.writes(1));
228 Transactions::<T>::remove(obsolete);
229 }
230
231 weight.saturating_accrue(db_weight.reads_writes(3, 1));
233 weight
234 }
235
236 fn on_finalize(n: BlockNumberFor<T>) {
237 assert!(
238 ProofChecked::<T>::take() || {
239 let number = frame_system::Pallet::<T>::block_number();
241 let period = Self::retention_period();
242 let target_number = number.saturating_sub(period);
243
244 target_number.is_zero() || {
245 !Transactions::<T>::contains_key(target_number)
248 }
249 },
250 "Storage proof must be checked once in the block"
251 );
252
253 let transactions = BlockTransactions::<T>::take();
255 let total_chunks = TransactionInfo::total_chunks(&transactions);
256 if total_chunks != 0 {
257 Transactions::<T>::insert(n, transactions);
258 }
259 }
260
261 fn integrity_test() {
262 assert!(
263 !T::MaxBlockTransactions::get().is_zero(),
264 "MaxTransactionSize must be greater than zero"
265 );
266 assert!(
267 !T::MaxTransactionSize::get().is_zero(),
268 "MaxTransactionSize must be greater than zero"
269 );
270 let default_period = DEFAULT_RETENTION_PERIOD.into();
271 let retention_period = GenesisConfig::<T>::default().retention_period;
272 assert_eq!(
273 retention_period, default_period,
274 "GenesisConfig.retention_period must match DEFAULT_RETENTION_PERIOD"
275 );
276 }
277 }
278
279 #[pallet::call]
280 impl<T: Config> Pallet<T> {
281 #[pallet::call_index(0)]
291 #[pallet::weight(T::WeightInfo::store(data.len() as u32))]
292 pub fn store(origin: OriginFor<T>, data: Vec<u8>) -> DispatchResult {
293 Self::ensure_data_size_ok(data.len())?;
297 let sender = ensure_signed(origin)?;
298 Self::apply_fee(sender, data.len() as u32)?;
299
300 let chunks: Vec<_> = data.chunks(CHUNK_SIZE).map(|c| c.to_vec()).collect();
302 let chunk_count = chunks.len() as u32;
303 debug_assert_eq!(chunk_count, num_chunks(data.len() as u32));
304 let root = sp_io::trie::blake2_256_ordered_root(chunks, sp_runtime::StateVersion::V1);
305
306 let content_hash = sp_io::hashing::blake2_256(&data);
307 let extrinsic_index =
308 frame_system::Pallet::<T>::extrinsic_index().ok_or(Error::<T>::BadContext)?;
309 sp_io::transaction_index::index(extrinsic_index, data.len() as u32, content_hash);
310
311 let mut index = 0;
312 BlockTransactions::<T>::mutate(|transactions| {
313 if transactions.len() + 1 > T::MaxBlockTransactions::get() as usize {
314 return Err(Error::<T>::TooManyTransactions);
315 }
316 let total_chunks = TransactionInfo::total_chunks(&transactions) + chunk_count;
317 index = transactions.len() as u32;
318 transactions
319 .try_push(TransactionInfo {
320 chunk_root: root,
321 size: data.len() as u32,
322 content_hash: content_hash.into(),
323 block_chunks: total_chunks,
324 })
325 .map_err(|_| Error::<T>::TooManyTransactions)
326 })?;
327 Self::deposit_event(Event::Stored { index, content_hash });
328 Ok(())
329 }
330
331 #[pallet::call_index(1)]
341 #[pallet::weight(T::WeightInfo::renew())]
342 pub fn renew(
343 origin: OriginFor<T>,
344 block: BlockNumberFor<T>,
345 index: u32,
346 ) -> DispatchResultWithPostInfo {
347 let sender = ensure_signed(origin)?;
348 let info = Self::transaction_info(block, index).ok_or(Error::<T>::RenewedNotFound)?;
349
350 Self::ensure_data_size_ok(info.size as usize)?;
354
355 let extrinsic_index =
356 frame_system::Pallet::<T>::extrinsic_index().ok_or(Error::<T>::BadContext)?;
357 Self::apply_fee(sender, info.size)?;
358 let content_hash = info.content_hash.into();
359 sp_io::transaction_index::renew(extrinsic_index, content_hash);
360
361 let mut index = 0;
362 BlockTransactions::<T>::mutate(|transactions| {
363 if transactions.len() + 1 > T::MaxBlockTransactions::get() as usize {
364 return Err(Error::<T>::TooManyTransactions);
365 }
366 let chunks = num_chunks(info.size);
367 let total_chunks = TransactionInfo::total_chunks(&transactions) + chunks;
368 index = transactions.len() as u32;
369 transactions
370 .try_push(TransactionInfo {
371 chunk_root: info.chunk_root,
372 size: info.size,
373 content_hash: info.content_hash,
374 block_chunks: total_chunks,
375 })
376 .map_err(|_| Error::<T>::TooManyTransactions)
377 })?;
378 Self::deposit_event(Event::Renewed { index, content_hash });
379 Ok(().into())
380 }
381
382 #[pallet::call_index(2)]
390 #[pallet::weight((T::WeightInfo::check_proof_max(), DispatchClass::Mandatory))]
391 pub fn check_proof(
392 origin: OriginFor<T>,
393 proof: TransactionStorageProof,
394 ) -> DispatchResultWithPostInfo {
395 ensure_none(origin)?;
396 ensure!(!ProofChecked::<T>::get(), Error::<T>::DoubleCheck);
397
398 let number = frame_system::Pallet::<T>::block_number();
400 let period = Self::retention_period();
401 let target_number = number.saturating_sub(period);
402 ensure!(!target_number.is_zero(), Error::<T>::UnexpectedProof);
403 let transactions =
404 Transactions::<T>::get(target_number).ok_or(Error::<T>::MissingStateData)?;
405
406 let parent_hash = frame_system::Pallet::<T>::parent_hash();
408 Self::verify_chunk_proof(proof, parent_hash.as_ref(), transactions.to_vec())?;
409 ProofChecked::<T>::put(true);
410 Self::deposit_event(Event::ProofChecked);
411 Ok(().into())
412 }
413 }
414
415 #[pallet::event]
416 #[pallet::generate_deposit(pub(super) fn deposit_event)]
417 pub enum Event<T: Config> {
418 Stored { index: u32, content_hash: ContentHash },
420 Renewed { index: u32, content_hash: ContentHash },
422 ProofChecked,
424 AccountAuthorized { who: T::AccountId, transactions: u32, bytes: u64 },
426 AccountAuthorizationRefreshed { who: T::AccountId },
428 PreimageAuthorized { content_hash: ContentHash, max_size: u64 },
431 PreimageAuthorizationRefreshed { content_hash: ContentHash },
433 ExpiredAccountAuthorizationRemoved { who: T::AccountId },
435 ExpiredPreimageAuthorizationRemoved { content_hash: ContentHash },
437 }
438
439 #[pallet::storage]
441 pub(super) type Authorizations<T: Config> =
442 StorageMap<_, Blake2_128Concat, AuthorizationScopeFor<T>, AuthorizationFor<T>, OptionQuery>;
443
444 #[pallet::storage]
446 pub type Transactions<T: Config> = StorageMap<
447 _,
448 Blake2_128Concat,
449 BlockNumberFor<T>,
450 BoundedVec<TransactionInfo, T::MaxBlockTransactions>,
451 OptionQuery,
452 >;
453
454 #[pallet::storage]
455 pub type ByteFee<T: Config> = StorageValue<_, BalanceOf<T>>;
457
458 #[pallet::storage]
459 pub type EntryFee<T: Config> = StorageValue<_, BalanceOf<T>>;
461
462 #[pallet::storage]
468 pub type RetentionPeriod<T: Config> = StorageValue<_, BlockNumberFor<T>, ValueQuery>;
469
470 #[pallet::storage]
472 pub type BlockTransactions<T: Config> =
473 StorageValue<_, BoundedVec<TransactionInfo, T::MaxBlockTransactions>, ValueQuery>;
474
475 #[pallet::storage]
477 pub type ProofChecked<T: Config> = StorageValue<_, bool, ValueQuery>;
478
479 #[pallet::genesis_config]
480 pub struct GenesisConfig<T: Config> {
481 pub byte_fee: BalanceOf<T>,
482 pub entry_fee: BalanceOf<T>,
483 pub retention_period: BlockNumberFor<T>,
484 }
485
486 impl<T: Config> Default for GenesisConfig<T> {
487 fn default() -> Self {
488 Self {
489 byte_fee: 10u32.into(),
490 entry_fee: 1000u32.into(),
491 retention_period: DEFAULT_RETENTION_PERIOD.into(),
492 }
493 }
494 }
495
496 #[pallet::genesis_build]
497 impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
498 fn build(&self) {
499 ByteFee::<T>::put(self.byte_fee);
500 EntryFee::<T>::put(self.entry_fee);
501 RetentionPeriod::<T>::put(self.retention_period);
502 }
503 }
504
505 #[pallet::inherent]
506 impl<T: Config> ProvideInherent for Pallet<T> {
507 type Call = Call<T>;
508 type Error = InherentError;
509 const INHERENT_IDENTIFIER: InherentIdentifier = INHERENT_IDENTIFIER;
510
511 fn create_inherent(data: &InherentData) -> Option<Self::Call> {
512 let proof = data
513 .get_data::<TransactionStorageProof>(&Self::INHERENT_IDENTIFIER)
514 .unwrap_or(None);
515 proof.map(|proof| Call::check_proof { proof })
516 }
517
518 fn check_inherent(_call: &Self::Call, _data: &InherentData) -> Result<(), Self::Error> {
519 Ok(())
520 }
521
522 fn is_inherent(call: &Self::Call) -> bool {
523 matches!(call, Call::check_proof { .. })
524 }
525 }
526
527 impl<T: Config> Pallet<T> {
528 pub fn transaction_roots(
530 block: BlockNumberFor<T>,
531 ) -> Option<BoundedVec<TransactionInfo, T::MaxBlockTransactions>> {
532 Transactions::<T>::get(block)
533 }
534
535 pub fn indexed_transactions(
536 block: BlockNumberFor<T>,
537 ) -> alloc::vec::Vec<sp_transaction_storage_proof::IndexedTransactionInfo> {
538 const RAW_CID_CODEC: sp_transaction_storage_proof::CidCodec = 0x55;
539
540 Transactions::<T>::get(block)
541 .map(|txs| {
542 txs.into_iter()
543 .map(|tx| sp_transaction_storage_proof::IndexedTransactionInfo {
544 content_hash: tx.content_hash.into(),
545 size: tx.size,
546 hashing: sp_transaction_storage_proof::HashingAlgorithm::Blake2b256,
547 cid_codec: RAW_CID_CODEC,
548 extrinsic_index: u32::MAX,
549 })
550 .collect()
551 })
552 .unwrap_or_default()
553 }
554
555 pub fn byte_fee() -> Option<BalanceOf<T>> {
557 ByteFee::<T>::get()
558 }
559 pub fn entry_fee() -> Option<BalanceOf<T>> {
561 EntryFee::<T>::get()
562 }
563 pub fn retention_period() -> BlockNumberFor<T> {
565 RetentionPeriod::<T>::get()
566 }
567
568 fn apply_fee(sender: T::AccountId, size: u32) -> DispatchResult {
569 let byte_fee = ByteFee::<T>::get().ok_or(Error::<T>::NotConfigured)?;
570 let entry_fee = EntryFee::<T>::get().ok_or(Error::<T>::NotConfigured)?;
571 let fee = byte_fee.saturating_mul(size.into()).saturating_add(entry_fee);
572 T::Currency::hold(&HoldReason::StorageFeeHold.into(), &sender, fee)?;
573 let (credit, _remainder) =
574 T::Currency::slash(&HoldReason::StorageFeeHold.into(), &sender, fee);
575 debug_assert!(_remainder.is_zero());
576 T::FeeDestination::on_unbalanced(credit);
577 Ok(())
578 }
579
580 fn data_size_ok(size: usize) -> bool {
582 (size > 0) && (size <= T::MaxTransactionSize::get() as usize)
583 }
584
585 fn ensure_data_size_ok(size: usize) -> Result<(), Error<T>> {
587 ensure!(Self::data_size_ok(size), Error::<T>::BadDataSize);
588 Ok(())
589 }
590
591 fn transaction_info(
593 block_number: BlockNumberFor<T>,
594 index: u32,
595 ) -> Option<TransactionInfo> {
596 let transactions = Transactions::<T>::get(block_number)?;
597 transactions.into_iter().nth(index as usize)
598 }
599
600 pub(crate) fn verify_chunk_proof(
603 proof: TransactionStorageProof,
604 random_hash: &[u8],
605 infos: Vec<TransactionInfo>,
606 ) -> Result<(), Error<T>> {
607 let total_chunks: ChunkIndex = TransactionInfo::total_chunks(&infos);
609 ensure!(total_chunks != 0, Error::<T>::UnexpectedProof);
610 let selected_block_chunk_index = random_chunk(random_hash, total_chunks);
611
612 let (tx_info, tx_chunk_index) = {
615 let tx_index = infos
618 .binary_search_by_key(&selected_block_chunk_index, |info| {
619 info.block_chunks.saturating_sub(1)
622 })
623 .unwrap_or_else(|tx_index| tx_index);
624
625 let tx_info = infos.get(tx_index).ok_or(Error::<T>::MissingStateData)?;
627 ensure!(!tx_info.block_chunks.is_zero(), Error::<T>::BadDataSize);
631
632 let tx_chunks = num_chunks(tx_info.size);
634 let prev_chunks = tx_info.block_chunks - tx_chunks;
635 let tx_chunk_index = selected_block_chunk_index - prev_chunks;
636
637 (tx_info, tx_chunk_index)
638 };
639
640 ensure!(
642 sp_io::trie::blake2_256_verify_proof(
643 tx_info.chunk_root,
644 &proof.proof,
645 &encode_index(tx_chunk_index),
646 &proof.chunk,
647 sp_runtime::StateVersion::V1,
648 ),
649 Error::<T>::InvalidProof
650 );
651
652 Ok(())
653 }
654 }
655}