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, 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
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 type MaxBlockTransactions: Get<u32>;
172 type MaxTransactionSize: Get<u32>;
174 }
175
176 #[pallet::error]
177 pub enum Error<T> {
178 BadContext,
180 BadDataSize,
182 TooManyTransactions,
184 NotConfigured,
186 RenewedNotFound,
188 EmptyTransaction,
190 UnexpectedProof,
192 InvalidProof,
194 MissingProof,
196 MissingStateData,
198 DoubleCheck,
200 ProofNotChecked,
202 TransactionTooLarge,
204 AuthorizationNotFound,
206 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 let mut weight = Weight::zero();
218 let db_weight = T::DbWeight::get();
219
220 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 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 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 !Transactions::<T>::contains_key(target_number)
247 }
248 },
249 "Storage proof must be checked once in the block"
250 );
251 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 #[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 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 #[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 #[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 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 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 { index: u32, content_hash: ContentHash },
391 Renewed { index: u32, content_hash: ContentHash },
393 ProofChecked,
395 AccountAuthorized { who: T::AccountId, transactions: u32, bytes: u64 },
397 AccountAuthorizationRefreshed { who: T::AccountId },
399 PreimageAuthorized { content_hash: ContentHash, max_size: u64 },
402 PreimageAuthorizationRefreshed { content_hash: ContentHash },
404 ExpiredAccountAuthorizationRemoved { who: T::AccountId },
406 ExpiredPreimageAuthorizationRemoved { content_hash: ContentHash },
408 }
409
410 #[pallet::storage]
412 pub(super) type Authorizations<T: Config> =
413 StorageMap<_, Blake2_128Concat, AuthorizationScopeFor<T>, AuthorizationFor<T>, OptionQuery>;
414
415 #[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 pub type ByteFee<T: Config> = StorageValue<_, BalanceOf<T>>;
428
429 #[pallet::storage]
430 pub type EntryFee<T: Config> = StorageValue<_, BalanceOf<T>>;
432
433 #[pallet::storage]
436 pub type StoragePeriod<T: Config> = StorageValue<_, BlockNumberFor<T>, ValueQuery>;
437
438 #[pallet::storage]
440 pub type BlockTransactions<T: Config> =
441 StorageValue<_, BoundedVec<TransactionInfo, T::MaxBlockTransactions>, ValueQuery>;
442
443 #[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 pub fn transaction_roots(
501 block: BlockNumberFor<T>,
502 ) -> Option<BoundedVec<TransactionInfo, T::MaxBlockTransactions>> {
503 Transactions::<T>::get(block)
504 }
505 pub fn byte_fee() -> Option<BalanceOf<T>> {
507 ByteFee::<T>::get()
508 }
509 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 pub(crate) fn verify_chunk_proof(
529 proof: TransactionStorageProof,
530 random_hash: &[u8],
531 infos: Vec<TransactionInfo>,
532 ) -> Result<(), Error<T>> {
533 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 (tx_info, tx_chunk_index) = {
541 let tx_index = infos
544 .binary_search_by_key(&selected_block_chunk_index, |info| {
545 info.block_chunks.saturating_sub(1)
548 })
549 .unwrap_or_else(|tx_index| tx_index);
550
551 let tx_info = infos.get(tx_index).ok_or(Error::<T>::MissingStateData)?;
553 ensure!(!tx_info.block_chunks.is_zero(), Error::<T>::EmptyTransaction);
557
558 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 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}