1#![doc = include_str!("../README.md")]
11#![allow(clippy::expect_used)]
20#![cfg_attr(not(feature = "std"), no_std)]
22#![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
39pub 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 #[allow(deprecated)]
89 type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
90
91 type WeightInfo: WeightInfo;
93
94 type MsaInfoProvider: MsaLookup + MsaValidator<AccountId = Self::AccountId>;
96
97 type SchemaGrantValidator: GrantValidator<IntentId, BlockNumberFor<Self>>;
99
100 type SchemaProvider: SchemaProvider<SchemaId>;
102
103 #[pallet::constant]
105 type MaxItemizedPageSizeBytes: Get<u32> + Default;
106
107 #[pallet::constant]
109 type MaxPaginatedPageSizeBytes: Get<u32> + Default;
110
111 #[pallet::constant]
113 type MaxItemizedBlobSizeBytes: Get<u32> + Clone + core::fmt::Debug + PartialEq;
114
115 #[pallet::constant]
117 type MaxPaginatedPageId: Get<u16>;
118
119 #[pallet::constant]
121 type MaxItemizedActionsCount: Get<u32>;
122
123 #[cfg(feature = "runtime-benchmarks")]
124 type MsaBenchmarkHelper: MsaBenchmarkHelper<Self::AccountId>;
126
127 #[cfg(feature = "runtime-benchmarks")]
128 type SchemaBenchmarkHelper: SchemaBenchmarkHelper;
130
131 type KeyHasher: stateful_child_tree::MultipartKeyStorageHasher;
133
134 type ConvertIntoAccountId32: Convert<Self::AccountId, AccountId32>;
136
137 #[pallet::constant]
140 type MortalityWindowSize: Get<u32>;
141
142 type MigrateEmitEvery: Get<u32> + Clone + Debug;
147 }
148
149 #[pallet::pallet]
152 #[pallet::storage_version(STATEFUL_STORAGE_VERSION)]
153 pub struct Pallet<T>(_);
154
155 #[pallet::storage]
157 pub(super) type MigrationPageIndex<T: Config> = StorageValue<_, u32, ValueQuery>;
158
159 #[pallet::error]
160 pub enum Error<T> {
161 PageIdExceedsMaxAllowed,
163
164 PageExceedsMaxPageSizeBytes,
166
167 InvalidSchemaId,
169
170 UnsupportedOperationForSchema,
172
173 SchemaPayloadLocationMismatch,
175
176 InvalidMessageSourceAccount,
178
179 UnauthorizedDelegate,
181
182 CorruptedState,
184
185 InvalidItemAction,
187
188 StalePageState,
190
191 InvalidSignature,
193
194 ProofHasExpired,
196
197 ProofNotYetValid,
199
200 UnsupportedPageVersion,
202
203 InvalidIntentId,
205
206 PayloadLocationMismatch,
208 }
209
210 #[pallet::event]
211 #[pallet::generate_deposit(pub(super) fn deposit_event)]
212 pub enum Event<T: Config> {
213 ItemizedPageUpdated {
215 msa_id: MessageSourceId,
217 intent_id: IntentId,
219 prev_content_hash: PageHash,
221 curr_content_hash: PageHash,
223 },
224
225 ItemizedPageDeleted {
227 msa_id: MessageSourceId,
229 intent_id: IntentId,
231 prev_content_hash: PageHash,
233 },
234
235 PaginatedPageUpdated {
237 msa_id: MessageSourceId,
239 intent_id: IntentId,
241 page_id: PageId,
243 prev_content_hash: PageHash,
245 curr_content_hash: PageHash,
247 },
248
249 PaginatedPageDeleted {
251 msa_id: MessageSourceId,
253 intent_id: IntentId,
255 page_id: PageId,
257 prev_content_hash: PageHash,
259 },
260
261 StatefulPagesMigrated {
263 last_trie: (u64, PayloadLocation),
265 total_page_count: u64,
267 },
268 }
269
270 #[pallet::call]
271 impl<T: Config> Pallet<T> {
272 #[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 #[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 #[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 #[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 #[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 #[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 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 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 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 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 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 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 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 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 fn mortality_block_limit(current_block: BlockNumberFor<T>) -> BlockNumberFor<T> {
690 current_block + BlockNumberFor::<T>::from(T::MortalityWindowSize::get())
691 }
692
693 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!(
708 schema.payload_location == expected_payload_location,
709 Error::<T>::PayloadLocationMismatch
710 );
711
712 Ok(schema)
713 }
714
715 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!(
730 intent.payload_location == expected_payload_location,
731 Error::<T>::PayloadLocationMismatch
732 );
733
734 Ok(intent)
735 }
736
737 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 if schema.settings.contains(&IntentSetting::SignatureRequired) {
755 ensure!(is_payload_signed, Error::<T>::UnsupportedOperationForSchema);
756 }
757
758 if schema.settings.contains(&IntentSetting::AppendOnly) {
760 ensure!(!is_deleting, Error::<T>::UnsupportedOperationForSchema);
761 }
762
763 Ok(schema)
764 }
765
766 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_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 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 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 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 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 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 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 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 pub fn should_extrinsics_be_run() -> bool {
998 Self::on_chain_storage_version() == STATEFUL_STORAGE_VERSION
999 }
1000}
1001
1002#[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 pub fn new() -> Self {
1011 Self(PhantomData)
1012 }
1013
1014 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
1030pub enum ValidityError {
1032 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#[derive(RuntimeDebugNoBound)]
1049pub enum Val {
1050 Valid,
1052 Refund(Weight),
1054}
1055
1056#[derive(RuntimeDebugNoBound)]
1058pub enum Pre {
1059 Valid,
1061 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}