#![doc = include_str!("../README.md")]
#![allow(clippy::expect_used)]
#![cfg_attr(not(feature = "std"), no_std)]
#![allow(rustdoc::private_intra_doc_links)]
#![deny(
rustdoc::broken_intra_doc_links,
rustdoc::missing_crate_level_docs,
rustdoc::invalid_codeblock_attributes,
missing_docs
)]
use common_primitives::{
node::ProposalProvider,
parquet::ParquetModel,
schema::{
ModelType, PayloadLocation, SchemaId, SchemaProvider, SchemaResponse, SchemaSetting,
SchemaSettings, SchemaValidator,
},
};
use frame_support::{
dispatch::{DispatchResult, PostDispatchInfo},
ensure,
traits::{BuildGenesisConfig, Get},
};
use sp_runtime::{traits::Dispatchable, BoundedVec, DispatchError};
use sp_std::{boxed::Box, vec::Vec};
#[cfg(test)]
mod tests;
#[cfg(feature = "runtime-benchmarks")]
mod benchmarking;
#[cfg(feature = "runtime-benchmarks")]
use common_primitives::benchmarks::SchemaBenchmarkHelper;
use common_primitives::schema::{SchemaInfoResponse, SchemaVersionResponse};
pub mod migration;
mod types;
pub use pallet::*;
pub mod weights;
pub use types::*;
pub use weights::*;
mod serde;
const LOG_TARGET: &str = "runtime::schemas";
#[frame_support::pallet]
pub mod pallet {
use super::*;
use frame_support::pallet_prelude::*;
use frame_system::pallet_prelude::*;
#[pallet::config]
pub trait Config: frame_system::Config {
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
type WeightInfo: WeightInfo;
#[pallet::constant]
type MinSchemaModelSizeBytes: Get<u32>;
#[pallet::constant]
type SchemaModelMaxBytesBoundedVecLimit: Get<u32> + MaxEncodedLen;
#[pallet::constant]
type MaxSchemaRegistrations: Get<SchemaId>;
type CreateSchemaViaGovernanceOrigin: EnsureOrigin<Self::RuntimeOrigin>;
type Proposal: Parameter
+ Dispatchable<RuntimeOrigin = Self::RuntimeOrigin, PostInfo = PostDispatchInfo>
+ From<Call<Self>>;
type ProposalProvider: ProposalProvider<Self::AccountId, Self::Proposal>;
#[pallet::constant]
type MaxSchemaSettingsPerSchema: Get<u32>;
}
#[pallet::event]
#[pallet::generate_deposit(pub (super) fn deposit_event)]
pub enum Event<T: Config> {
SchemaCreated {
key: T::AccountId,
schema_id: SchemaId,
},
SchemaMaxSizeChanged {
max_size: u32,
},
SchemaNameCreated {
schema_id: SchemaId,
name: Vec<u8>,
},
}
#[derive(PartialEq, Eq)] #[pallet::error]
pub enum Error<T> {
InvalidSchema,
ExceedsMaxSchemaModelBytes,
LessThanMinSchemaModelBytes,
SchemaCountOverflow,
InvalidSetting,
InvalidSchemaNameEncoding,
InvalidSchemaNameCharacters,
InvalidSchemaNameStructure,
InvalidSchemaNameLength,
InvalidSchemaNamespaceLength,
InvalidSchemaDescriptorLength,
ExceedsMaxNumberOfVersions,
SchemaIdAlreadyExists,
SchemaIdDoesNotExist,
SchemaIdAlreadyHasName,
}
#[pallet::pallet]
#[pallet::storage_version(SCHEMA_STORAGE_VERSION)]
pub struct Pallet<T>(_);
#[pallet::storage]
pub(super) type GovernanceSchemaModelMaxBytes<T: Config> = StorageValue<_, u32, ValueQuery>;
#[pallet::storage]
pub(super) type CurrentSchemaIdentifierMaximum<T: Config> =
StorageValue<_, SchemaId, ValueQuery>;
#[pallet::storage]
pub(super) type SchemaInfos<T: Config> =
StorageMap<_, Twox64Concat, SchemaId, SchemaInfo, OptionQuery>;
#[pallet::storage]
pub(super) type SchemaPayloads<T: Config> = StorageMap<
_,
Twox64Concat,
SchemaId,
BoundedVec<u8, T::SchemaModelMaxBytesBoundedVecLimit>,
OptionQuery,
>;
#[pallet::storage]
pub(super) type SchemaNameToIds<T: Config> = StorageDoubleMap<
_,
Blake2_128Concat,
SchemaNamespace,
Blake2_128Concat,
SchemaDescriptor,
SchemaVersionId,
ValueQuery,
>;
#[pallet::genesis_config]
pub struct GenesisConfig<T: Config> {
pub initial_schema_identifier_max: u16,
pub initial_max_schema_model_size: u32,
pub initial_schemas: Vec<GenesisSchema>,
#[serde(skip)]
pub _config: PhantomData<T>,
}
impl<T: Config> sp_std::default::Default for GenesisConfig<T> {
fn default() -> Self {
Self {
initial_schema_identifier_max: 16_000,
initial_max_schema_model_size: 1024,
initial_schemas: Default::default(),
_config: Default::default(),
}
}
}
#[pallet::genesis_build]
impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
fn build(&self) {
GovernanceSchemaModelMaxBytes::<T>::put(self.initial_max_schema_model_size);
for schema in self.initial_schemas.iter() {
let model: BoundedVec<u8, T::SchemaModelMaxBytesBoundedVecLimit> =
BoundedVec::try_from(schema.model.clone().into_bytes()).expect(
"Genesis Schema Model larger than SchemaModelMaxBytesBoundedVecLimit",
);
let name_payload: SchemaNamePayload =
BoundedVec::try_from(schema.name.clone().into_bytes())
.expect("Genesis Schema Name larger than SCHEMA_NAME_BYTES_MAX");
let parsed_name: Option<SchemaName> = if name_payload.len() > 0 {
Some(
SchemaName::try_parse::<T>(name_payload, true)
.expect("Bad Genesis Schema Name"),
)
} else {
None
};
let settings: BoundedVec<SchemaSetting, T::MaxSchemaSettingsPerSchema> =
BoundedVec::try_from(schema.settings.clone()).expect(
"Bad Genesis Schema Settings. Perhaps larger than MaxSchemaSettingsPerSchema"
);
let _ = Pallet::<T>::add_schema(
model,
schema.model_type,
schema.payload_location,
settings,
parsed_name,
)
.expect("Failed to set Schema in Genesis!");
}
CurrentSchemaIdentifierMaximum::<T>::put(self.initial_schema_identifier_max);
}
}
#[pallet::call]
impl<T: Config> Pallet<T> {
#[pallet::call_index(0)]
#[pallet::weight(T::WeightInfo::create_schema(model.len() as u32))]
#[allow(deprecated)]
#[deprecated(
note = "please use `create_schema_v3` since `create_schema` has been deprecated."
)]
pub fn create_schema(
origin: OriginFor<T>,
model: BoundedVec<u8, T::SchemaModelMaxBytesBoundedVecLimit>,
model_type: ModelType,
payload_location: PayloadLocation,
) -> DispatchResult {
let sender = ensure_signed(origin)?;
let (schema_id, _) = Self::create_schema_for(
model,
model_type,
payload_location,
BoundedVec::default(),
None,
)?;
Self::deposit_event(Event::SchemaCreated { key: sender, schema_id });
Ok(())
}
#[pallet::call_index(1)]
#[pallet::weight((T::WeightInfo::set_max_schema_model_bytes(), DispatchClass::Operational))]
pub fn set_max_schema_model_bytes(
origin: OriginFor<T>,
#[pallet::compact] max_size: u32,
) -> DispatchResult {
ensure_root(origin)?;
ensure!(
max_size <= T::SchemaModelMaxBytesBoundedVecLimit::get(),
Error::<T>::ExceedsMaxSchemaModelBytes
);
GovernanceSchemaModelMaxBytes::<T>::set(max_size);
Self::deposit_event(Event::SchemaMaxSizeChanged { max_size });
Ok(())
}
#[pallet::call_index(2)]
#[pallet::weight(T::WeightInfo::propose_to_create_schema(model.len() as u32))]
#[allow(deprecated)]
#[deprecated(
note = "please use `propose_to_create_schema_v2` since `propose_to_create_schema` has been deprecated."
)]
pub fn propose_to_create_schema(
origin: OriginFor<T>,
model: BoundedVec<u8, T::SchemaModelMaxBytesBoundedVecLimit>,
model_type: ModelType,
payload_location: PayloadLocation,
settings: BoundedVec<SchemaSetting, T::MaxSchemaSettingsPerSchema>,
) -> DispatchResult {
let proposer = ensure_signed(origin)?;
let proposal: Box<T::Proposal> = Box::new(
(Call::<T>::create_schema_via_governance {
creator_key: proposer.clone(),
model,
model_type,
payload_location,
settings,
})
.into(),
);
T::ProposalProvider::propose_with_simple_majority(proposer, proposal)?;
Ok(())
}
#[pallet::call_index(3)]
#[pallet::weight(T::WeightInfo::create_schema_via_governance(model.len() as u32+ settings.len() as u32))]
#[allow(deprecated)]
#[deprecated(
note = "please use `create_schema_via_governance_v2` since `create_schema_via_governance` has been deprecated."
)]
pub fn create_schema_via_governance(
origin: OriginFor<T>,
creator_key: T::AccountId,
model: BoundedVec<u8, T::SchemaModelMaxBytesBoundedVecLimit>,
model_type: ModelType,
payload_location: PayloadLocation,
settings: BoundedVec<SchemaSetting, T::MaxSchemaSettingsPerSchema>,
) -> DispatchResult {
T::CreateSchemaViaGovernanceOrigin::ensure_origin(origin)?;
let (schema_id, _) =
Self::create_schema_for(model, model_type, payload_location, settings, None)?;
Self::deposit_event(Event::SchemaCreated { key: creator_key, schema_id });
Ok(())
}
#[pallet::call_index(4)]
#[pallet::weight(T::WeightInfo::create_schema_v2(model.len() as u32 + settings.len() as u32))]
#[allow(deprecated)]
#[deprecated(
note = "please use `create_schema_v3` since `create_schema_v2` has been deprecated."
)]
pub fn create_schema_v2(
origin: OriginFor<T>,
model: BoundedVec<u8, T::SchemaModelMaxBytesBoundedVecLimit>,
model_type: ModelType,
payload_location: PayloadLocation,
settings: BoundedVec<SchemaSetting, T::MaxSchemaSettingsPerSchema>,
) -> DispatchResult {
let sender = ensure_signed(origin)?;
let (schema_id, _) =
Self::create_schema_for(model, model_type, payload_location, settings, None)?;
Self::deposit_event(Event::SchemaCreated { key: sender, schema_id });
Ok(())
}
#[pallet::call_index(5)]
#[pallet::weight(
match schema_name {
Some(_) => T::WeightInfo::propose_to_create_schema_v2(model.len() as u32),
None => T::WeightInfo::propose_to_create_schema(model.len() as u32)
}
)]
pub fn propose_to_create_schema_v2(
origin: OriginFor<T>,
model: BoundedVec<u8, T::SchemaModelMaxBytesBoundedVecLimit>,
model_type: ModelType,
payload_location: PayloadLocation,
settings: BoundedVec<SchemaSetting, T::MaxSchemaSettingsPerSchema>,
schema_name: Option<SchemaNamePayload>,
) -> DispatchResult {
let proposer = ensure_signed(origin)?;
let proposal: Box<T::Proposal> = Box::new(
(Call::<T>::create_schema_via_governance_v2 {
creator_key: proposer.clone(),
model,
model_type,
payload_location,
settings,
schema_name,
})
.into(),
);
T::ProposalProvider::propose_with_simple_majority(proposer, proposal)?;
Ok(())
}
#[pallet::call_index(6)]
#[pallet::weight(
match schema_name {
Some(_) => T::WeightInfo::create_schema_via_governance_v2(model.len() as u32+ settings.len() as u32),
None => T::WeightInfo::create_schema_via_governance(model.len() as u32+ settings.len() as u32)
}
)]
pub fn create_schema_via_governance_v2(
origin: OriginFor<T>,
creator_key: T::AccountId,
model: BoundedVec<u8, T::SchemaModelMaxBytesBoundedVecLimit>,
model_type: ModelType,
payload_location: PayloadLocation,
settings: BoundedVec<SchemaSetting, T::MaxSchemaSettingsPerSchema>,
schema_name: Option<SchemaNamePayload>,
) -> DispatchResult {
T::CreateSchemaViaGovernanceOrigin::ensure_origin(origin)?;
let (schema_id, schema_name) = Self::create_schema_for(
model,
model_type,
payload_location,
settings,
schema_name,
)?;
Self::deposit_event(Event::SchemaCreated { key: creator_key, schema_id });
if let Some(inner_name) = schema_name {
Self::deposit_event(Event::SchemaNameCreated {
schema_id,
name: inner_name.get_combined_name(),
});
}
Ok(())
}
#[pallet::call_index(7)]
#[pallet::weight(
match schema_name {
Some(_) => T::WeightInfo::create_schema_v3(model.len() as u32 + settings.len() as u32),
None => T::WeightInfo::create_schema_v2(model.len() as u32 + settings.len() as u32)
}
)]
pub fn create_schema_v3(
origin: OriginFor<T>,
model: BoundedVec<u8, T::SchemaModelMaxBytesBoundedVecLimit>,
model_type: ModelType,
payload_location: PayloadLocation,
settings: BoundedVec<SchemaSetting, T::MaxSchemaSettingsPerSchema>,
schema_name: Option<SchemaNamePayload>,
) -> DispatchResult {
let sender = ensure_signed(origin)?;
let (schema_id, schema_name) = Self::create_schema_for(
model,
model_type,
payload_location,
settings,
schema_name,
)?;
Self::deposit_event(Event::SchemaCreated { key: sender, schema_id });
if let Some(inner_name) = schema_name {
Self::deposit_event(Event::SchemaNameCreated {
schema_id,
name: inner_name.get_combined_name(),
});
}
Ok(())
}
#[pallet::call_index(8)]
#[pallet::weight(T::WeightInfo::propose_to_create_schema_name())]
pub fn propose_to_create_schema_name(
origin: OriginFor<T>,
schema_id: SchemaId,
schema_name: SchemaNamePayload,
) -> DispatchResult {
let proposer = ensure_signed(origin)?;
let _ = Self::parse_and_verify_schema_name(schema_id, &schema_name)?;
let proposal: Box<T::Proposal> = Box::new(
(Call::<T>::create_schema_name_via_governance { schema_id, schema_name }).into(),
);
T::ProposalProvider::propose_with_simple_majority(proposer, proposal)?;
Ok(())
}
#[pallet::call_index(9)]
#[pallet::weight(T::WeightInfo::create_schema_name_via_governance())]
pub fn create_schema_name_via_governance(
origin: OriginFor<T>,
schema_id: SchemaId,
schema_name: SchemaNamePayload,
) -> DispatchResult {
T::CreateSchemaViaGovernanceOrigin::ensure_origin(origin)?;
let parsed_name = Self::parse_and_verify_schema_name(schema_id, &schema_name)?;
SchemaNameToIds::<T>::try_mutate(
&parsed_name.namespace,
&parsed_name.descriptor,
|schema_version_id| -> DispatchResult {
schema_version_id.add::<T>(schema_id)?;
Self::deposit_event(Event::SchemaNameCreated {
schema_id,
name: parsed_name.get_combined_name(),
});
Ok(())
},
)
}
}
impl<T: Config> Pallet<T> {
#[cfg(any(feature = "std", feature = "runtime-benchmarks", test))]
pub fn set_schema_count(n: SchemaId) {
<CurrentSchemaIdentifierMaximum<T>>::set(n);
}
pub fn add_schema(
model: BoundedVec<u8, T::SchemaModelMaxBytesBoundedVecLimit>,
model_type: ModelType,
payload_location: PayloadLocation,
settings: BoundedVec<SchemaSetting, T::MaxSchemaSettingsPerSchema>,
schema_name_option: Option<SchemaName>,
) -> Result<SchemaId, DispatchError> {
let schema_id = Self::get_next_schema_id()?;
let has_name = schema_name_option.is_some();
let mut set_settings = SchemaSettings::all_disabled();
if !settings.is_empty() {
for i in settings.into_inner() {
set_settings.set(i);
}
}
if let Some(schema_name) = schema_name_option {
SchemaNameToIds::<T>::try_mutate(
schema_name.namespace,
schema_name.descriptor,
|schema_version_id| -> Result<(), DispatchError> {
schema_version_id.add::<T>(schema_id)?;
Ok(())
},
)?;
};
let schema_info =
SchemaInfo { model_type, payload_location, settings: set_settings, has_name };
<CurrentSchemaIdentifierMaximum<T>>::set(schema_id);
<SchemaInfos<T>>::insert(schema_id, schema_info);
<SchemaPayloads<T>>::insert(schema_id, model);
Ok(schema_id)
}
pub fn get_schema_by_id(schema_id: SchemaId) -> Option<SchemaResponse> {
match (SchemaInfos::<T>::get(schema_id), SchemaPayloads::<T>::get(schema_id)) {
(Some(schema_info), Some(payload)) => {
let model_vec: Vec<u8> = payload.into_inner();
let saved_settings = schema_info.settings;
let settings = saved_settings.0.iter().collect::<Vec<SchemaSetting>>();
let response = SchemaResponse {
schema_id,
model: model_vec,
model_type: schema_info.model_type,
payload_location: schema_info.payload_location,
settings,
};
Some(response)
},
(None, Some(_)) | (Some(_), None) => {
log::error!("Corrupted state for schema {:?}, Should never happen!", schema_id);
None
},
(None, None) => None,
}
}
pub fn get_schema_info_by_id(schema_id: SchemaId) -> Option<SchemaInfoResponse> {
if let Some(schema_info) = SchemaInfos::<T>::get(schema_id) {
let saved_settings = schema_info.settings;
let settings = saved_settings.0.iter().collect::<Vec<SchemaSetting>>();
let response = SchemaInfoResponse {
schema_id,
model_type: schema_info.model_type,
payload_location: schema_info.payload_location,
settings,
};
return Some(response);
}
None
}
pub fn ensure_valid_model(
model_type: &ModelType,
model: &BoundedVec<u8, T::SchemaModelMaxBytesBoundedVecLimit>,
) -> DispatchResult {
match model_type {
&ModelType::Parquet => {
serde_json::from_slice::<ParquetModel>(model)
.map_err(|_| Error::<T>::InvalidSchema)?;
},
&ModelType::AvroBinary => serde::validate_json_model(model.clone().into_inner())
.map_err(|_| Error::<T>::InvalidSchema)?,
};
Ok(())
}
fn get_next_schema_id() -> Result<SchemaId, DispatchError> {
let next = CurrentSchemaIdentifierMaximum::<T>::get()
.checked_add(1)
.ok_or(Error::<T>::SchemaCountOverflow)?;
Ok(next)
}
pub fn create_schema_for(
model: BoundedVec<u8, T::SchemaModelMaxBytesBoundedVecLimit>,
model_type: ModelType,
payload_location: PayloadLocation,
settings: BoundedVec<SchemaSetting, T::MaxSchemaSettingsPerSchema>,
optional_schema_name: Option<SchemaNamePayload>,
) -> Result<(SchemaId, Option<SchemaName>), DispatchError> {
Self::ensure_valid_model(&model_type, &model)?;
ensure!(
model.len() >= T::MinSchemaModelSizeBytes::get() as usize,
Error::<T>::LessThanMinSchemaModelBytes
);
ensure!(
model.len() <= GovernanceSchemaModelMaxBytes::<T>::get() as usize,
Error::<T>::ExceedsMaxSchemaModelBytes
);
ensure!(
!settings.contains(&SchemaSetting::AppendOnly) ||
payload_location == PayloadLocation::Itemized,
Error::<T>::InvalidSetting
);
ensure!(
!settings.contains(&SchemaSetting::SignatureRequired) ||
payload_location == PayloadLocation::Itemized ||
payload_location == PayloadLocation::Paginated,
Error::<T>::InvalidSetting
);
let schema_name = match optional_schema_name {
None => None,
Some(name_payload) => {
let parsed_name = SchemaName::try_parse::<T>(name_payload, true)?;
Some(parsed_name)
},
};
let schema_id = Self::add_schema(
model,
model_type,
payload_location,
settings,
schema_name.clone(),
)?;
Ok((schema_id, schema_name))
}
pub fn get_schema_versions(schema_name: Vec<u8>) -> Option<Vec<SchemaVersionResponse>> {
let bounded_name = BoundedVec::try_from(schema_name).ok()?;
let parsed_name = SchemaName::try_parse::<T>(bounded_name, false).ok()?;
let versions: Vec<_> = match parsed_name.descriptor_exists() {
true => SchemaNameToIds::<T>::get(&parsed_name.namespace, &parsed_name.descriptor)
.convert_to_response(&parsed_name),
false => SchemaNameToIds::<T>::iter_prefix(&parsed_name.namespace)
.flat_map(|(descriptor, val)| {
val.convert_to_response(&parsed_name.new_with_descriptor(descriptor))
})
.collect(),
};
Some(versions)
}
fn parse_and_verify_schema_name(
schema_id: SchemaId,
schema_name: &SchemaNamePayload,
) -> Result<SchemaName, DispatchError> {
let schema_option = SchemaInfos::<T>::get(schema_id);
ensure!(schema_option.is_some(), Error::<T>::SchemaIdDoesNotExist);
if let Some(info) = schema_option {
ensure!(!info.has_name, Error::<T>::SchemaIdAlreadyHasName);
}
let parsed_name = SchemaName::try_parse::<T>(schema_name.clone(), true)?;
Ok(parsed_name)
}
}
}
#[allow(clippy::unwrap_used)]
#[cfg(feature = "runtime-benchmarks")]
impl<T: Config> SchemaBenchmarkHelper for Pallet<T> {
fn set_schema_count(schema_id: SchemaId) {
Self::set_schema_count(schema_id);
}
fn create_schema(
model: Vec<u8>,
model_type: ModelType,
payload_location: PayloadLocation,
) -> DispatchResult {
let model: BoundedVec<u8, T::SchemaModelMaxBytesBoundedVecLimit> =
model.try_into().unwrap();
Self::ensure_valid_model(&model_type, &model)?;
Self::add_schema(model, model_type, payload_location, BoundedVec::default(), None)?;
Ok(())
}
}
impl<T: Config> SchemaValidator<SchemaId> for Pallet<T> {
fn are_all_schema_ids_valid(schema_ids: &Vec<SchemaId>) -> bool {
let latest_issue_schema_id = CurrentSchemaIdentifierMaximum::<T>::get();
schema_ids.iter().all(|id| id <= &latest_issue_schema_id)
}
#[cfg(any(feature = "std", feature = "runtime-benchmarks", test))]
fn set_schema_count(n: SchemaId) {
Self::set_schema_count(n);
}
}
impl<T: Config> SchemaProvider<SchemaId> for Pallet<T> {
fn get_schema_by_id(schema_id: SchemaId) -> Option<SchemaResponse> {
Self::get_schema_by_id(schema_id)
}
fn get_schema_info_by_id(schema_id: SchemaId) -> Option<SchemaInfoResponse> {
Self::get_schema_info_by_id(schema_id)
}
}