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_MAX_TRANSACTION_SIZE: u32 = 8 * 1024 * 1024;
64pub const DEFAULT_MAX_BLOCK_TRANSACTIONS: u32 = 512;
65
66pub const IMPOSSIBLE: InvalidTransaction = InvalidTransaction::Custom(0);
68pub const BAD_DATA_SIZE: InvalidTransaction = InvalidTransaction::Custom(1);
70pub const RENEWED_NOT_FOUND: InvalidTransaction = InvalidTransaction::Custom(2);
72pub const AUTHORIZATION_NOT_FOUND: InvalidTransaction = InvalidTransaction::Custom(3);
74pub const AUTHORIZATION_NOT_EXPIRED: InvalidTransaction = InvalidTransaction::Custom(4);
76
77#[derive(PartialEq, Eq, Debug, Encode, Decode, scale_info::TypeInfo, MaxEncodedLen)]
79pub struct AuthorizationExtent {
80 pub transactions: u32,
82 pub bytes: u64,
84}
85
86type ContentHash = [u8; 32];
88
89#[derive(Encode, Decode, scale_info::TypeInfo, MaxEncodedLen)]
91enum AuthorizationScope<AccountId> {
92 Account(AccountId),
94 Preimage(ContentHash),
96}
97
98type AuthorizationScopeFor<T> = AuthorizationScope<<T as frame_system::Config>::AccountId>;
99
100#[derive(Encode, Decode, scale_info::TypeInfo, MaxEncodedLen)]
102struct Authorization<BlockNumber> {
103 extent: AuthorizationExtent,
105 expiration: BlockNumber,
107}
108
109type AuthorizationFor<T> = Authorization<BlockNumberFor<T>>;
110
111#[derive(Encode, Decode, Clone, Debug, PartialEq, Eq, scale_info::TypeInfo, MaxEncodedLen)]
113pub struct TransactionInfo {
114 chunk_root: <BlakeTwo256 as Hash>::Output,
116 content_hash: <BlakeTwo256 as Hash>::Output,
118 size: u32,
120 block_chunks: ChunkIndex,
126}
127
128impl TransactionInfo {
129 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 #[pallet::composite_enum]
145 pub enum HoldReason {
146 StorageFeeHold,
148 }
149
150 #[pallet::config]
151 pub trait Config: frame_system::Config {
152 #[allow(deprecated)]
154 type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
155 type RuntimeCall: Parameter
157 + Dispatchable<RuntimeOrigin = Self::RuntimeOrigin>
158 + GetDispatchInfo
159 + From<frame_system::Call<Self>>;
160 type Currency: Mutate<Self::AccountId>
162 + MutateHold<Self::AccountId, Reason = Self::RuntimeHoldReason>
163 + Balanced<Self::AccountId>;
164 type RuntimeHoldReason: From<HoldReason>;
166 type FeeDestination: OnUnbalanced<CreditOf<Self>>;
168 type WeightInfo: WeightInfo;
170 #[pallet::constant]
172 type MaxBlockTransactions: Get<u32>;
173 #[pallet::constant]
175 type MaxTransactionSize: Get<u32>;
176 }
177
178 #[pallet::error]
179 pub enum Error<T> {
180 BadContext,
182 BadDataSize,
184 TooManyTransactions,
186 NotConfigured,
188 RenewedNotFound,
190 UnexpectedProof,
192 InvalidProof,
194 MissingProof,
196 MissingStateData,
198 DoubleCheck,
200 ProofNotChecked,
202 AuthorizationNotFound,
204 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 let mut weight = Weight::zero();
216 let db_weight = T::DbWeight::get();
217
218 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 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 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 !Transactions::<T>::contains_key(target_number)
245 }
246 },
247 "Storage proof must be checked once in the block"
248 );
249
250 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 #[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 Self::ensure_data_size_ok(data.len())?;
294 let sender = ensure_signed(origin)?;
295 Self::apply_fee(sender, data.len() as u32)?;
296
297 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 #[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 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 #[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 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 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 { index: u32, content_hash: ContentHash },
417 Renewed { index: u32, content_hash: ContentHash },
419 ProofChecked,
421 AccountAuthorized { who: T::AccountId, transactions: u32, bytes: u64 },
423 AccountAuthorizationRefreshed { who: T::AccountId },
425 PreimageAuthorized { content_hash: ContentHash, max_size: u64 },
428 PreimageAuthorizationRefreshed { content_hash: ContentHash },
430 ExpiredAccountAuthorizationRemoved { who: T::AccountId },
432 ExpiredPreimageAuthorizationRemoved { content_hash: ContentHash },
434 }
435
436 #[pallet::storage]
438 pub(super) type Authorizations<T: Config> =
439 StorageMap<_, Blake2_128Concat, AuthorizationScopeFor<T>, AuthorizationFor<T>, OptionQuery>;
440
441 #[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 pub type ByteFee<T: Config> = StorageValue<_, BalanceOf<T>>;
454
455 #[pallet::storage]
456 pub type EntryFee<T: Config> = StorageValue<_, BalanceOf<T>>;
458
459 #[pallet::storage]
462 pub type StoragePeriod<T: Config> = StorageValue<_, BlockNumberFor<T>, ValueQuery>;
463
464 #[pallet::storage]
466 pub type BlockTransactions<T: Config> =
467 StorageValue<_, BoundedVec<TransactionInfo, T::MaxBlockTransactions>, ValueQuery>;
468
469 #[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 pub fn transaction_roots(
524 block: BlockNumberFor<T>,
525 ) -> Option<BoundedVec<TransactionInfo, T::MaxBlockTransactions>> {
526 Transactions::<T>::get(block)
527 }
528 pub fn byte_fee() -> Option<BalanceOf<T>> {
530 ByteFee::<T>::get()
531 }
532 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 fn data_size_ok(size: usize) -> bool {
551 (size > 0) && (size <= T::MaxTransactionSize::get() as usize)
552 }
553
554 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 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 pub(crate) fn verify_chunk_proof(
572 proof: TransactionStorageProof,
573 random_hash: &[u8],
574 infos: Vec<TransactionInfo>,
575 ) -> Result<(), Error<T>> {
576 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 (tx_info, tx_chunk_index) = {
584 let tx_index = infos
587 .binary_search_by_key(&selected_block_chunk_index, |info| {
588 info.block_chunks.saturating_sub(1)
591 })
592 .unwrap_or_else(|tx_index| tx_index);
593
594 let tx_info = infos.get(tx_index).ok_or(Error::<T>::MissingStateData)?;
596 ensure!(!tx_info.block_chunks.is_zero(), Error::<T>::BadDataSize);
600
601 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 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}