1#![doc = include_str!("../README.md")]
11#![allow(clippy::expect_used)]
18#![cfg_attr(not(feature = "std"), no_std)]
19#![allow(rustdoc::private_intra_doc_links)]
20#![deny(
22 rustdoc::broken_intra_doc_links,
23 rustdoc::missing_crate_level_docs,
24 rustdoc::invalid_codeblock_attributes,
25 missing_docs
26)]
27
28use common_primitives::{
29 node::ProposalProvider,
30 parquet::ParquetModel,
31 schema::{
32 ModelType, PayloadLocation, SchemaId, SchemaProvider, SchemaResponse, SchemaSetting,
33 SchemaSettings, SchemaValidator,
34 },
35};
36use frame_support::{
37 dispatch::{DispatchResult, PostDispatchInfo},
38 ensure,
39 traits::{BuildGenesisConfig, Get},
40};
41use sp_runtime::{traits::Dispatchable, BoundedVec, DispatchError};
42extern crate alloc;
43use alloc::{boxed::Box, vec::Vec};
44
45#[cfg(test)]
46mod tests;
47
48#[cfg(feature = "runtime-benchmarks")]
49mod benchmarking;
50#[cfg(feature = "runtime-benchmarks")]
51use common_primitives::benchmarks::SchemaBenchmarkHelper;
52use common_primitives::schema::{SchemaInfoResponse, SchemaVersionResponse};
53mod types;
54
55pub use pallet::*;
56pub mod weights;
57pub use types::*;
58pub use weights::*;
59
60mod serde;
61
62#[frame_support::pallet]
63pub mod pallet {
64 use super::*;
65 use frame_support::pallet_prelude::*;
66 use frame_system::pallet_prelude::*;
67
68 #[pallet::config]
69 pub trait Config: frame_system::Config {
70 #[allow(deprecated)]
72 type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
73
74 type WeightInfo: WeightInfo;
76
77 #[pallet::constant]
79 type MinSchemaModelSizeBytes: Get<u32>;
80
81 #[pallet::constant]
83 type SchemaModelMaxBytesBoundedVecLimit: Get<u32> + MaxEncodedLen;
84
85 #[pallet::constant]
87 type MaxSchemaRegistrations: Get<SchemaId>;
88
89 type CreateSchemaViaGovernanceOrigin: EnsureOrigin<Self::RuntimeOrigin>;
91
92 type Proposal: Parameter
94 + Dispatchable<RuntimeOrigin = Self::RuntimeOrigin, PostInfo = PostDispatchInfo>
95 + From<Call<Self>>;
96
97 type ProposalProvider: ProposalProvider<Self::AccountId, Self::Proposal>;
99
100 #[pallet::constant]
102 type MaxSchemaSettingsPerSchema: Get<u32>;
103 }
104
105 #[pallet::event]
106 #[pallet::generate_deposit(pub (super) fn deposit_event)]
107 pub enum Event<T: Config> {
108 SchemaCreated {
110 key: T::AccountId,
112
113 schema_id: SchemaId,
115 },
116
117 SchemaMaxSizeChanged {
119 max_size: u32,
121 },
122
123 SchemaNameCreated {
125 schema_id: SchemaId,
127 name: Vec<u8>,
129 },
130 }
131
132 #[derive(PartialEq, Eq)] #[pallet::error]
134 pub enum Error<T> {
135 InvalidSchema,
137
138 ExceedsMaxSchemaModelBytes,
140
141 LessThanMinSchemaModelBytes,
143
144 SchemaCountOverflow,
146
147 InvalidSetting,
149
150 InvalidSchemaNameEncoding,
152
153 InvalidSchemaNameCharacters,
155
156 InvalidSchemaNameStructure,
158
159 InvalidSchemaNameLength,
161
162 InvalidSchemaNamespaceLength,
164
165 InvalidSchemaDescriptorLength,
167
168 ExceedsMaxNumberOfVersions,
170
171 SchemaIdAlreadyExists,
173
174 SchemaIdDoesNotExist,
176
177 SchemaIdAlreadyHasName,
179 }
180
181 #[pallet::pallet]
182 #[pallet::storage_version(SCHEMA_STORAGE_VERSION)]
183 pub struct Pallet<T>(_);
184
185 #[pallet::storage]
189 pub(super) type GovernanceSchemaModelMaxBytes<T: Config> = StorageValue<_, u32, ValueQuery>;
190
191 #[pallet::storage]
195 pub(super) type CurrentSchemaIdentifierMaximum<T: Config> =
196 StorageValue<_, SchemaId, ValueQuery>;
197
198 #[pallet::storage]
202 pub(super) type SchemaInfos<T: Config> =
203 StorageMap<_, Twox64Concat, SchemaId, SchemaInfo, OptionQuery>;
204
205 #[pallet::storage]
209 pub(super) type SchemaPayloads<T: Config> = StorageMap<
210 _,
211 Twox64Concat,
212 SchemaId,
213 BoundedVec<u8, T::SchemaModelMaxBytesBoundedVecLimit>,
214 OptionQuery,
215 >;
216
217 #[pallet::storage]
221 pub(super) type SchemaNameToIds<T: Config> = StorageDoubleMap<
222 _,
223 Blake2_128Concat,
224 SchemaNamespace,
225 Blake2_128Concat,
226 SchemaDescriptor,
227 SchemaVersionId,
228 ValueQuery,
229 >;
230
231 #[pallet::genesis_config]
232 pub struct GenesisConfig<T: Config> {
233 pub initial_schema_identifier_max: u16,
235 pub initial_max_schema_model_size: u32,
237 pub initial_schemas: Vec<GenesisSchema>,
239 #[serde(skip)]
241 pub _config: PhantomData<T>,
242 }
243
244 impl<T: Config> core::default::Default for GenesisConfig<T> {
245 fn default() -> Self {
246 Self {
247 initial_schema_identifier_max: 16_000,
248 initial_max_schema_model_size: 1024,
249 initial_schemas: Default::default(),
250 _config: Default::default(),
251 }
252 }
253 }
254 #[pallet::genesis_build]
255 impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
256 fn build(&self) {
257 GovernanceSchemaModelMaxBytes::<T>::put(self.initial_max_schema_model_size);
258
259 for schema in self.initial_schemas.iter() {
261 let model: BoundedVec<u8, T::SchemaModelMaxBytesBoundedVecLimit> =
262 BoundedVec::try_from(schema.model.clone().into_bytes()).expect(
263 "Genesis Schema Model larger than SchemaModelMaxBytesBoundedVecLimit",
264 );
265 let name_payload: SchemaNamePayload =
266 BoundedVec::try_from(schema.name.clone().into_bytes())
267 .expect("Genesis Schema Name larger than SCHEMA_NAME_BYTES_MAX");
268 let parsed_name: Option<SchemaName> = if name_payload.len() > 0 {
269 Some(
270 SchemaName::try_parse::<T>(name_payload, true)
271 .expect("Bad Genesis Schema Name"),
272 )
273 } else {
274 None
275 };
276 let settings: BoundedVec<SchemaSetting, T::MaxSchemaSettingsPerSchema> =
277 BoundedVec::try_from(schema.settings.clone()).expect(
278 "Bad Genesis Schema Settings. Perhaps larger than MaxSchemaSettingsPerSchema"
279 );
280
281 let _ = Pallet::<T>::add_schema(
282 model,
283 schema.model_type,
284 schema.payload_location,
285 settings,
286 parsed_name,
287 )
288 .expect("Failed to set Schema in Genesis!");
289 }
290
291 CurrentSchemaIdentifierMaximum::<T>::put(self.initial_schema_identifier_max);
293 }
294 }
295
296 #[pallet::call]
297 impl<T: Config> Pallet<T> {
298 #[pallet::call_index(1)]
313 #[pallet::weight((T::WeightInfo::set_max_schema_model_bytes(), DispatchClass::Operational))]
314 pub fn set_max_schema_model_bytes(
315 origin: OriginFor<T>,
316 #[pallet::compact] max_size: u32,
317 ) -> DispatchResult {
318 ensure_root(origin)?;
319 ensure!(
320 max_size <= T::SchemaModelMaxBytesBoundedVecLimit::get(),
321 Error::<T>::ExceedsMaxSchemaModelBytes
322 );
323 GovernanceSchemaModelMaxBytes::<T>::set(max_size);
324 Self::deposit_event(Event::SchemaMaxSizeChanged { max_size });
325 Ok(())
326 }
327
328 #[pallet::call_index(5)]
335 #[pallet::weight(
336 match schema_name {
337 Some(_) => T::WeightInfo::propose_to_create_schema_v2_with_name(model.len() as u32),
338 None => T::WeightInfo::propose_to_create_schema_v2_without_name(model.len() as u32)
339 }
340 )]
341 pub fn propose_to_create_schema_v2(
342 origin: OriginFor<T>,
343 model: BoundedVec<u8, T::SchemaModelMaxBytesBoundedVecLimit>,
344 model_type: ModelType,
345 payload_location: PayloadLocation,
346 settings: BoundedVec<SchemaSetting, T::MaxSchemaSettingsPerSchema>,
347 schema_name: Option<SchemaNamePayload>,
348 ) -> DispatchResult {
349 let proposer = ensure_signed(origin)?;
350
351 let proposal: Box<T::Proposal> = Box::new(
352 (Call::<T>::create_schema_via_governance_v2 {
353 creator_key: proposer.clone(),
354 model,
355 model_type,
356 payload_location,
357 settings,
358 schema_name,
359 })
360 .into(),
361 );
362 T::ProposalProvider::propose_with_simple_majority(proposer, proposal)?;
363 Ok(())
364 }
365
366 #[pallet::call_index(6)]
386 #[pallet::weight(
387 match schema_name {
388 Some(_) => T::WeightInfo::create_schema_via_governance_v2_with_name(model.len() as u32+ settings.len() as u32),
389 None => T::WeightInfo::create_schema_via_governance_v2_without_name(model.len() as u32+ settings.len() as u32)
390 }
391 )]
392 pub fn create_schema_via_governance_v2(
393 origin: OriginFor<T>,
394 creator_key: T::AccountId,
395 model: BoundedVec<u8, T::SchemaModelMaxBytesBoundedVecLimit>,
396 model_type: ModelType,
397 payload_location: PayloadLocation,
398 settings: BoundedVec<SchemaSetting, T::MaxSchemaSettingsPerSchema>,
399 schema_name: Option<SchemaNamePayload>,
400 ) -> DispatchResult {
401 T::CreateSchemaViaGovernanceOrigin::ensure_origin(origin)?;
402 let (schema_id, schema_name) = Self::create_schema_for(
403 model,
404 model_type,
405 payload_location,
406 settings,
407 schema_name,
408 )?;
409
410 Self::deposit_event(Event::SchemaCreated { key: creator_key, schema_id });
411 if let Some(inner_name) = schema_name {
412 Self::deposit_event(Event::SchemaNameCreated {
413 schema_id,
414 name: inner_name.get_combined_name(),
415 });
416 }
417 Ok(())
418 }
419
420 #[pallet::call_index(7)]
445 #[pallet::weight(
446 match schema_name {
447 Some(_) => T::WeightInfo::create_schema_v3_with_name(model.len() as u32 + settings.len() as u32),
448 None => T::WeightInfo::create_schema_v3_without_name(model.len() as u32 + settings.len() as u32)
449 }
450 )]
451 pub fn create_schema_v3(
452 origin: OriginFor<T>,
453 model: BoundedVec<u8, T::SchemaModelMaxBytesBoundedVecLimit>,
454 model_type: ModelType,
455 payload_location: PayloadLocation,
456 settings: BoundedVec<SchemaSetting, T::MaxSchemaSettingsPerSchema>,
457 schema_name: Option<SchemaNamePayload>,
458 ) -> DispatchResult {
459 let sender = ensure_signed(origin)?;
460
461 let (schema_id, schema_name) = Self::create_schema_for(
462 model,
463 model_type,
464 payload_location,
465 settings,
466 schema_name,
467 )?;
468
469 Self::deposit_event(Event::SchemaCreated { key: sender, schema_id });
470 if let Some(inner_name) = schema_name {
471 Self::deposit_event(Event::SchemaNameCreated {
472 schema_id,
473 name: inner_name.get_combined_name(),
474 });
475 }
476 Ok(())
477 }
478
479 #[pallet::call_index(8)]
493 #[pallet::weight(T::WeightInfo::propose_to_create_schema_name())]
494 pub fn propose_to_create_schema_name(
495 origin: OriginFor<T>,
496 schema_id: SchemaId,
497 schema_name: SchemaNamePayload,
498 ) -> DispatchResult {
499 let proposer = ensure_signed(origin)?;
500
501 let _ = Self::parse_and_verify_schema_name(schema_id, &schema_name)?;
502
503 let proposal: Box<T::Proposal> = Box::new(
504 (Call::<T>::create_schema_name_via_governance { schema_id, schema_name }).into(),
505 );
506 T::ProposalProvider::propose_with_simple_majority(proposer, proposal)?;
507 Ok(())
508 }
509
510 #[pallet::call_index(9)]
531 #[pallet::weight(T::WeightInfo::create_schema_name_via_governance())]
532 pub fn create_schema_name_via_governance(
533 origin: OriginFor<T>,
534 schema_id: SchemaId,
535 schema_name: SchemaNamePayload,
536 ) -> DispatchResult {
537 T::CreateSchemaViaGovernanceOrigin::ensure_origin(origin)?;
538
539 let parsed_name = Self::parse_and_verify_schema_name(schema_id, &schema_name)?;
540 SchemaNameToIds::<T>::try_mutate(
541 &parsed_name.namespace,
542 &parsed_name.descriptor,
543 |schema_version_id| -> DispatchResult {
544 schema_version_id.add::<T>(schema_id)?;
545
546 Self::deposit_event(Event::SchemaNameCreated {
547 schema_id,
548 name: parsed_name.get_combined_name(),
549 });
550 Ok(())
551 },
552 )
553 }
554 }
555
556 impl<T: Config> Pallet<T> {
557 #[cfg(any(feature = "std", feature = "runtime-benchmarks", test))]
559 pub fn set_schema_count(n: SchemaId) {
560 <CurrentSchemaIdentifierMaximum<T>>::set(n);
561 }
562
563 pub fn add_schema(
566 model: BoundedVec<u8, T::SchemaModelMaxBytesBoundedVecLimit>,
567 model_type: ModelType,
568 payload_location: PayloadLocation,
569 settings: BoundedVec<SchemaSetting, T::MaxSchemaSettingsPerSchema>,
570 schema_name_option: Option<SchemaName>,
571 ) -> Result<SchemaId, DispatchError> {
572 let schema_id = Self::get_next_schema_id()?;
573 let has_name = schema_name_option.is_some();
574 let mut set_settings = SchemaSettings::all_disabled();
575 if !settings.is_empty() {
576 for i in settings.into_inner() {
577 set_settings.set(i);
578 }
579 }
580
581 if let Some(schema_name) = schema_name_option {
582 SchemaNameToIds::<T>::try_mutate(
583 schema_name.namespace,
584 schema_name.descriptor,
585 |schema_version_id| -> Result<(), DispatchError> {
586 schema_version_id.add::<T>(schema_id)?;
587 Ok(())
588 },
589 )?;
590 };
591
592 let schema_info =
593 SchemaInfo { model_type, payload_location, settings: set_settings, has_name };
594 <CurrentSchemaIdentifierMaximum<T>>::set(schema_id);
595 <SchemaInfos<T>>::insert(schema_id, schema_info);
596 <SchemaPayloads<T>>::insert(schema_id, model);
597
598 Ok(schema_id)
599 }
600
601 pub fn get_schema_by_id(schema_id: SchemaId) -> Option<SchemaResponse> {
603 match (SchemaInfos::<T>::get(schema_id), SchemaPayloads::<T>::get(schema_id)) {
604 (Some(schema_info), Some(payload)) => {
605 let model_vec: Vec<u8> = payload.into_inner();
606 let saved_settings = schema_info.settings;
607 let settings = saved_settings.0.iter().collect::<Vec<SchemaSetting>>();
608 let response = SchemaResponse {
609 schema_id,
610 model: model_vec,
611 model_type: schema_info.model_type,
612 payload_location: schema_info.payload_location,
613 settings,
614 };
615 Some(response)
616 },
617 (None, Some(_)) | (Some(_), None) => {
618 log::error!("Corrupted state for schema {schema_id:?}, Should never happen!");
619 None
620 },
621 (None, None) => None,
622 }
623 }
624
625 pub fn get_schema_info_by_id(schema_id: SchemaId) -> Option<SchemaInfoResponse> {
627 if let Some(schema_info) = SchemaInfos::<T>::get(schema_id) {
628 let saved_settings = schema_info.settings;
629 let settings = saved_settings.0.iter().collect::<Vec<SchemaSetting>>();
630 let response = SchemaInfoResponse {
631 schema_id,
632 model_type: schema_info.model_type,
633 payload_location: schema_info.payload_location,
634 settings,
635 };
636 return Some(response);
637 }
638 None
639 }
640
641 pub fn ensure_valid_model(
648 model_type: &ModelType,
649 model: &BoundedVec<u8, T::SchemaModelMaxBytesBoundedVecLimit>,
650 ) -> DispatchResult {
651 match *model_type {
652 ModelType::Parquet => {
653 serde_json::from_slice::<ParquetModel>(model)
654 .map_err(|_| Error::<T>::InvalidSchema)?;
655 },
656 ModelType::AvroBinary => serde::validate_json_model(model.clone().into_inner())
657 .map_err(|_| Error::<T>::InvalidSchema)?,
658 };
659 Ok(())
660 }
661
662 fn get_next_schema_id() -> Result<SchemaId, DispatchError> {
668 let next = CurrentSchemaIdentifierMaximum::<T>::get()
669 .checked_add(1)
670 .ok_or(Error::<T>::SchemaCountOverflow)?;
671
672 Ok(next)
673 }
674
675 pub fn create_schema_for(
688 model: BoundedVec<u8, T::SchemaModelMaxBytesBoundedVecLimit>,
689 model_type: ModelType,
690 payload_location: PayloadLocation,
691 settings: BoundedVec<SchemaSetting, T::MaxSchemaSettingsPerSchema>,
692 optional_schema_name: Option<SchemaNamePayload>,
693 ) -> Result<(SchemaId, Option<SchemaName>), DispatchError> {
694 Self::ensure_valid_model(&model_type, &model)?;
695 ensure!(
696 model.len() >= T::MinSchemaModelSizeBytes::get() as usize,
697 Error::<T>::LessThanMinSchemaModelBytes
698 );
699 ensure!(
700 model.len() <= GovernanceSchemaModelMaxBytes::<T>::get() as usize,
701 Error::<T>::ExceedsMaxSchemaModelBytes
702 );
703 ensure!(
705 !settings.contains(&SchemaSetting::AppendOnly) ||
706 payload_location == PayloadLocation::Itemized,
707 Error::<T>::InvalidSetting
708 );
709 ensure!(
711 !settings.contains(&SchemaSetting::SignatureRequired) ||
712 payload_location == PayloadLocation::Itemized ||
713 payload_location == PayloadLocation::Paginated,
714 Error::<T>::InvalidSetting
715 );
716 let schema_name = match optional_schema_name {
717 None => None,
718 Some(name_payload) => {
719 let parsed_name = SchemaName::try_parse::<T>(name_payload, true)?;
720 Some(parsed_name)
721 },
722 };
723 let schema_id = Self::add_schema(
724 model,
725 model_type,
726 payload_location,
727 settings,
728 schema_name.clone(),
729 )?;
730 Ok((schema_id, schema_name))
731 }
732
733 pub fn get_schema_versions(schema_name: Vec<u8>) -> Option<Vec<SchemaVersionResponse>> {
736 let bounded_name = BoundedVec::try_from(schema_name).ok()?;
737 let parsed_name = SchemaName::try_parse::<T>(bounded_name, false).ok()?;
738 let versions: Vec<_> = match parsed_name.descriptor_exists() {
739 true => SchemaNameToIds::<T>::get(&parsed_name.namespace, &parsed_name.descriptor)
740 .convert_to_response(&parsed_name),
741 false => SchemaNameToIds::<T>::iter_prefix(&parsed_name.namespace)
742 .flat_map(|(descriptor, val)| {
743 val.convert_to_response(&parsed_name.new_with_descriptor(descriptor))
744 })
745 .collect(),
746 };
747 Some(versions)
748 }
749
750 fn parse_and_verify_schema_name(
752 schema_id: SchemaId,
753 schema_name: &SchemaNamePayload,
754 ) -> Result<SchemaName, DispatchError> {
755 let schema_option = SchemaInfos::<T>::get(schema_id);
756 ensure!(schema_option.is_some(), Error::<T>::SchemaIdDoesNotExist);
757 if let Some(info) = schema_option {
758 ensure!(!info.has_name, Error::<T>::SchemaIdAlreadyHasName);
759 }
760 let parsed_name = SchemaName::try_parse::<T>(schema_name.clone(), true)?;
761 Ok(parsed_name)
762 }
763 }
764}
765
766#[allow(clippy::unwrap_used)]
767#[cfg(feature = "runtime-benchmarks")]
768impl<T: Config> SchemaBenchmarkHelper for Pallet<T> {
769 fn set_schema_count(schema_id: SchemaId) {
771 Self::set_schema_count(schema_id);
772 }
773
774 fn create_schema(
776 model: Vec<u8>,
777 model_type: ModelType,
778 payload_location: PayloadLocation,
779 ) -> DispatchResult {
780 let model: BoundedVec<u8, T::SchemaModelMaxBytesBoundedVecLimit> =
781 model.try_into().unwrap();
782 Self::ensure_valid_model(&model_type, &model)?;
783 Self::add_schema(model, model_type, payload_location, BoundedVec::default(), None)?;
784 Ok(())
785 }
786}
787
788impl<T: Config> SchemaValidator<SchemaId> for Pallet<T> {
789 fn are_all_schema_ids_valid(schema_ids: &[SchemaId]) -> bool {
790 let latest_issue_schema_id = CurrentSchemaIdentifierMaximum::<T>::get();
791 schema_ids.iter().all(|id| id <= &latest_issue_schema_id)
792 }
793
794 #[cfg(any(feature = "std", feature = "runtime-benchmarks", test))]
795 fn set_schema_count(n: SchemaId) {
796 Self::set_schema_count(n);
797 }
798}
799
800impl<T: Config> SchemaProvider<SchemaId> for Pallet<T> {
801 fn get_schema_by_id(schema_id: SchemaId) -> Option<SchemaResponse> {
802 Self::get_schema_by_id(schema_id)
803 }
804
805 fn get_schema_info_by_id(schema_id: SchemaId) -> Option<SchemaInfoResponse> {
806 Self::get_schema_info_by_id(schema_id)
807 }
808}