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
39mod stateful_child_tree;
40pub mod types;
41pub mod weights;
42
43extern crate alloc;
44use alloc::vec::Vec;
45
46use crate::{stateful_child_tree::StatefulChildTree, types::*};
47use common_primitives::{
48	msa::{
49		DelegatorId, MessageSourceId, MsaLookup, MsaValidator, ProviderId, SchemaGrantValidator,
50	},
51	node::EIP712Encode,
52	schema::{PayloadLocation, SchemaId, SchemaInfoResponse, SchemaProvider, SchemaSetting},
53	stateful_storage::{
54		ItemizedStoragePageResponse, ItemizedStorageResponse, PageHash, PageId,
55		PaginatedStorageResponse,
56	},
57};
58
59use frame_support::{dispatch::DispatchResult, ensure, pallet_prelude::*, traits::Get};
60use frame_system::pallet_prelude::*;
61pub use pallet::*;
62use sp_core::{bounded::BoundedVec, crypto::AccountId32};
63use sp_runtime::{traits::Convert, DispatchError, MultiSignature};
64pub use weights::*;
65
66#[frame_support::pallet]
67pub mod pallet {
68	use super::*;
69
70	#[pallet::config]
71	pub trait Config: frame_system::Config {
72		/// The overarching event type.
73		#[allow(deprecated)]
74		type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
75
76		/// Weight information for extrinsics in this pallet.
77		type WeightInfo: WeightInfo;
78
79		/// A type that will supply MSA related information
80		type MsaInfoProvider: MsaLookup + MsaValidator<AccountId = Self::AccountId>;
81
82		/// A type that will validate schema grants
83		type SchemaGrantValidator: SchemaGrantValidator<BlockNumberFor<Self>>;
84
85		/// A type that will supply schema related information.
86		type SchemaProvider: SchemaProvider<SchemaId>;
87
88		/// The maximum size of a page (in bytes) for an Itemized storage model
89		#[pallet::constant]
90		type MaxItemizedPageSizeBytes: Get<u32> + Default;
91
92		/// The maximum size of a page (in bytes) for a Paginated storage model
93		#[pallet::constant]
94		type MaxPaginatedPageSizeBytes: Get<u32> + Default;
95
96		/// The maximum size of a single item in an itemized storage model (in bytes)
97		#[pallet::constant]
98		type MaxItemizedBlobSizeBytes: Get<u32> + Clone + core::fmt::Debug + PartialEq;
99
100		/// The maximum number of pages in a Paginated storage model
101		#[pallet::constant]
102		type MaxPaginatedPageId: Get<u16>;
103
104		/// The maximum number of actions in itemized actions
105		#[pallet::constant]
106		type MaxItemizedActionsCount: Get<u32>;
107
108		#[cfg(feature = "runtime-benchmarks")]
109		/// A set of helper functions for benchmarking.
110		type MsaBenchmarkHelper: MsaBenchmarkHelper<Self::AccountId>;
111
112		#[cfg(feature = "runtime-benchmarks")]
113		/// A set of helper functions for benchmarking.
114		type SchemaBenchmarkHelper: SchemaBenchmarkHelper;
115
116		/// Hasher to use for MultipartKey
117		type KeyHasher: stateful_child_tree::MultipartKeyStorageHasher;
118
119		/// AccountId truncated to 32 bytes
120		type ConvertIntoAccountId32: Convert<Self::AccountId, AccountId32>;
121
122		/// The number of blocks that we allow for a signed payload to be valid. This is mainly used
123		/// to make sure a signed payload would not be replayable.
124		#[pallet::constant]
125		type MortalityWindowSize: Get<u32>;
126	}
127
128	// Simple declaration of the `Pallet` type. It is placeholder we use to implement traits and
129	// method.
130	#[pallet::pallet]
131	#[pallet::storage_version(STATEFUL_STORAGE_VERSION)]
132	pub struct Pallet<T>(_);
133
134	/// A temporary storage for migration
135	#[pallet::storage]
136	pub(super) type MigrationPageIndex<T: Config> = StorageValue<_, u32, ValueQuery>;
137
138	#[pallet::error]
139	pub enum Error<T> {
140		/// Page would exceed the highest allowable PageId
141		PageIdExceedsMaxAllowed,
142
143		/// Page size exceeds max allowable page size
144		PageExceedsMaxPageSizeBytes,
145
146		/// Invalid SchemaId or Schema not found
147		InvalidSchemaId,
148
149		/// Unsupported operation for schema
150		UnsupportedOperationForSchema,
151
152		/// Schema is not valid for storage model
153		SchemaPayloadLocationMismatch,
154
155		/// Invalid Message Source Account
156		InvalidMessageSourceAccount,
157
158		/// UnauthorizedDelegate
159		UnauthorizedDelegate,
160
161		/// Corrupted State
162		CorruptedState,
163
164		/// Invalid item action
165		InvalidItemAction,
166
167		/// Target page hash does not match current page hash
168		StalePageState,
169
170		/// Invalid Signature for payload
171		InvalidSignature,
172
173		/// The submitted proof has expired; the current block is less the expiration block
174		ProofHasExpired,
175
176		/// The submitted proof expiration block is too far in the future
177		ProofNotYetValid,
178	}
179
180	#[pallet::event]
181	#[pallet::generate_deposit(pub(super) fn deposit_event)]
182	pub enum Event<T: Config> {
183		/// An event for when an itemized storage is updated
184		ItemizedPageUpdated {
185			/// message source id of storage owner
186			msa_id: MessageSourceId,
187			/// schema related to the storage
188			schema_id: SchemaId,
189			/// previous content hash before update
190			prev_content_hash: PageHash,
191			/// current content hash after update
192			curr_content_hash: PageHash,
193		},
194
195		/// An event for when an itemized storage is deleted
196		ItemizedPageDeleted {
197			/// message source id of storage owner
198			msa_id: MessageSourceId,
199			/// schema related to the storage
200			schema_id: SchemaId,
201			/// previous content hash before removal
202			prev_content_hash: PageHash,
203		},
204
205		/// An event for when an paginated storage is updated
206		PaginatedPageUpdated {
207			/// message source id of storage owner
208			msa_id: MessageSourceId,
209			/// schema related to the storage
210			schema_id: SchemaId,
211			/// id of updated page
212			page_id: PageId,
213			/// previous content hash before update
214			prev_content_hash: PageHash,
215			/// current content hash after update
216			curr_content_hash: PageHash,
217		},
218
219		/// An event for when an paginated storage is deleted
220		PaginatedPageDeleted {
221			/// message source id of storage owner
222			msa_id: MessageSourceId,
223			/// schema related to the storage
224			schema_id: SchemaId,
225			/// id of updated page
226			page_id: PageId,
227			/// previous content hash before removal
228			prev_content_hash: PageHash,
229		},
230	}
231
232	#[pallet::hooks]
233	impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
234		fn on_initialize(_current: BlockNumberFor<T>) -> Weight {
235			Weight::zero()
236		}
237	}
238
239	#[pallet::call]
240	impl<T: Config> Pallet<T> {
241		/// Applies the Add or Delete Actions on the requested Itemized page.
242		/// This is treated as a transaction so either all actions succeed or none will be executed.
243		///
244		/// Note: if called by the state owner, call may succeed even on `SignatureRequired` schemas.
245		/// The fact that the entire (signed) transaction is submitted by the owner's keypair is
246		/// considered equivalent to supplying a separate signature. Note in that case that a delegate
247		/// submitting this extrinsic on behalf of a user would fail.
248		///
249		/// # Events
250		/// * [`Event::ItemizedPageUpdated`]
251		/// * [`Event::ItemizedPageDeleted`]
252		///
253		#[pallet::call_index(0)]
254		#[pallet::weight(
255			T::WeightInfo::apply_item_actions_delete(actions.len() as u32)
256			.max(T::WeightInfo::apply_item_actions_add(Pallet::<T>::sum_add_actions_bytes(actions)))
257		)]
258		pub fn apply_item_actions(
259			origin: OriginFor<T>,
260			#[pallet::compact] state_owner_msa_id: MessageSourceId,
261			#[pallet::compact] schema_id: SchemaId,
262			#[pallet::compact] target_hash: PageHash,
263			actions: BoundedVec<
264				ItemAction<T::MaxItemizedBlobSizeBytes>,
265				T::MaxItemizedActionsCount,
266			>,
267		) -> DispatchResult {
268			let key = ensure_signed(origin)?;
269			let is_pruning = actions.iter().any(|a| matches!(a, ItemAction::Delete { .. }));
270			let caller_msa_id = Self::check_msa_and_grants(key, state_owner_msa_id, schema_id)?;
271			let caller_is_state_owner = caller_msa_id == state_owner_msa_id;
272			Self::check_schema_for_write(
273				schema_id,
274				PayloadLocation::Itemized,
275				caller_is_state_owner,
276				is_pruning,
277			)?;
278			Self::update_itemized(state_owner_msa_id, schema_id, target_hash, actions)?;
279			Ok(())
280		}
281
282		/// Creates or updates an Paginated storage with new payload
283		///
284		/// Note: if called by the state owner, call may succeed even on `SignatureRequired` schemas.
285		/// The fact that the entire (signed) transaction is submitted by the owner's keypair is
286		/// considered equivalent to supplying a separate signature. Note in that case that a delegate
287		/// submitting this extrinsic on behalf of a user would fail.
288		///
289		/// # Events
290		/// * [`Event::PaginatedPageUpdated`]
291		///
292		#[pallet::call_index(1)]
293		#[pallet::weight(T::WeightInfo::upsert_page(payload.len() as u32))]
294		pub fn upsert_page(
295			origin: OriginFor<T>,
296			#[pallet::compact] state_owner_msa_id: MessageSourceId,
297			#[pallet::compact] schema_id: SchemaId,
298			#[pallet::compact] page_id: PageId,
299			#[pallet::compact] target_hash: PageHash,
300			payload: BoundedVec<u8, <T>::MaxPaginatedPageSizeBytes>,
301		) -> DispatchResult {
302			let provider_key = ensure_signed(origin)?;
303			ensure!(page_id <= T::MaxPaginatedPageId::get(), Error::<T>::PageIdExceedsMaxAllowed);
304			let caller_msa_id =
305				Self::check_msa_and_grants(provider_key, state_owner_msa_id, schema_id)?;
306			let caller_is_state_owner = caller_msa_id == state_owner_msa_id;
307			Self::check_schema_for_write(
308				schema_id,
309				PayloadLocation::Paginated,
310				caller_is_state_owner,
311				false,
312			)?;
313			Self::update_paginated(
314				state_owner_msa_id,
315				schema_id,
316				page_id,
317				target_hash,
318				PaginatedPage::<T>::from(payload),
319			)?;
320			Ok(())
321		}
322
323		/// Deletes a Paginated storage
324		///
325		/// Note: if called by the state owner, call may succeed even on `SignatureRequired` schemas.
326		/// The fact that the entire (signed) transaction is submitted by the owner's keypair is
327		/// considered equivalent to supplying a separate signature. Note in that case that a delegate
328		/// submitting this extrinsic on behalf of a user would fail.
329		///
330		/// # Events
331		/// * [`Event::PaginatedPageDeleted`]
332		///
333		#[pallet::call_index(2)]
334		#[pallet::weight(T::WeightInfo::delete_page())]
335		pub fn delete_page(
336			origin: OriginFor<T>,
337			#[pallet::compact] state_owner_msa_id: MessageSourceId,
338			#[pallet::compact] schema_id: SchemaId,
339			#[pallet::compact] page_id: PageId,
340			#[pallet::compact] target_hash: PageHash,
341		) -> DispatchResult {
342			let provider_key = ensure_signed(origin)?;
343			ensure!(page_id <= T::MaxPaginatedPageId::get(), Error::<T>::PageIdExceedsMaxAllowed);
344			let caller_msa_id =
345				Self::check_msa_and_grants(provider_key, state_owner_msa_id, schema_id)?;
346			let caller_is_state_owner = caller_msa_id == state_owner_msa_id;
347			Self::check_schema_for_write(
348				schema_id,
349				PayloadLocation::Paginated,
350				caller_is_state_owner,
351				true,
352			)?;
353			Self::delete_paginated(state_owner_msa_id, schema_id, page_id, target_hash)?;
354			Ok(())
355		}
356
357		// REMOVED apply_item_actions_with_signature() at call index 3
358		// REMOVED upsert_page_with_signature() at call index 4
359		// REMOVED delete_page_with_signature() at call index 5
360
361		/// Applies the Add or Delete Actions on the requested Itemized page that requires signature
362		/// since the signature of delegator is checked there is no need for delegation validation
363		/// This is treated as a transaction so either all actions succeed or none will be executed.
364		///
365		/// # Events
366		/// * [`Event::ItemizedPageUpdated`]
367		/// * [`Event::ItemizedPageDeleted`]
368		///
369		#[pallet::call_index(6)]
370		#[pallet::weight(
371		T::WeightInfo::apply_item_actions_with_signature_v2_delete(payload.actions.len() as u32)
372		.max(T::WeightInfo::apply_item_actions_with_signature_v2_add(Pallet::<T>::sum_add_actions_bytes(&payload.actions)))
373		)]
374		pub fn apply_item_actions_with_signature_v2(
375			origin: OriginFor<T>,
376			delegator_key: T::AccountId,
377			proof: MultiSignature,
378			payload: ItemizedSignaturePayloadV2<T>,
379		) -> DispatchResult {
380			ensure_signed(origin)?;
381
382			let is_pruning = payload.actions.iter().any(|a| matches!(a, ItemAction::Delete { .. }));
383			Self::check_payload_expiration(
384				frame_system::Pallet::<T>::block_number(),
385				payload.expiration,
386			)?;
387			Self::check_signature(&proof, &delegator_key.clone(), &payload)?;
388			let state_owner_msa_id = T::MsaInfoProvider::ensure_valid_msa_key(&delegator_key)
389				.map_err(|_| Error::<T>::InvalidMessageSourceAccount)?;
390			Self::check_schema_for_write(
391				payload.schema_id,
392				PayloadLocation::Itemized,
393				true,
394				is_pruning,
395			)?;
396			Self::update_itemized(
397				state_owner_msa_id,
398				payload.schema_id,
399				payload.target_hash,
400				payload.actions,
401			)?;
402			Ok(())
403		}
404
405		/// Creates or updates an Paginated storage with new payload that requires signature
406		/// since the signature of delegator is checked there is no need for delegation validation
407		///
408		/// # Events
409		/// * [`Event::PaginatedPageUpdated`]
410		///
411		#[pallet::call_index(7)]
412		#[pallet::weight(T::WeightInfo::upsert_page_with_signature_v2(payload.payload.len() as u32))]
413		pub fn upsert_page_with_signature_v2(
414			origin: OriginFor<T>,
415			delegator_key: T::AccountId,
416			proof: MultiSignature,
417			payload: PaginatedUpsertSignaturePayloadV2<T>,
418		) -> DispatchResult {
419			ensure_signed(origin)?;
420			ensure!(
421				payload.page_id <= T::MaxPaginatedPageId::get(),
422				Error::<T>::PageIdExceedsMaxAllowed
423			);
424			Self::check_payload_expiration(
425				frame_system::Pallet::<T>::block_number(),
426				payload.expiration,
427			)?;
428			Self::check_signature(&proof, &delegator_key.clone(), &payload)?;
429			let state_owner_msa_id = T::MsaInfoProvider::ensure_valid_msa_key(&delegator_key)
430				.map_err(|_| Error::<T>::InvalidMessageSourceAccount)?;
431			Self::check_schema_for_write(
432				payload.schema_id,
433				PayloadLocation::Paginated,
434				true,
435				false,
436			)?;
437			Self::update_paginated(
438				state_owner_msa_id,
439				payload.schema_id,
440				payload.page_id,
441				payload.target_hash,
442				PaginatedPage::<T>::from(payload.payload),
443			)?;
444			Ok(())
445		}
446
447		/// Deletes a Paginated storage that requires signature
448		/// since the signature of delegator is checked there is no need for delegation validation
449		///
450		/// # Events
451		/// * [`Event::PaginatedPageDeleted`]
452		///
453		#[pallet::call_index(8)]
454		#[pallet::weight(T::WeightInfo::delete_page_with_signature_v2())]
455		pub fn delete_page_with_signature_v2(
456			origin: OriginFor<T>,
457			delegator_key: T::AccountId,
458			proof: MultiSignature,
459			payload: PaginatedDeleteSignaturePayloadV2<T>,
460		) -> DispatchResult {
461			ensure_signed(origin)?;
462			ensure!(
463				payload.page_id <= T::MaxPaginatedPageId::get(),
464				Error::<T>::PageIdExceedsMaxAllowed
465			);
466			Self::check_payload_expiration(
467				frame_system::Pallet::<T>::block_number(),
468				payload.expiration,
469			)?;
470			Self::check_signature(&proof, &delegator_key.clone(), &payload)?;
471			let state_owner_msa_id = T::MsaInfoProvider::ensure_valid_msa_key(&delegator_key)
472				.map_err(|_| Error::<T>::InvalidMessageSourceAccount)?;
473			Self::check_schema_for_write(
474				payload.schema_id,
475				PayloadLocation::Paginated,
476				true,
477				true,
478			)?;
479			Self::delete_paginated(
480				state_owner_msa_id,
481				payload.schema_id,
482				payload.page_id,
483				payload.target_hash,
484			)?;
485			Ok(())
486		}
487	}
488}
489
490impl<T: Config> Pallet<T> {
491	/// Sums the total bytes of each item actions
492	pub fn sum_add_actions_bytes(
493		actions: &BoundedVec<
494			ItemAction<<T as Config>::MaxItemizedBlobSizeBytes>,
495			<T as Config>::MaxItemizedActionsCount,
496		>,
497	) -> u32 {
498		actions.iter().fold(0, |acc, a| {
499			acc.saturating_add(match a {
500				ItemAction::Add { data } => data.len() as u32,
501				_ => 0,
502			})
503		})
504	}
505
506	/// This function returns all the paginated storage associated with `msa_id` and `schema_id`
507	///
508	/// Warning: since this function iterates over all the potential keys it should never called
509	/// from runtime.
510	pub fn get_paginated_storage(
511		msa_id: MessageSourceId,
512		schema_id: SchemaId,
513	) -> Result<Vec<PaginatedStorageResponse>, DispatchError> {
514		Self::check_schema_for_read(schema_id, PayloadLocation::Paginated)?;
515		let prefix: PaginatedPrefixKey = (schema_id,);
516		Ok(StatefulChildTree::<T::KeyHasher>::prefix_iterator::<
517			PaginatedPage<T>,
518			PaginatedKey,
519			PaginatedPrefixKey,
520		>(&msa_id, PALLET_STORAGE_PREFIX, PAGINATED_STORAGE_PREFIX, &prefix)
521		.map(|(k, v)| {
522			let content_hash = v.get_hash();
523			let nonce = v.nonce;
524			PaginatedStorageResponse::new(
525				k.1,
526				msa_id,
527				schema_id,
528				content_hash,
529				nonce,
530				v.data.into_inner(),
531			)
532		})
533		.collect())
534	}
535
536	/// This function returns all the itemized storage associated with `msa_id` and `schema_id`
537	pub fn get_itemized_storage(
538		msa_id: MessageSourceId,
539		schema_id: SchemaId,
540	) -> Result<ItemizedStoragePageResponse, DispatchError> {
541		Self::check_schema_for_read(schema_id, PayloadLocation::Itemized)?;
542		let page = Self::get_itemized_page_for(msa_id, schema_id)?.unwrap_or_default();
543		let items: Vec<ItemizedStorageResponse> = ItemizedOperations::<T>::try_parse(&page, false)
544			.map_err(|_| Error::<T>::CorruptedState)?
545			.items
546			.iter()
547			.map(|(key, v)| ItemizedStorageResponse::new(*key, v.to_vec()))
548			.collect();
549		Ok(ItemizedStoragePageResponse::new(msa_id, schema_id, page.get_hash(), page.nonce, items))
550	}
551
552	/// This function checks to ensure `payload_expire_block` is in a valid range
553	///
554	/// # Errors
555	/// * [`Error::ProofHasExpired`]
556	/// * [`Error::ProofNotYetValid`]
557	///
558	pub fn check_payload_expiration(
559		current_block: BlockNumberFor<T>,
560		payload_expire_block: BlockNumberFor<T>,
561	) -> Result<(), DispatchError> {
562		ensure!(payload_expire_block > current_block, Error::<T>::ProofHasExpired);
563		let max_supported_signature_block = Self::mortality_block_limit(current_block);
564		ensure!(payload_expire_block < max_supported_signature_block, Error::<T>::ProofNotYetValid);
565		Ok(())
566	}
567
568	/// Verify the `signature` was signed by `signer` on `payload` by a wallet
569	/// Note the `wrap_binary_data` follows the Polkadot wallet pattern of wrapping with `<Byte>` tags.
570	///
571	/// # Errors
572	/// * [`Error::InvalidSignature`]
573	///
574	pub fn check_signature<P>(
575		signature: &MultiSignature,
576		signer: &T::AccountId,
577		payload: &P,
578	) -> DispatchResult
579	where
580		P: Encode + EIP712Encode,
581	{
582		let key = T::ConvertIntoAccountId32::convert(signer.clone());
583
584		ensure!(
585			common_runtime::signature::check_signature(signature, key, payload),
586			Error::<T>::InvalidSignature
587		);
588
589		Ok(())
590	}
591
592	/// The furthest in the future a mortality_block value is allowed
593	/// to be for current_block
594	/// This is calculated to be past the risk of a replay attack
595	fn mortality_block_limit(current_block: BlockNumberFor<T>) -> BlockNumberFor<T> {
596		current_block + BlockNumberFor::<T>::from(T::MortalityWindowSize::get())
597	}
598
599	/// Checks that the schema is valid for is action
600	///
601	/// # Errors
602	/// * [`Error::InvalidSchemaId`]
603	/// * [`Error::SchemaPayloadLocationMismatch`]
604	///
605	fn check_schema_for_read(
606		schema_id: SchemaId,
607		expected_payload_location: PayloadLocation,
608	) -> Result<SchemaInfoResponse, DispatchError> {
609		let schema = T::SchemaProvider::get_schema_info_by_id(schema_id)
610			.ok_or(Error::<T>::InvalidSchemaId)?;
611
612		// Ensure that the schema's payload location matches the expected location.
613		ensure!(
614			schema.payload_location == expected_payload_location,
615			Error::<T>::SchemaPayloadLocationMismatch
616		);
617
618		Ok(schema)
619	}
620
621	/// Checks that the schema is valid for is action
622	///
623	/// # Errors
624	/// * [`Error::InvalidSchemaId`]
625	/// * [`Error::SchemaPayloadLocationMismatch`]
626	/// * [`Error::UnsupportedOperationForSchema`]
627	///
628	fn check_schema_for_write(
629		schema_id: SchemaId,
630		expected_payload_location: PayloadLocation,
631		is_payload_signed: bool,
632		is_deleting: bool,
633	) -> DispatchResult {
634		let schema = Self::check_schema_for_read(schema_id, expected_payload_location)?;
635
636		// Ensure that the schema allows signed payloads.
637		// If so, calling extrinsic must be of signature type.
638		if schema.settings.contains(&SchemaSetting::SignatureRequired) {
639			ensure!(is_payload_signed, Error::<T>::UnsupportedOperationForSchema);
640		}
641
642		// Ensure that the schema does not allow deletion for AppendOnly SchemaSetting.
643		if schema.settings.contains(&SchemaSetting::AppendOnly) {
644			ensure!(!is_deleting, Error::<T>::UnsupportedOperationForSchema);
645		}
646
647		Ok(())
648	}
649
650	/// Checks that existence of Msa for certain key and if the grant is valid when the caller Msa
651	/// is different from the state owner Msa
652	///
653	/// # Errors
654	/// * [`Error::InvalidMessageSourceAccount`]
655	/// * [`Error::UnauthorizedDelegate`]
656	///
657	fn check_msa_and_grants(
658		key: T::AccountId,
659		state_owner_msa_id: MessageSourceId,
660		schema_id: SchemaId,
661	) -> Result<MessageSourceId, DispatchError> {
662		let caller_msa_id = T::MsaInfoProvider::ensure_valid_msa_key(&key)
663			.map_err(|_| Error::<T>::InvalidMessageSourceAccount)?;
664
665		// if caller and owner are the same no delegation is needed
666		if caller_msa_id != state_owner_msa_id {
667			let current_block = frame_system::Pallet::<T>::block_number();
668			T::SchemaGrantValidator::ensure_valid_schema_grant(
669				ProviderId(caller_msa_id),
670				DelegatorId(state_owner_msa_id),
671				schema_id,
672				current_block,
673			)
674			.map_err(|_| Error::<T>::UnauthorizedDelegate)?;
675		}
676
677		Ok(caller_msa_id)
678	}
679
680	/// Updates an itemized storage by applying provided actions and deposit events
681	///
682	/// # Events
683	/// * [`Event::ItemizedPageUpdated`]
684	/// * [`Event::ItemizedPageDeleted`]
685	///
686	fn update_itemized(
687		state_owner_msa_id: MessageSourceId,
688		schema_id: SchemaId,
689		target_hash: PageHash,
690		actions: BoundedVec<ItemAction<T::MaxItemizedBlobSizeBytes>, T::MaxItemizedActionsCount>,
691	) -> DispatchResult {
692		let key: ItemizedKey = (schema_id,);
693		let existing_page =
694			Self::get_itemized_page_for(state_owner_msa_id, schema_id)?.unwrap_or_default();
695
696		let prev_content_hash = existing_page.get_hash();
697		ensure!(target_hash == prev_content_hash, Error::<T>::StalePageState);
698
699		let mut updated_page =
700			ItemizedOperations::<T>::apply_item_actions(&existing_page, &actions[..]).map_err(
701				|e| match e {
702					PageError::ErrorParsing(err) => {
703						log::warn!(
704							"failed parsing Itemized msa={state_owner_msa_id:?} schema_id={schema_id:?} {err:?}",
705						);
706						Error::<T>::CorruptedState
707					},
708					_ => Error::<T>::InvalidItemAction,
709				},
710			)?;
711		updated_page.nonce = existing_page.nonce.wrapping_add(1);
712
713		match updated_page.is_empty() {
714			true => {
715				StatefulChildTree::<T::KeyHasher>::kill(
716					&state_owner_msa_id,
717					PALLET_STORAGE_PREFIX,
718					ITEMIZED_STORAGE_PREFIX,
719					&key,
720				);
721				Self::deposit_event(Event::ItemizedPageDeleted {
722					msa_id: state_owner_msa_id,
723					schema_id,
724					prev_content_hash,
725				});
726			},
727			false => {
728				StatefulChildTree::<T::KeyHasher>::write(
729					&state_owner_msa_id,
730					PALLET_STORAGE_PREFIX,
731					ITEMIZED_STORAGE_PREFIX,
732					&key,
733					&updated_page,
734				);
735				Self::deposit_event(Event::ItemizedPageUpdated {
736					msa_id: state_owner_msa_id,
737					schema_id,
738					curr_content_hash: updated_page.get_hash(),
739					prev_content_hash,
740				});
741			},
742		};
743		Ok(())
744	}
745
746	/// Updates a page from paginated storage by provided new page
747	///
748	/// # Events
749	/// * [`Event::PaginatedPageUpdated`]
750	///
751	fn update_paginated(
752		state_owner_msa_id: MessageSourceId,
753		schema_id: SchemaId,
754		page_id: PageId,
755		target_hash: PageHash,
756		mut new_page: PaginatedPage<T>,
757	) -> DispatchResult {
758		let keys: PaginatedKey = (schema_id, page_id);
759		let existing_page: PaginatedPage<T> =
760			Self::get_paginated_page_for(state_owner_msa_id, schema_id, page_id)?
761				.unwrap_or_default();
762
763		let prev_content_hash: PageHash = existing_page.get_hash();
764		ensure!(target_hash == prev_content_hash, Error::<T>::StalePageState);
765
766		new_page.nonce = existing_page.nonce.wrapping_add(1);
767
768		StatefulChildTree::<T::KeyHasher>::write(
769			&state_owner_msa_id,
770			PALLET_STORAGE_PREFIX,
771			PAGINATED_STORAGE_PREFIX,
772			&keys,
773			&new_page,
774		);
775		Self::deposit_event(Event::PaginatedPageUpdated {
776			msa_id: state_owner_msa_id,
777			schema_id,
778			page_id,
779			curr_content_hash: new_page.get_hash(),
780			prev_content_hash,
781		});
782		Ok(())
783	}
784
785	/// Deletes a page from paginated storage
786	///
787	/// # Events
788	/// * [`Event::PaginatedPageDeleted`]
789	///
790	fn delete_paginated(
791		state_owner_msa_id: MessageSourceId,
792		schema_id: SchemaId,
793		page_id: PageId,
794		target_hash: PageHash,
795	) -> DispatchResult {
796		let keys: PaginatedKey = (schema_id, page_id);
797		if let Some(existing_page) =
798			Self::get_paginated_page_for(state_owner_msa_id, schema_id, page_id)?
799		{
800			let prev_content_hash: PageHash = existing_page.get_hash();
801			ensure!(target_hash == prev_content_hash, Error::<T>::StalePageState);
802			StatefulChildTree::<T::KeyHasher>::kill(
803				&state_owner_msa_id,
804				PALLET_STORAGE_PREFIX,
805				PAGINATED_STORAGE_PREFIX,
806				&keys,
807			);
808			Self::deposit_event(Event::PaginatedPageDeleted {
809				msa_id: state_owner_msa_id,
810				schema_id,
811				page_id,
812				prev_content_hash,
813			});
814		}
815
816		Ok(())
817	}
818
819	/// Gets a paginated storage for desired parameters
820	pub fn get_paginated_page_for(
821		msa_id: MessageSourceId,
822		schema_id: SchemaId,
823		page_id: PageId,
824	) -> Result<Option<PaginatedPage<T>>, DispatchError> {
825		let keys: PaginatedKey = (schema_id, page_id);
826		Ok(StatefulChildTree::<T::KeyHasher>::try_read::<_, PaginatedPage<T>>(
827			&msa_id,
828			PALLET_STORAGE_PREFIX,
829			PAGINATED_STORAGE_PREFIX,
830			&keys,
831		)
832		.map_err(|_| Error::<T>::CorruptedState)?)
833	}
834
835	/// Gets an itemized storage for desired parameters
836	pub fn get_itemized_page_for(
837		msa_id: MessageSourceId,
838		schema_id: SchemaId,
839	) -> Result<Option<ItemizedPage<T>>, DispatchError> {
840		let keys: ItemizedKey = (schema_id,);
841		Ok(StatefulChildTree::<T::KeyHasher>::try_read::<_, ItemizedPage<T>>(
842			&msa_id,
843			PALLET_STORAGE_PREFIX,
844			ITEMIZED_STORAGE_PREFIX,
845			&keys,
846		)
847		.map_err(|_| Error::<T>::CorruptedState)?)
848	}
849}