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
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 #[allow(deprecated)]
74 type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
75
76 type WeightInfo: WeightInfo;
78
79 type MsaInfoProvider: MsaLookup + MsaValidator<AccountId = Self::AccountId>;
81
82 type SchemaGrantValidator: SchemaGrantValidator<BlockNumberFor<Self>>;
84
85 type SchemaProvider: SchemaProvider<SchemaId>;
87
88 #[pallet::constant]
90 type MaxItemizedPageSizeBytes: Get<u32> + Default;
91
92 #[pallet::constant]
94 type MaxPaginatedPageSizeBytes: Get<u32> + Default;
95
96 #[pallet::constant]
98 type MaxItemizedBlobSizeBytes: Get<u32> + Clone + core::fmt::Debug + PartialEq;
99
100 #[pallet::constant]
102 type MaxPaginatedPageId: Get<u16>;
103
104 #[pallet::constant]
106 type MaxItemizedActionsCount: Get<u32>;
107
108 #[cfg(feature = "runtime-benchmarks")]
109 type MsaBenchmarkHelper: MsaBenchmarkHelper<Self::AccountId>;
111
112 #[cfg(feature = "runtime-benchmarks")]
113 type SchemaBenchmarkHelper: SchemaBenchmarkHelper;
115
116 type KeyHasher: stateful_child_tree::MultipartKeyStorageHasher;
118
119 type ConvertIntoAccountId32: Convert<Self::AccountId, AccountId32>;
121
122 #[pallet::constant]
125 type MortalityWindowSize: Get<u32>;
126 }
127
128 #[pallet::pallet]
131 #[pallet::storage_version(STATEFUL_STORAGE_VERSION)]
132 pub struct Pallet<T>(_);
133
134 #[pallet::storage]
136 pub(super) type MigrationPageIndex<T: Config> = StorageValue<_, u32, ValueQuery>;
137
138 #[pallet::error]
139 pub enum Error<T> {
140 PageIdExceedsMaxAllowed,
142
143 PageExceedsMaxPageSizeBytes,
145
146 InvalidSchemaId,
148
149 UnsupportedOperationForSchema,
151
152 SchemaPayloadLocationMismatch,
154
155 InvalidMessageSourceAccount,
157
158 UnauthorizedDelegate,
160
161 CorruptedState,
163
164 InvalidItemAction,
166
167 StalePageState,
169
170 InvalidSignature,
172
173 ProofHasExpired,
175
176 ProofNotYetValid,
178 }
179
180 #[pallet::event]
181 #[pallet::generate_deposit(pub(super) fn deposit_event)]
182 pub enum Event<T: Config> {
183 ItemizedPageUpdated {
185 msa_id: MessageSourceId,
187 schema_id: SchemaId,
189 prev_content_hash: PageHash,
191 curr_content_hash: PageHash,
193 },
194
195 ItemizedPageDeleted {
197 msa_id: MessageSourceId,
199 schema_id: SchemaId,
201 prev_content_hash: PageHash,
203 },
204
205 PaginatedPageUpdated {
207 msa_id: MessageSourceId,
209 schema_id: SchemaId,
211 page_id: PageId,
213 prev_content_hash: PageHash,
215 curr_content_hash: PageHash,
217 },
218
219 PaginatedPageDeleted {
221 msa_id: MessageSourceId,
223 schema_id: SchemaId,
225 page_id: PageId,
227 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 #[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 #[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 #[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 #[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 #[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 #[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 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 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 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 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 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 fn mortality_block_limit(current_block: BlockNumberFor<T>) -> BlockNumberFor<T> {
596 current_block + BlockNumberFor::<T>::from(T::MortalityWindowSize::get())
597 }
598
599 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!(
614 schema.payload_location == expected_payload_location,
615 Error::<T>::SchemaPayloadLocationMismatch
616 );
617
618 Ok(schema)
619 }
620
621 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 if schema.settings.contains(&SchemaSetting::SignatureRequired) {
639 ensure!(is_payload_signed, Error::<T>::UnsupportedOperationForSchema);
640 }
641
642 if schema.settings.contains(&SchemaSetting::AppendOnly) {
644 ensure!(!is_deleting, Error::<T>::UnsupportedOperationForSchema);
645 }
646
647 Ok(())
648 }
649
650 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_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 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 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 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 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 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}