pallet_stateful_storage/
lib.rs

1//! MSA-centric Storage for [`PayloadLocation::Paginated`] and [`PayloadLocation::Itemized`]
2//!
3//! ## Quick Links
4//! - [Configuration: `Config`](Config)
5//! - [Extrinsics: `Call`](Call)
6//! - [Runtime API: `StatefulStorageRuntimeApi`](../pallet_stateful_storage_runtime_api/trait.StatefulStorageRuntimeApi.html)
7//! - [Custom RPC API: `StatefulStorageApiServer`](../pallet_stateful_storage_rpc/trait.StatefulStorageApiServer.html)
8//! - [Event Enum: `Event`](Event)
9//! - [Error Enum: `Error`](Error)
10#![doc = include_str!("../README.md")]
11//!
12//! ## Terminology
13//! - [`Page`](../pallet_stateful_storage/types/struct.Page.html): Block of on-chain data of a fixed size, which is the underlying type for Itemized and Paginated storage.
14//! - [`ItemizedPage`](../pallet_stateful_storage/types/type.ItemizedPage.html): A page containing itemized data
15//! - [`PaginatedPage`](../pallet_stateful_storage/types/type.PaginatedPage.html): A page containing paginated data
16//!
17
18// Substrate macros are tripping the clippy::expect_used lint.
19#![allow(clippy::expect_used)]
20// Ensure we're `no_std` when compiling for Wasm.
21#![cfg_attr(not(feature = "std"), no_std)]
22// Strong Documentation Lints
23#![deny(
24	rustdoc::broken_intra_doc_links,
25	rustdoc::missing_crate_level_docs,
26	rustdoc::invalid_codeblock_attributes,
27	missing_docs
28)]
29
30#[cfg(feature = "runtime-benchmarks")]
31mod benchmarking;
32#[cfg(any(feature = "runtime-benchmarks", test))]
33mod test_common;
34#[cfg(test)]
35mod tests;
36#[cfg(feature = "runtime-benchmarks")]
37use common_primitives::benchmarks::{MsaBenchmarkHelper, SchemaBenchmarkHelper};
38
39/// Migration module
40pub mod migration;
41mod stateful_child_tree;
42pub mod types;
43pub mod weights;
44
45extern crate alloc;
46extern crate core;
47
48use alloc::vec::Vec;
49
50use crate::{stateful_child_tree::StatefulChildTree, types::*};
51use common_primitives::{
52	msa::{DelegatorId, GrantValidator, MessageSourceId, MsaLookup, MsaValidator, ProviderId},
53	node::EIP712Encode,
54	schema::{IntentSetting, PayloadLocation, SchemaId, SchemaInfoResponse, SchemaProvider},
55	stateful_storage::{
56		ItemizedStoragePageResponse, ItemizedStoragePageResponseV2, ItemizedStorageResponseV2,
57		PageHash, PageId, PaginatedStorageResponse, PaginatedStorageResponseV2,
58	},
59};
60
61use common_primitives::schema::{IntentId, IntentResponse};
62use frame_support::{
63	dispatch::{DispatchInfo, DispatchResult},
64	ensure,
65	pallet_prelude::*,
66	traits::{Get, IsSubType},
67};
68use frame_system::pallet_prelude::*;
69pub use pallet::*;
70use sp_core::{bounded::BoundedVec, crypto::AccountId32};
71use sp_runtime::{
72	traits::{
73		AsSystemOriginSigner, Convert, DispatchInfoOf, Dispatchable, PostDispatchInfoOf,
74		TransactionExtension,
75	},
76	DispatchError, MultiSignature,
77};
78pub use weights::*;
79
80#[frame_support::pallet]
81pub mod pallet {
82	use super::*;
83	use core::fmt::Debug;
84
85	#[pallet::config]
86	pub trait Config: frame_system::Config {
87		/// The overarching event type.
88		#[allow(deprecated)]
89		type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
90
91		/// Weight information for extrinsics in this pallet.
92		type WeightInfo: WeightInfo;
93
94		/// A type that will supply MSA related information
95		type MsaInfoProvider: MsaLookup + MsaValidator<AccountId = Self::AccountId>;
96
97		/// A type that will validate schema grants
98		type SchemaGrantValidator: GrantValidator<IntentId, BlockNumberFor<Self>>;
99
100		/// A type that will supply schema related information.
101		type SchemaProvider: SchemaProvider<SchemaId>;
102
103		/// The maximum size of a page (in bytes) for an Itemized storage model
104		#[pallet::constant]
105		type MaxItemizedPageSizeBytes: Get<u32> + Default;
106
107		/// The maximum size of a page (in bytes) for a Paginated storage model
108		#[pallet::constant]
109		type MaxPaginatedPageSizeBytes: Get<u32> + Default;
110
111		/// The maximum size of a single item in an itemized storage model (in bytes)
112		#[pallet::constant]
113		type MaxItemizedBlobSizeBytes: Get<u32> + Clone + core::fmt::Debug + PartialEq;
114
115		/// The maximum number of pages in a Paginated storage model
116		#[pallet::constant]
117		type MaxPaginatedPageId: Get<u16>;
118
119		/// The maximum number of actions in itemized actions
120		#[pallet::constant]
121		type MaxItemizedActionsCount: Get<u32>;
122
123		#[cfg(feature = "runtime-benchmarks")]
124		/// A set of helper functions for benchmarking.
125		type MsaBenchmarkHelper: MsaBenchmarkHelper<Self::AccountId>;
126
127		#[cfg(feature = "runtime-benchmarks")]
128		/// A set of helper functions for benchmarking.
129		type SchemaBenchmarkHelper: SchemaBenchmarkHelper;
130
131		/// Hasher to use for MultipartKey
132		type KeyHasher: stateful_child_tree::MultipartKeyStorageHasher;
133
134		/// AccountId truncated to 32 bytes
135		type ConvertIntoAccountId32: Convert<Self::AccountId, AccountId32>;
136
137		/// The number of blocks that we allow for a signed payload to be valid. This is mainly used
138		/// to make sure a signed payload would not be replayable.
139		#[pallet::constant]
140		type MortalityWindowSize: Get<u32>;
141
142		/// How often to emit status events during a storage migration.
143		/// Try to make this larger than the number of message migrations that will fit
144		/// in a block by weight, as multiple of these events in a block is not really useful
145		/// or desireable.
146		type MigrateEmitEvery: Get<u32> + Clone + Debug;
147	}
148
149	// Simple declaration of the `Pallet` type. It is placeholder we use to implement traits and
150	// method.
151	#[pallet::pallet]
152	#[pallet::storage_version(STATEFUL_STORAGE_VERSION)]
153	pub struct Pallet<T>(_);
154
155	/// A temporary storage for migration
156	#[pallet::storage]
157	pub(super) type MigrationPageIndex<T: Config> = StorageValue<_, u32, ValueQuery>;
158
159	#[pallet::error]
160	pub enum Error<T> {
161		/// Page would exceed the highest allowable PageId
162		PageIdExceedsMaxAllowed,
163
164		/// Page size exceeds max allowable page size
165		PageExceedsMaxPageSizeBytes,
166
167		/// Invalid SchemaId or Schema not found
168		InvalidSchemaId,
169
170		/// Unsupported operation for schema
171		UnsupportedOperationForSchema,
172
173		/// Schema is not valid for storage model
174		SchemaPayloadLocationMismatch,
175
176		/// Invalid Message Source Account
177		InvalidMessageSourceAccount,
178
179		/// UnauthorizedDelegate
180		UnauthorizedDelegate,
181
182		/// Corrupted State
183		CorruptedState,
184
185		/// Invalid item action
186		InvalidItemAction,
187
188		/// Target page hash does not match current page hash
189		StalePageState,
190
191		/// Invalid Signature for payload
192		InvalidSignature,
193
194		/// The submitted proof has expired; the current block is less the expiration block
195		ProofHasExpired,
196
197		/// The submitted proof expiration block is too far in the future
198		ProofNotYetValid,
199
200		/// Page was written with an unsupported page storage version
201		UnsupportedPageVersion,
202
203		/// Invalid IntentId or Intent not found.
204		InvalidIntentId,
205
206		/// Specified payload location is not valid for the entity
207		PayloadLocationMismatch,
208	}
209
210	#[pallet::event]
211	#[pallet::generate_deposit(pub(super) fn deposit_event)]
212	pub enum Event<T: Config> {
213		/// An event for when an itemized storage is updated
214		ItemizedPageUpdated {
215			/// message source id of storage owner
216			msa_id: MessageSourceId,
217			/// schema related to the storage
218			intent_id: IntentId,
219			/// previous content hash before update
220			prev_content_hash: PageHash,
221			/// current content hash after update
222			curr_content_hash: PageHash,
223		},
224
225		/// An event for when an itemized storage is deleted
226		ItemizedPageDeleted {
227			/// message source id of storage owner
228			msa_id: MessageSourceId,
229			/// schema related to the storage
230			intent_id: IntentId,
231			/// previous content hash before removal
232			prev_content_hash: PageHash,
233		},
234
235		/// An event for when an paginated storage is updated
236		PaginatedPageUpdated {
237			/// message source id of storage owner
238			msa_id: MessageSourceId,
239			/// schema related to the storage
240			intent_id: IntentId,
241			/// id of updated page
242			page_id: PageId,
243			/// previous content hash before update
244			prev_content_hash: PageHash,
245			/// current content hash after update
246			curr_content_hash: PageHash,
247		},
248
249		/// An event for when an paginated storage is deleted
250		PaginatedPageDeleted {
251			/// message source id of storage owner
252			msa_id: MessageSourceId,
253			/// schema related to the storage
254			intent_id: IntentId,
255			/// id of updated page
256			page_id: PageId,
257			/// previous content hash before removal
258			prev_content_hash: PageHash,
259		},
260
261		/// An event to track storage migrations
262		StatefulPagesMigrated {
263			/// Last child trie prefix processed
264			last_trie: (u64, PayloadLocation),
265			/// Total number of pages migrated
266			total_page_count: u64,
267		},
268	}
269
270	#[pallet::call]
271	impl<T: Config> Pallet<T> {
272		/// Applies the Add or Delete Actions on the requested Itemized page.
273		/// This is treated as a transaction so either all actions succeed or none will be executed.
274		///
275		/// Note: if called by the state owner, call may succeed even on `SignatureRequired` schemas.
276		/// The fact that the entire (signed) transaction is submitted by the owner's keypair is
277		/// considered equivalent to supplying a separate signature. Note in that case that a delegate
278		/// submitting this extrinsic on behalf of a user would fail.
279		///
280		/// # Events
281		/// * [`Event::ItemizedPageUpdated`]
282		/// * [`Event::ItemizedPageDeleted`]
283		///
284		#[pallet::call_index(0)]
285		#[pallet::weight(
286            T::WeightInfo::apply_item_actions_delete(actions.len() as u32)
287            .max(T::WeightInfo::apply_item_actions_add(Pallet::<T>::sum_add_actions_bytes(actions)))
288        )]
289		pub fn apply_item_actions(
290			origin: OriginFor<T>,
291			#[pallet::compact] state_owner_msa_id: MessageSourceId,
292			#[pallet::compact] schema_id: SchemaId,
293			#[pallet::compact] target_hash: PageHash,
294			actions: BoundedVec<
295				ItemAction<T::MaxItemizedBlobSizeBytes>,
296				T::MaxItemizedActionsCount,
297			>,
298		) -> DispatchResult {
299			let key = ensure_signed(origin)?;
300			let schema = T::SchemaProvider::get_schema_info_by_id(schema_id)
301				.ok_or(Error::<T>::InvalidSchemaId)?;
302			let is_pruning = actions.iter().any(|a| matches!(a, ItemAction::Delete { .. }));
303			let caller_msa_id =
304				Self::check_msa_and_grants(key, state_owner_msa_id, schema.intent_id)?;
305			let caller_is_state_owner = caller_msa_id == state_owner_msa_id;
306			Self::check_schema_for_write(
307				schema_id,
308				PayloadLocation::Itemized,
309				caller_is_state_owner,
310				is_pruning,
311			)?;
312			Self::update_itemized(
313				state_owner_msa_id,
314				schema.intent_id,
315				schema_id,
316				target_hash,
317				actions,
318			)?;
319			Ok(())
320		}
321
322		/// Creates or updates a Paginated page with new payload
323		///
324		/// Note: if called by the state owner, call may succeed even on `SignatureRequired` schemas.
325		/// The fact that the entire (signed) transaction is submitted by the owner's keypair is
326		/// considered equivalent to supplying a separate signature. Note in that case that a delegate
327		/// submitting this extrinsic on behalf of a user would fail.
328		///
329		/// # Events
330		/// * [`Event::PaginatedPageUpdated`]
331		///
332		#[pallet::call_index(1)]
333		#[pallet::weight(T::WeightInfo::upsert_page(payload.len() as u32))]
334		pub fn upsert_page(
335			origin: OriginFor<T>,
336			#[pallet::compact] state_owner_msa_id: MessageSourceId,
337			#[pallet::compact] schema_id: SchemaId,
338			#[pallet::compact] page_id: PageId,
339			#[pallet::compact] target_hash: PageHash,
340			payload: BoundedVec<u8, <T>::MaxPaginatedPageSizeBytes>,
341		) -> DispatchResult {
342			let provider_key = ensure_signed(origin)?;
343			ensure!(page_id <= T::MaxPaginatedPageId::get(), Error::<T>::PageIdExceedsMaxAllowed);
344			let schema = T::SchemaProvider::get_schema_info_by_id(schema_id)
345				.ok_or(Error::<T>::InvalidSchemaId)?;
346			let caller_msa_id =
347				Self::check_msa_and_grants(provider_key, state_owner_msa_id, schema.intent_id)?;
348			let caller_is_state_owner = caller_msa_id == state_owner_msa_id;
349			Self::check_schema_for_write(
350				schema_id,
351				PayloadLocation::Paginated,
352				caller_is_state_owner,
353				false,
354			)?;
355			Self::update_paginated(
356				state_owner_msa_id,
357				schema.intent_id,
358				schema_id,
359				page_id,
360				target_hash,
361				PaginatedPage::<T>::from(payload),
362			)?;
363			Ok(())
364		}
365
366		/// Deletes a Paginated storage
367		///
368		/// Note: if called by the state owner, call may succeed even on `SignatureRequired` schemas.
369		/// The fact that the entire (signed) transaction is submitted by the owner's keypair is
370		/// considered equivalent to supplying a separate signature. Note in that case that a delegate
371		/// submitting this extrinsic on behalf of a user would fail.
372		///
373		/// # Events
374		/// * [`Event::PaginatedPageDeleted`]
375		///
376		#[pallet::call_index(2)]
377		#[pallet::weight(T::WeightInfo::delete_page())]
378		pub fn delete_page(
379			origin: OriginFor<T>,
380			#[pallet::compact] state_owner_msa_id: MessageSourceId,
381			#[pallet::compact] schema_id: SchemaId,
382			#[pallet::compact] page_id: PageId,
383			#[pallet::compact] target_hash: PageHash,
384		) -> DispatchResult {
385			let provider_key = ensure_signed(origin)?;
386			ensure!(page_id <= T::MaxPaginatedPageId::get(), Error::<T>::PageIdExceedsMaxAllowed);
387			let schema = T::SchemaProvider::get_schema_info_by_id(schema_id)
388				.ok_or(Error::<T>::InvalidSchemaId)?;
389			let caller_msa_id =
390				Self::check_msa_and_grants(provider_key, state_owner_msa_id, schema.intent_id)?;
391			let caller_is_state_owner = caller_msa_id == state_owner_msa_id;
392			let SchemaInfoResponse { intent_id, .. } = Self::check_schema_for_write(
393				schema_id,
394				PayloadLocation::Paginated,
395				caller_is_state_owner,
396				true,
397			)?;
398			Self::delete_paginated(state_owner_msa_id, intent_id, page_id, target_hash)
399		}
400
401		// REMOVED apply_item_actions_with_signature() at call index 3
402		// REMOVED upsert_page_with_signature() at call index 4
403		// REMOVED delete_page_with_signature() at call index 5
404
405		/// Applies the Add or Delete Actions on the requested Itemized page that requires signature
406		/// since the signature of delegator is checked there is no need for delegation validation
407		/// This is treated as a transaction so either all actions succeed or none will be executed.
408		///
409		/// # Events
410		/// * [`Event::ItemizedPageUpdated`]
411		/// * [`Event::ItemizedPageDeleted`]
412		///
413		#[pallet::call_index(6)]
414		#[pallet::weight(
415            T::WeightInfo::apply_item_actions_with_signature_v2_delete(payload.actions.len() as u32)
416            .max(T::WeightInfo::apply_item_actions_with_signature_v2_add(Pallet::<T>::sum_add_actions_bytes(&payload.actions)))
417        )]
418		pub fn apply_item_actions_with_signature_v2(
419			origin: OriginFor<T>,
420			delegator_key: T::AccountId,
421			proof: MultiSignature,
422			payload: ItemizedSignaturePayloadV2<T>,
423		) -> DispatchResult {
424			ensure_signed(origin)?;
425
426			let is_pruning = payload.actions.iter().any(|a| matches!(a, ItemAction::Delete { .. }));
427			Self::check_payload_expiration(
428				frame_system::Pallet::<T>::block_number(),
429				payload.expiration,
430			)?;
431			Self::check_signature(&proof, &delegator_key.clone(), &payload)?;
432			let state_owner_msa_id = T::MsaInfoProvider::ensure_valid_msa_key(&delegator_key)
433				.map_err(|_| Error::<T>::InvalidMessageSourceAccount)?;
434			let schema = Self::check_schema_for_write(
435				payload.schema_id,
436				PayloadLocation::Itemized,
437				true,
438				is_pruning,
439			)?;
440			Self::update_itemized(
441				state_owner_msa_id,
442				schema.intent_id,
443				payload.schema_id,
444				payload.target_hash,
445				payload.actions,
446			)?;
447			Ok(())
448		}
449
450		/// Creates or updates Paginated storage with new payload that requires signature.
451		/// Since the signature of delegator is checked, there is no need for delegation validation.
452		///
453		/// # Events
454		/// * [`Event::PaginatedPageUpdated`]
455		///
456		#[pallet::call_index(7)]
457		#[pallet::weight(
458            T::WeightInfo::upsert_page_with_signature_v2(payload.payload.len() as u32)
459        )]
460		pub fn upsert_page_with_signature_v2(
461			origin: OriginFor<T>,
462			delegator_key: T::AccountId,
463			proof: MultiSignature,
464			payload: PaginatedUpsertSignaturePayloadV2<T>,
465		) -> DispatchResult {
466			ensure_signed(origin)?;
467			ensure!(
468				payload.page_id <= T::MaxPaginatedPageId::get(),
469				Error::<T>::PageIdExceedsMaxAllowed
470			);
471			Self::check_payload_expiration(
472				frame_system::Pallet::<T>::block_number(),
473				payload.expiration,
474			)?;
475			Self::check_signature(&proof, &delegator_key.clone(), &payload)?;
476			let state_owner_msa_id = T::MsaInfoProvider::ensure_valid_msa_key(&delegator_key)
477				.map_err(|_| Error::<T>::InvalidMessageSourceAccount)?;
478			let schema = Self::check_schema_for_write(
479				payload.schema_id,
480				PayloadLocation::Paginated,
481				true,
482				false,
483			)?;
484			Self::update_paginated(
485				state_owner_msa_id,
486				schema.intent_id,
487				payload.schema_id,
488				payload.page_id,
489				payload.target_hash,
490				PaginatedPage::<T>::from(payload.payload),
491			)?;
492			Ok(())
493		}
494
495		/// Deletes a Paginated storage that requires signature
496		/// since the signature of delegator is checked there is no need for delegation validation
497		///
498		/// # Events
499		/// * [`Event::PaginatedPageDeleted`]
500		///
501		#[pallet::call_index(8)]
502		#[pallet::weight(T::WeightInfo::delete_page_with_signature_v2())]
503		pub fn delete_page_with_signature_v2(
504			origin: OriginFor<T>,
505			delegator_key: T::AccountId,
506			proof: MultiSignature,
507			payload: PaginatedDeleteSignaturePayloadV2<T>,
508		) -> DispatchResult {
509			ensure_signed(origin)?;
510			ensure!(
511				payload.page_id <= T::MaxPaginatedPageId::get(),
512				Error::<T>::PageIdExceedsMaxAllowed
513			);
514			Self::check_payload_expiration(
515				frame_system::Pallet::<T>::block_number(),
516				payload.expiration,
517			)?;
518			Self::check_signature(&proof, &delegator_key.clone(), &payload)?;
519			let state_owner_msa_id = T::MsaInfoProvider::ensure_valid_msa_key(&delegator_key)
520				.map_err(|_| Error::<T>::InvalidMessageSourceAccount)?;
521			let schema = Self::check_schema_for_write(
522				payload.schema_id,
523				PayloadLocation::Paginated,
524				true,
525				true,
526			)?;
527			Self::delete_paginated(
528				state_owner_msa_id,
529				schema.intent_id,
530				payload.page_id,
531				payload.target_hash,
532			)?;
533			Ok(())
534		}
535	}
536}
537
538impl<T: Config> Pallet<T> {
539	/// Sums the total bytes over all item actions
540	pub fn sum_add_actions_bytes(
541		actions: &BoundedVec<
542			ItemAction<<T as Config>::MaxItemizedBlobSizeBytes>,
543			<T as Config>::MaxItemizedActionsCount,
544		>,
545	) -> u32 {
546		actions.iter().fold(0, |acc, a| {
547			acc.saturating_add(match a {
548				ItemAction::Add { data } => data.len() as u32,
549				_ => 0,
550			})
551		})
552	}
553
554	/// This function returns all the paginated storage associated with `msa_id` and `schema_id`
555	///
556	/// Warning: since this function iterates over all the potential keys it should never called
557	/// from runtime.
558	pub fn get_paginated_storage_v1(
559		msa_id: MessageSourceId,
560		schema_id: SchemaId,
561	) -> Result<Vec<PaginatedStorageResponse>, DispatchError> {
562		let schema = T::SchemaProvider::get_schema_info_by_id(schema_id)
563			.ok_or(Error::<T>::InvalidSchemaId)?;
564		Self::get_paginated_storage(msa_id, schema.intent_id).map(|v| {
565			v.into_iter()
566				.map(<PaginatedStorageResponseV2 as Into<PaginatedStorageResponse>>::into)
567				.collect()
568		})
569	}
570
571	/// This function returns all the paginated storage associated with `msa_id` and `schema_id`
572	///
573	/// Warning: since this function iterates over all the potential keys it should never called
574	/// from runtime.
575	pub fn get_paginated_storage(
576		msa_id: MessageSourceId,
577		intent_id: IntentId,
578	) -> Result<Vec<PaginatedStorageResponseV2>, DispatchError> {
579		Self::check_intent_for_read(intent_id, PayloadLocation::Paginated)?;
580		let prefix: PaginatedPrefixKey = (intent_id,);
581		Ok(StatefulChildTree::<T::KeyHasher>::prefix_iterator::<
582			PaginatedPage<T>,
583			PaginatedKey,
584			PaginatedPrefixKey,
585		>(&msa_id, PALLET_STORAGE_PREFIX, PAGINATED_STORAGE_PREFIX, &prefix)
586		.map(|(k, v)| {
587			let content_hash = v.get_hash();
588			let nonce = v.nonce;
589			PaginatedStorageResponseV2::new(
590				k.1,
591				msa_id,
592				intent_id,
593				v.schema_id.unwrap_or_default(),
594				content_hash,
595				nonce,
596				v.data.into_inner(),
597			)
598		})
599		.collect())
600	}
601
602	/// This function returns all the itemized storage associated with `msa_id` and `schema_id`
603	pub fn get_itemized_storage_v1(
604		msa_id: MessageSourceId,
605		schema_id: SchemaId,
606	) -> Result<ItemizedStoragePageResponse, DispatchError> {
607		let schema = T::SchemaProvider::get_schema_info_by_id(schema_id)
608			.ok_or(Error::<T>::InvalidSchemaId)?;
609		Self::get_itemized_storage(msa_id, schema.intent_id).map(|v| v.into())
610	}
611
612	/// This function returns all the itemized storage associated with `msa_id` and `schema_id`
613	pub fn get_itemized_storage(
614		msa_id: MessageSourceId,
615		intent_id: IntentId,
616	) -> Result<ItemizedStoragePageResponseV2, DispatchError> {
617		Self::check_intent_for_read(intent_id, PayloadLocation::Itemized)?;
618		let page = Self::get_itemized_page_for(msa_id, intent_id)?.unwrap_or_default();
619		let items: Vec<ItemizedStorageResponseV2> =
620			ItemizedOperations::<T>::try_parse(&page, false)
621				.map_err(|_| Error::<T>::CorruptedState)?
622				.items
623				.iter()
624				.map(|(key, v)| {
625					ItemizedStorageResponseV2::new(
626						*key,
627						match v.header {
628							// V1 items should not exist post-migration, but theoretically any items without a schema_id would have the same
629							// numeric schema_id as the intent_id.
630							ItemHeader::V1 { .. } => intent_id as SchemaId,
631							ItemHeader::V2 { schema_id, .. } => schema_id,
632						},
633						v.payload.to_vec(),
634					)
635				})
636				.collect();
637		Ok(ItemizedStoragePageResponseV2::new(
638			msa_id,
639			intent_id,
640			page.get_hash(),
641			page.nonce,
642			items,
643		))
644	}
645
646	/// This function checks to ensure `payload_expire_block` is in a valid range
647	///
648	/// # Errors
649	/// * [`Error::ProofHasExpired`]
650	/// * [`Error::ProofNotYetValid`]
651	///
652	pub fn check_payload_expiration(
653		current_block: BlockNumberFor<T>,
654		payload_expire_block: BlockNumberFor<T>,
655	) -> Result<(), DispatchError> {
656		ensure!(payload_expire_block > current_block, Error::<T>::ProofHasExpired);
657		let max_supported_signature_block = Self::mortality_block_limit(current_block);
658		ensure!(payload_expire_block < max_supported_signature_block, Error::<T>::ProofNotYetValid);
659		Ok(())
660	}
661
662	/// Verify the `signature` was signed by `signer` on `payload` by a wallet
663	/// Note the `wrap_binary_data` follows the Polkadot wallet pattern of wrapping with `<Byte>` tags.
664	///
665	/// # Errors
666	/// * [`Error::InvalidSignature`]
667	///
668	pub fn check_signature<P>(
669		signature: &MultiSignature,
670		signer: &T::AccountId,
671		payload: &P,
672	) -> DispatchResult
673	where
674		P: Encode + EIP712Encode,
675	{
676		let key = T::ConvertIntoAccountId32::convert(signer.clone());
677
678		ensure!(
679			common_runtime::signature::check_signature(signature, key, payload),
680			Error::<T>::InvalidSignature
681		);
682
683		Ok(())
684	}
685
686	/// The furthest in the future a mortality_block value is allowed
687	/// to be for current_block
688	/// This is calculated to be past the risk of a replay attack
689	fn mortality_block_limit(current_block: BlockNumberFor<T>) -> BlockNumberFor<T> {
690		current_block + BlockNumberFor::<T>::from(T::MortalityWindowSize::get())
691	}
692
693	/// Checks that the schema is valid for is action
694	///
695	/// # Errors
696	/// * [`Error::InvalidSchemaId`]
697	/// * [`Error::PayloadLocationMismatch`]
698	///
699	fn check_schema_for_read(
700		schema_id: SchemaId,
701		expected_payload_location: PayloadLocation,
702	) -> Result<SchemaInfoResponse, DispatchError> {
703		let schema = T::SchemaProvider::get_schema_info_by_id(schema_id)
704			.ok_or(Error::<T>::InvalidSchemaId)?;
705
706		// Ensure that the schema's payload location matches the expected location.
707		ensure!(
708			schema.payload_location == expected_payload_location,
709			Error::<T>::PayloadLocationMismatch
710		);
711
712		Ok(schema)
713	}
714
715	/// Checks that the Intent is valid for is action
716	///
717	/// # Errors
718	/// * [`Error::InvalidIntentId`]
719	/// * [`Error::PayloadLocationMismatch`]
720	///
721	fn check_intent_for_read(
722		intent_id: IntentId,
723		expected_payload_location: PayloadLocation,
724	) -> Result<IntentResponse, DispatchError> {
725		let intent =
726			T::SchemaProvider::get_intent_by_id(intent_id).ok_or(Error::<T>::InvalidIntentId)?;
727
728		// Ensure that the intent's payload location matches the expected location.
729		ensure!(
730			intent.payload_location == expected_payload_location,
731			Error::<T>::PayloadLocationMismatch
732		);
733
734		Ok(intent)
735	}
736
737	/// Checks that the schema is valid for is action
738	///
739	/// # Errors
740	/// * [`Error::InvalidSchemaId`]
741	/// * [`Error::PayloadLocationMismatch`]
742	/// * [`Error::UnsupportedOperationForSchema`]
743	///
744	fn check_schema_for_write(
745		schema_id: SchemaId,
746		expected_payload_location: PayloadLocation,
747		is_payload_signed: bool,
748		is_deleting: bool,
749	) -> Result<SchemaInfoResponse, DispatchError> {
750		let schema = Self::check_schema_for_read(schema_id, expected_payload_location)?;
751
752		// Ensure that the schema allows signed payloads.
753		// If so, calling extrinsic must be of signature type.
754		if schema.settings.contains(&IntentSetting::SignatureRequired) {
755			ensure!(is_payload_signed, Error::<T>::UnsupportedOperationForSchema);
756		}
757
758		// Ensure that the schema does not allow deletion for AppendOnly SchemaSetting.
759		if schema.settings.contains(&IntentSetting::AppendOnly) {
760			ensure!(!is_deleting, Error::<T>::UnsupportedOperationForSchema);
761		}
762
763		Ok(schema)
764	}
765
766	/// Checks that existence of Msa for certain key and if the grant is valid when the caller Msa
767	/// is different from the state owner Msa
768	///
769	/// # Errors
770	/// * [`Error::InvalidMessageSourceAccount`]
771	/// * [`Error::UnauthorizedDelegate`]
772	///
773	fn check_msa_and_grants(
774		key: T::AccountId,
775		state_owner_msa_id: MessageSourceId,
776		intent_id: IntentId,
777	) -> Result<MessageSourceId, DispatchError> {
778		let caller_msa_id = T::MsaInfoProvider::ensure_valid_msa_key(&key)
779			.map_err(|_| Error::<T>::InvalidMessageSourceAccount)?;
780
781		// if caller and owner are the same no delegation is needed
782		if caller_msa_id != state_owner_msa_id {
783			let current_block = frame_system::Pallet::<T>::block_number();
784			T::SchemaGrantValidator::ensure_valid_grant(
785				ProviderId(caller_msa_id),
786				DelegatorId(state_owner_msa_id),
787				intent_id,
788				current_block,
789			)
790			.map_err(|_| Error::<T>::UnauthorizedDelegate)?;
791		}
792
793		Ok(caller_msa_id)
794	}
795
796	/// Updates an itemized storage by applying provided actions and deposit events.
797	/// All items must
798	///
799	/// # Errors
800	/// * [`Error::StalePageState`] - Page content hash does not match on-chain hash.
801	/// * [`Error::CorruptedState`] - Unable to read the existing page data.
802	/// * [`Error::InvalidItemAction`] - Supplied action item is invalid.
803	///
804	///
805	/// # Events
806	/// * [`Event::ItemizedPageUpdated`]
807	/// * [`Event::ItemizedPageDeleted`]
808	///
809	fn update_itemized(
810		state_owner_msa_id: MessageSourceId,
811		intent_id: IntentId,
812		schema_id: SchemaId,
813		target_hash: PageHash,
814		actions: BoundedVec<ItemAction<T::MaxItemizedBlobSizeBytes>, T::MaxItemizedActionsCount>,
815	) -> DispatchResult {
816		let key: ItemizedKey = (intent_id,);
817		let existing_page =
818			Self::get_itemized_page_for(state_owner_msa_id, intent_id)?.unwrap_or_default();
819
820		let prev_content_hash = existing_page.get_hash();
821		ensure!(target_hash == prev_content_hash, Error::<T>::StalePageState);
822
823		let mut updated_page =
824			ItemizedOperations::<T>::apply_item_actions(&existing_page, schema_id, &actions[..])
825				.map_err(|e| match e {
826					PageError::ErrorParsing(err) => {
827						log::warn!(
828							"failed parsing Itemized msa={state_owner_msa_id:?} intent_id={intent_id:?} {err:?}",
829						);
830						Error::<T>::CorruptedState
831					},
832					_ => Error::<T>::InvalidItemAction,
833				})?;
834		updated_page.nonce = existing_page.nonce.wrapping_add(1);
835
836		match updated_page.is_empty() {
837			true => {
838				StatefulChildTree::<T::KeyHasher>::kill(
839					&state_owner_msa_id,
840					PALLET_STORAGE_PREFIX,
841					ITEMIZED_STORAGE_PREFIX,
842					&key,
843				);
844				Self::deposit_event(Event::ItemizedPageDeleted {
845					msa_id: state_owner_msa_id,
846					intent_id,
847					prev_content_hash,
848				});
849			},
850			false => {
851				StatefulChildTree::<T::KeyHasher>::write(
852					&state_owner_msa_id,
853					PALLET_STORAGE_PREFIX,
854					ITEMIZED_STORAGE_PREFIX,
855					&key,
856					&updated_page,
857				);
858				Self::deposit_event(Event::ItemizedPageUpdated {
859					msa_id: state_owner_msa_id,
860					intent_id,
861					curr_content_hash: updated_page.get_hash(),
862					prev_content_hash,
863				});
864			},
865		};
866		Ok(())
867	}
868
869	/// Updates a page from paginated storage by provided new page
870	///
871	/// # Events
872	/// * [`Event::PaginatedPageUpdated`]
873	///
874	fn update_paginated(
875		state_owner_msa_id: MessageSourceId,
876		intent_id: IntentId,
877		schema_id: SchemaId,
878		page_id: PageId,
879		target_hash: PageHash,
880		mut new_page: PaginatedPage<T>,
881	) -> DispatchResult {
882		let keys: PaginatedKey = (intent_id, page_id);
883		let existing_page: PaginatedPage<T> =
884			Self::get_paginated_page_for(state_owner_msa_id, intent_id, page_id)?
885				.unwrap_or_default();
886
887		let prev_content_hash: PageHash = existing_page.get_hash();
888		ensure!(target_hash == prev_content_hash, Error::<T>::StalePageState);
889
890		new_page.page_version = PageVersion::V2;
891		new_page.schema_id = Some(schema_id);
892		new_page.nonce = existing_page.nonce.wrapping_add(1);
893
894		StatefulChildTree::<T::KeyHasher>::write(
895			&state_owner_msa_id,
896			PALLET_STORAGE_PREFIX,
897			PAGINATED_STORAGE_PREFIX,
898			&keys,
899			&new_page,
900		);
901		Self::deposit_event(Event::PaginatedPageUpdated {
902			msa_id: state_owner_msa_id,
903			intent_id,
904			page_id,
905			curr_content_hash: new_page.get_hash(),
906			prev_content_hash,
907		});
908		Ok(())
909	}
910
911	/// Deletes a page from paginated storage
912	///
913	/// # Events
914	/// * [`Event::PaginatedPageDeleted`]
915	///
916	fn delete_paginated(
917		state_owner_msa_id: MessageSourceId,
918		intent_id: IntentId,
919		page_id: PageId,
920		target_hash: PageHash,
921	) -> DispatchResult {
922		let keys: PaginatedKey = (intent_id, page_id);
923		if let Some(existing_page) =
924			Self::get_paginated_page_for(state_owner_msa_id, intent_id, page_id)?
925		{
926			let prev_content_hash: PageHash = existing_page.get_hash();
927			ensure!(target_hash == prev_content_hash, Error::<T>::StalePageState);
928			StatefulChildTree::<T::KeyHasher>::kill(
929				&state_owner_msa_id,
930				PALLET_STORAGE_PREFIX,
931				PAGINATED_STORAGE_PREFIX,
932				&keys,
933			);
934			Self::deposit_event(Event::PaginatedPageDeleted {
935				msa_id: state_owner_msa_id,
936				intent_id,
937				page_id,
938				prev_content_hash,
939			});
940		}
941
942		Ok(())
943	}
944
945	/// Gets a paginated storage for desired parameters
946	pub fn get_paginated_page_for(
947		msa_id: MessageSourceId,
948		intent_id: IntentId,
949		page_id: PageId,
950	) -> Result<Option<PaginatedPage<T>>, DispatchError> {
951		let keys: PaginatedKey = (intent_id, page_id);
952		let page = StatefulChildTree::<T::KeyHasher>::try_read::<_, PaginatedPage<T>>(
953			&msa_id,
954			PALLET_STORAGE_PREFIX,
955			PAGINATED_STORAGE_PREFIX,
956			&keys,
957		)
958		.map_err(|_| Error::<T>::CorruptedState)?;
959
960		// Note: PageVersion::V1 is unsupported because no pages were ever written with that tag;
961		// hence it indicates some kind of corrupted state. In future, the pallet should be able
962		// to support reading pages with multiple versions.
963		if let Some(Page { page_version, .. }) = &page {
964			if *page_version != PageVersion::V2 {
965				return Err(Error::<T>::UnsupportedPageVersion.into());
966			}
967		};
968		Ok(page)
969	}
970
971	/// Gets an itemized storage for desired parameters
972	pub fn get_itemized_page_for(
973		msa_id: MessageSourceId,
974		intent_id: IntentId,
975	) -> Result<Option<ItemizedPage<T>>, DispatchError> {
976		let keys: ItemizedKey = (intent_id,);
977		let page = StatefulChildTree::<T::KeyHasher>::try_read::<_, ItemizedPage<T>>(
978			&msa_id,
979			PALLET_STORAGE_PREFIX,
980			ITEMIZED_STORAGE_PREFIX,
981			&keys,
982		)
983		.map_err(|_| Error::<T>::CorruptedState)?;
984
985		// Note: PageVersion::V1 is unsupported because no pages were ever written with that tag;
986		// hence it indicates some kind of corrupted state. In future, the pallet should be able
987		// to support reading pages with multiple versions.
988		if let Some(Page { page_version, .. }) = &page {
989			if *page_version != PageVersion::V2 {
990				return Err(Error::<T>::UnsupportedPageVersion.into());
991			}
992		};
993		Ok(page)
994	}
995
996	/// Prevent extrinsics in this pallet from being executed while a migration is in progress.
997	pub fn should_extrinsics_be_run() -> bool {
998		Self::on_chain_storage_version() == STATEFUL_STORAGE_VERSION
999	}
1000}
1001
1002/// The TransactionExtension trait is implemented on BlockDuringMigration to prevent extrinsics
1003/// in this pallet from being executed while a migration is in progress
1004#[derive(Encode, Decode, DecodeWithMemTracking, Clone, Eq, PartialEq, TypeInfo)]
1005#[scale_info(skip_type_params(T))]
1006pub struct BlockDuringMigration<T: Config + Send + Sync>(PhantomData<T>);
1007
1008impl<T: Config + Send + Sync> BlockDuringMigration<T> {
1009	/// Create new `TransactionExtension`
1010	pub fn new() -> Self {
1011		Self(PhantomData)
1012	}
1013
1014	/// Prevent an extrinsic from being executed while a migraiton is in progress.
1015	/// Returns a `ValidTransaction` or wrapped [`ValidityError`]
1016	///
1017	/// # Errors
1018	/// * [`ValidityError::InvalidStorageVersion`] - the on-chain storage version does not match the in-code storage version
1019	///
1020	pub fn block_extrinsics_during_migration() -> TransactionValidity {
1021		const TAG_PREFIX: &str = "StatefulStorageExtrinsicPostV2Migration";
1022		ensure!(
1023			Pallet::<T>::should_extrinsics_be_run(),
1024			InvalidTransaction::Custom(ValidityError::InvalidStorageVersion as u8)
1025		);
1026		ValidTransaction::with_tag_prefix(TAG_PREFIX).build()
1027	}
1028}
1029
1030/// Errors related to the validity of the BlockDuringMigration signed extension.
1031pub enum ValidityError {
1032	/// On-chain storage version
1033	InvalidStorageVersion,
1034}
1035
1036impl<T: Config + Send + Sync> core::fmt::Debug for BlockDuringMigration<T> {
1037	#[cfg(feature = "std")]
1038	fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
1039		write!(f, "BlockDuringMigration<{:?}>", self.0)
1040	}
1041	#[cfg(not(feature = "std"))]
1042	fn fmt(&self, _: &mut core::fmt::Formatter) -> core::fmt::Result {
1043		Ok(())
1044	}
1045}
1046
1047/// The info passed between the `validate` and `prepare` steps for the `BlockDuringMigration` extension.
1048#[derive(RuntimeDebugNoBound)]
1049pub enum Val {
1050	/// Valid transaction, no weight refund.
1051	Valid,
1052	/// Weight refund for the transaction.
1053	Refund(Weight),
1054}
1055
1056/// The info passed between the `prepare` and `post-dispatch` steps for the `BlockDuringMigration` extension.
1057#[derive(RuntimeDebugNoBound)]
1058pub enum Pre {
1059	/// Valid transaction, no weight refund.
1060	Valid,
1061	/// Weight refund for the transaction.
1062	Refund(Weight),
1063}
1064
1065impl<T: Config + Send + Sync> TransactionExtension<T::RuntimeCall> for BlockDuringMigration<T>
1066where
1067	T::RuntimeCall: Dispatchable<Info = DispatchInfo> + IsSubType<Call<T>>,
1068	<T as frame_system::Config>::RuntimeOrigin: AsSystemOriginSigner<T::AccountId> + Clone,
1069{
1070	const IDENTIFIER: &'static str = "BlockDuringMigration";
1071	type Implicit = ();
1072	type Val = Val;
1073	type Pre = Pre;
1074
1075	fn weight(&self, _call: &T::RuntimeCall) -> Weight {
1076		T::DbWeight::get().reads(1)
1077	}
1078
1079	fn validate(
1080		&self,
1081		origin: <T as frame_system::Config>::RuntimeOrigin,
1082		call: &T::RuntimeCall,
1083		_info: &DispatchInfoOf<T::RuntimeCall>,
1084		_len: usize,
1085		_self_implicit: Self::Implicit,
1086		_inherited_implication: &impl Encode,
1087		_source: TransactionSource,
1088	) -> ValidateResult<Self::Val, T::RuntimeCall> {
1089		let weight = self.weight(call);
1090		let Some(_who) = origin.as_system_origin_signer() else {
1091			return Ok((ValidTransaction::default(), Val::Refund(weight), origin));
1092		};
1093		let validity = Self::block_extrinsics_during_migration();
1094		validity.map(|v| (v, Val::Valid, origin))
1095	}
1096
1097	fn prepare(
1098		self,
1099		val: Self::Val,
1100		_origin: &<T as frame_system::Config>::RuntimeOrigin,
1101		_call: &T::RuntimeCall,
1102		_info: &DispatchInfoOf<T::RuntimeCall>,
1103		_len: usize,
1104	) -> Result<Self::Pre, TransactionValidityError> {
1105		match val {
1106			Val::Valid => Ok(Pre::Valid),
1107			Val::Refund(w) => Ok(Pre::Refund(w)),
1108		}
1109	}
1110
1111	fn post_dispatch_details(
1112		pre: Self::Pre,
1113		_info: &DispatchInfo,
1114		_post_info: &PostDispatchInfoOf<T::RuntimeCall>,
1115		_len: usize,
1116		_result: &sp_runtime::DispatchResult,
1117	) -> Result<Weight, TransactionValidityError> {
1118		match pre {
1119			Pre::Valid => Ok(Weight::zero()),
1120			Pre::Refund(w) => Ok(w),
1121		}
1122	}
1123}