#![cfg_attr(not(feature = "std"), no_std)]
mod access_control;
pub mod credentials;
pub mod default_weights;
pub mod migrations;
#[cfg(feature = "runtime-benchmarks")]
mod benchmarking;
#[cfg(any(test, feature = "runtime-benchmarks", feature = "mock"))]
pub mod mock;
#[cfg(test)]
mod tests;
#[cfg(any(test, feature = "try-runtime"))]
mod try_state;
pub use crate::{
access_control::AccessControl as PublicCredentialsAccessControl, credentials::*, default_weights::WeightInfo,
pallet::*,
};
#[frame_support::pallet]
pub mod pallet {
use super::*;
use frame_support::{
pallet_prelude::*,
traits::{
fungible::{Inspect, MutateHold},
IsType, StorageVersion,
},
Parameter,
};
use frame_system::pallet_prelude::*;
use kilt_support::traits::BalanceMigrationManager;
use sp_runtime::{
traits::{Hash, SaturatedConversion},
DispatchError,
};
use sp_std::{boxed::Box, vec::Vec};
pub use ctype::CtypeHashOf;
use kilt_support::{
traits::{CallSources, StorageDepositCollector},
Deposit,
};
const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
pub(crate) type CurrencyOf<T> = <T as Config>::Currency;
pub type InputSubjectIdOf<T> = BoundedVec<u8, <T as Config>::MaxSubjectIdLength>;
pub type InputClaimsContentOf<T> = BoundedVec<u8, <T as Config>::MaxEncodedClaimsLength>;
pub type AccountIdOf<T> = <T as frame_system::Config>::AccountId;
pub type CredentialEntryOf<T> = CredentialEntry<
CtypeHashOf<T>,
AttesterOf<T>,
BlockNumberFor<T>,
AccountIdOf<T>,
BalanceOf<T>,
AuthorizationIdOf<T>,
>;
pub type AttesterOf<T> = <T as Config>::AttesterId;
pub type BalanceOf<T> = <CurrencyOf<T> as Inspect<AccountIdOf<T>>>::Balance;
pub(crate) type BalanceMigrationManagerOf<T> = <T as Config>::BalanceMigrationManager;
pub(crate) type AuthorizationIdOf<T> = <T as Config>::AuthorizationId;
pub type CredentialIdOf<T> = <<T as Config>::CredentialHash as sp_runtime::traits::Hash>::Output;
pub type InputCredentialOf<T> =
Credential<CtypeHashOf<T>, InputSubjectIdOf<T>, InputClaimsContentOf<T>, <T as Config>::AccessControl>;
pub type SubjectIdOf<T> = <T as Config>::SubjectId;
#[pallet::composite_enum]
pub enum HoldReason {
Deposit,
}
#[pallet::config]
pub trait Config: frame_system::Config + ctype::Config {
type AccessControl: Parameter
+ PublicCredentialsAccessControl<
Self::AttesterId,
Self::AuthorizationId,
CtypeHashOf<Self>,
CredentialIdOf<Self>,
>;
type RuntimeHoldReason: From<HoldReason>;
type AttesterId: Parameter + MaxEncodedLen;
type AuthorizationId: Parameter + MaxEncodedLen;
type EnsureOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = <Self as Config>::OriginSuccess>;
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
type CredentialHash: Hash<Output = Self::CredentialId>;
type CredentialId: Parameter + MaxEncodedLen;
type Currency: MutateHold<AccountIdOf<Self>, Reason = Self::RuntimeHoldReason>;
type OriginSuccess: CallSources<Self::AccountId, AttesterOf<Self>>;
type SubjectId: Parameter + MaxEncodedLen + TryFrom<Vec<u8>>;
type WeightInfo: WeightInfo;
#[pallet::constant]
type Deposit: Get<BalanceOf<Self>>;
#[pallet::constant]
type MaxEncodedClaimsLength: Get<u32>;
#[pallet::constant]
type MaxSubjectIdLength: Get<u32>;
type BalanceMigrationManager: BalanceMigrationManager<AccountIdOf<Self>, BalanceOf<Self>>;
}
#[pallet::pallet]
#[pallet::storage_version(STORAGE_VERSION)]
pub struct Pallet<T>(_);
#[pallet::storage]
#[pallet::getter(fn get_credential_info)]
pub type Credentials<T> =
StorageDoubleMap<_, Twox64Concat, SubjectIdOf<T>, Blake2_128Concat, CredentialIdOf<T>, CredentialEntryOf<T>>;
#[pallet::storage]
#[pallet::getter(fn get_credential_subject)]
pub type CredentialSubjects<T> = StorageMap<_, Blake2_128Concat, CredentialIdOf<T>, SubjectIdOf<T>>;
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
CredentialStored {
subject_id: T::SubjectId,
credential_id: CredentialIdOf<T>,
},
CredentialRemoved {
subject_id: T::SubjectId,
credential_id: CredentialIdOf<T>,
},
CredentialRevoked {
credential_id: CredentialIdOf<T>,
},
CredentialUnrevoked {
credential_id: CredentialIdOf<T>,
},
}
#[pallet::error]
pub enum Error<T> {
AlreadyAttested,
NotFound,
UnableToPayFees,
InvalidInput,
NotAuthorized,
Internal,
}
#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
#[cfg(feature = "try-runtime")]
fn try_state(_n: BlockNumberFor<T>) -> Result<(), sp_runtime::TryRuntimeError> {
crate::try_state::do_try_state::<T>()
}
}
#[pallet::call]
impl<T: Config> Pallet<T> {
#[allow(clippy::boxed_local)]
#[pallet::call_index(0)]
#[pallet::weight({
let xt_weight = <T as Config>::WeightInfo::add(credential.claims.len().saturated_into::<u32>());
let ac_weight = credential.authorization.as_ref().map(|ac| ac.can_issue_weight()).unwrap_or(Weight::zero());
xt_weight.saturating_add(ac_weight)
})]
pub fn add(origin: OriginFor<T>, credential: Box<InputCredentialOf<T>>) -> DispatchResultWithPostInfo {
let source = <T as Config>::EnsureOrigin::ensure_origin(origin)?;
let attester = source.subject();
let payer = source.sender();
let deposit_amount = <T as Config>::Deposit::get();
let Credential {
ctype_hash,
subject,
claims: _,
authorization,
} = *credential.clone();
ensure!(
ctype::Ctypes::<T>::contains_key(ctype_hash),
ctype::Error::<T>::NotFound
);
let credential_id =
T::CredentialHash::hash(&[&credential.encode()[..], &attester.encode()[..]].concat()[..]);
let ac_weight = authorization
.as_ref()
.map(|ac| ac.can_issue(&attester, &ctype_hash, &credential_id))
.transpose()
.map_err(|_| Error::<T>::NotAuthorized)?;
let authorization_id = authorization.as_ref().map(|ac| ac.authorization_id());
let subject = T::SubjectId::try_from(subject.into_inner()).map_err(|_| Error::<T>::InvalidInput)?;
ensure!(
!Credentials::<T>::contains_key(&subject, &credential_id),
Error::<T>::AlreadyAttested
);
let deposit = PublicCredentialDepositCollector::<T>::create_deposit(payer, deposit_amount)
.map_err(|_| Error::<T>::UnableToPayFees)?;
<T as Config>::BalanceMigrationManager::exclude_key_from_migration(&Credentials::<T>::hashed_key_for(
&subject,
&credential_id,
));
let block_number = frame_system::Pallet::<T>::block_number();
Credentials::<T>::insert(
&subject,
&credential_id,
CredentialEntryOf::<T> {
revoked: false,
attester,
deposit,
block_number,
ctype_hash,
authorization_id,
},
);
CredentialSubjects::<T>::insert(&credential_id, subject.clone());
Self::deposit_event(Event::CredentialStored {
subject_id: subject,
credential_id,
});
Ok(Some(
<T as Config>::WeightInfo::add(credential.claims.len().saturated_into::<u32>())
.saturating_add(ac_weight.unwrap_or(Weight::zero())),
)
.into())
}
#[pallet::call_index(1)]
#[pallet::weight({
let xt_weight = <T as Config>::WeightInfo::revoke();
let ac_weight = authorization.as_ref().map(|ac| ac.can_revoke_weight()).unwrap_or(Weight::zero());
xt_weight.saturating_add(ac_weight)
})]
pub fn revoke(
origin: OriginFor<T>,
credential_id: CredentialIdOf<T>,
authorization: Option<T::AccessControl>,
) -> DispatchResultWithPostInfo {
let source = <T as Config>::EnsureOrigin::ensure_origin(origin)?;
let caller = source.subject();
let credential_subject = CredentialSubjects::<T>::get(&credential_id).ok_or(Error::<T>::NotFound)?;
let ac_weight_used = Self::set_credential_revocation_status(
&caller,
&credential_subject,
&credential_id,
authorization,
true,
)?;
Self::deposit_event(Event::CredentialRevoked { credential_id });
Ok(Some(<T as Config>::WeightInfo::revoke().saturating_add(ac_weight_used)).into())
}
#[pallet::call_index(2)]
#[pallet::weight({
let xt_weight = <T as Config>::WeightInfo::unrevoke();
let ac_weight = authorization.as_ref().map(|ac| ac.can_unrevoke_weight()).unwrap_or(Weight::zero());
xt_weight.saturating_add(ac_weight)
})]
pub fn unrevoke(
origin: OriginFor<T>,
credential_id: CredentialIdOf<T>,
authorization: Option<T::AccessControl>,
) -> DispatchResultWithPostInfo {
let source = <T as Config>::EnsureOrigin::ensure_origin(origin)?;
let caller = source.subject();
let credential_subject = CredentialSubjects::<T>::get(&credential_id).ok_or(Error::<T>::NotFound)?;
let ac_weight_used = Self::set_credential_revocation_status(
&caller,
&credential_subject,
&credential_id,
authorization,
false,
)?;
Self::deposit_event(Event::CredentialUnrevoked { credential_id });
Ok(Some(<T as Config>::WeightInfo::unrevoke().saturating_add(ac_weight_used)).into())
}
#[pallet::call_index(3)]
#[pallet::weight({
let xt_weight = <T as Config>::WeightInfo::remove();
let ac_weight = authorization.as_ref().map(|ac| ac.can_remove_weight()).unwrap_or(Weight::zero());
xt_weight.saturating_add(ac_weight)
})]
pub fn remove(
origin: OriginFor<T>,
credential_id: CredentialIdOf<T>,
authorization: Option<T::AccessControl>,
) -> DispatchResultWithPostInfo {
let source = <T as Config>::EnsureOrigin::ensure_origin(origin)?;
let caller = source.subject();
let (credential_subject, credential_entry) = Self::retrieve_credential_entry(&credential_id)?;
let ac_weight_used = if credential_entry.attester == caller {
Weight::zero()
} else {
let credential_auth_id = credential_entry
.authorization_id
.as_ref()
.ok_or(Error::<T>::NotAuthorized)?;
authorization
.ok_or(Error::<T>::NotAuthorized)?
.can_remove(
&caller,
&credential_entry.ctype_hash,
&credential_id,
credential_auth_id,
)
.map_err(|_| Error::<T>::NotAuthorized)?
};
Self::remove_credential_entry(credential_subject, credential_id, credential_entry)?;
Ok(Some(<T as Config>::WeightInfo::remove().saturating_add(ac_weight_used)).into())
}
#[pallet::call_index(4)]
#[pallet::weight(<T as pallet::Config>::WeightInfo::reclaim_deposit())]
pub fn reclaim_deposit(origin: OriginFor<T>, credential_id: CredentialIdOf<T>) -> DispatchResult {
let submitter = ensure_signed(origin)?;
let (credential_subject, credential_entry) = Self::retrieve_credential_entry(&credential_id)?;
ensure!(submitter == credential_entry.deposit.owner, Error::<T>::NotAuthorized);
Self::remove_credential_entry(credential_subject, credential_id, credential_entry)?;
Ok(())
}
#[pallet::call_index(5)]
#[pallet::weight(<T as Config>::WeightInfo::change_deposit_owner())]
pub fn change_deposit_owner(origin: OriginFor<T>, credential_id: CredentialIdOf<T>) -> DispatchResult {
let source = <T as Config>::EnsureOrigin::ensure_origin(origin)?;
let subject = source.subject();
let (_, credential_entry) = Self::retrieve_credential_entry(&credential_id)?;
ensure!(subject == credential_entry.attester, Error::<T>::NotAuthorized);
PublicCredentialDepositCollector::<T>::change_deposit_owner::<BalanceMigrationManagerOf<T>>(
&credential_id,
source.sender(),
)?;
Ok(())
}
#[pallet::call_index(6)]
#[pallet::weight(<T as Config>::WeightInfo::update_deposit())]
pub fn update_deposit(origin: OriginFor<T>, credential_id: CredentialIdOf<T>) -> DispatchResult {
let source = ensure_signed(origin)?;
let (_, credential_entry) = Self::retrieve_credential_entry(&credential_id)?;
ensure!(source == credential_entry.deposit.owner, Error::<T>::NotAuthorized);
PublicCredentialDepositCollector::<T>::update_deposit::<BalanceMigrationManagerOf<T>>(&credential_id)?;
Ok(())
}
}
impl<T: Config> Pallet<T> {
fn remove_credential_entry(
credential_subject: T::SubjectId,
credential_id: CredentialIdOf<T>,
credential: CredentialEntryOf<T>,
) -> DispatchResult {
let details = Credentials::<T>::take(&credential_subject, &credential_id).ok_or(Error::<T>::NotFound)?;
CredentialSubjects::<T>::remove(&credential_id);
let is_key_migrated = <T as Config>::BalanceMigrationManager::is_key_migrated(
&Credentials::<T>::hashed_key_for(&credential_subject, &credential_id),
);
if is_key_migrated {
PublicCredentialDepositCollector::<T>::free_deposit(credential.deposit)?;
} else {
<T as Config>::BalanceMigrationManager::release_reserved_deposit(
&details.deposit.owner,
&details.deposit.amount,
);
}
Self::deposit_event(Event::CredentialRemoved {
subject_id: credential_subject,
credential_id,
});
Ok(())
}
fn retrieve_credential_entry(
credential_id: &CredentialIdOf<T>,
) -> Result<(T::SubjectId, CredentialEntryOf<T>), Error<T>> {
let credential_subject = CredentialSubjects::<T>::get(credential_id).ok_or(Error::<T>::NotFound)?;
Credentials::<T>::get(&credential_subject, credential_id)
.map(|entry| (credential_subject, entry))
.ok_or(Error::<T>::Internal)
}
fn set_credential_revocation_status(
caller: &AttesterOf<T>,
credential_subject: &T::SubjectId,
credential_id: &CredentialIdOf<T>,
authorization: Option<T::AccessControl>,
revocation: bool,
) -> Result<Weight, Error<T>> {
Credentials::<T>::try_mutate(credential_subject, credential_id, |credential_entry| {
if let Some(credential) = credential_entry {
let additional_weight = if *caller == credential.attester {
Weight::zero()
} else {
let credential_auth_id =
credential.authorization_id.as_ref().ok_or(Error::<T>::NotAuthorized)?;
authorization
.ok_or(Error::<T>::NotAuthorized)?
.can_revoke(caller, &credential.ctype_hash, credential_id, credential_auth_id)
.map_err(|_| Error::<T>::NotAuthorized)?
};
credential.revoked = revocation;
Ok(additional_weight)
} else {
Err(Error::<T>::NotFound)
}
})
}
}
pub(crate) struct PublicCredentialDepositCollector<T: Config>(PhantomData<T>);
impl<T: Config> StorageDepositCollector<AccountIdOf<T>, CredentialIdOf<T>, T::RuntimeHoldReason>
for PublicCredentialDepositCollector<T>
{
type Currency = <T as Config>::Currency;
type Reason = HoldReason;
fn reason() -> Self::Reason {
HoldReason::Deposit
}
fn get_hashed_key(credential_id: &CredentialIdOf<T>) -> Result<sp_std::vec::Vec<u8>, DispatchError> {
let credential_subject = CredentialSubjects::<T>::get(credential_id).ok_or(Error::<T>::NotFound)?;
Ok(Credentials::<T>::hashed_key_for(&credential_subject, credential_id))
}
fn deposit(
credential_id: &CredentialIdOf<T>,
) -> Result<Deposit<AccountIdOf<T>, <Self::Currency as Inspect<AccountIdOf<T>>>::Balance>, DispatchError> {
let (_, credential_entry) = Pallet::<T>::retrieve_credential_entry(credential_id)?;
Ok(credential_entry.deposit)
}
fn deposit_amount(_credential_id: &CredentialIdOf<T>) -> <Self::Currency as Inspect<AccountIdOf<T>>>::Balance {
T::Deposit::get()
}
fn store_deposit(
credential_id: &CredentialIdOf<T>,
deposit: Deposit<AccountIdOf<T>, <Self::Currency as Inspect<AccountIdOf<T>>>::Balance>,
) -> Result<(), DispatchError> {
let credential_subject = CredentialSubjects::<T>::get(credential_id).ok_or(Error::<T>::NotFound)?;
Credentials::<T>::try_mutate(&credential_subject, credential_id, |credential_entry| {
if let Some(credential) = credential_entry {
credential.deposit = deposit;
Ok(())
} else {
Err(Error::<T>::NotFound.into())
}
})
}
}
}