#![cfg_attr(not(feature = "std"), no_std)]
#[cfg(test)]
mod mock;
#[cfg(test)]
mod tests;
mod impls;
pub mod traits;
mod types;
pub mod weights;
#[cfg(feature = "runtime-benchmarks")]
mod benchmarking;
use codec::{Codec, Decode, Encode};
use frame_support::dispatch::DispatchResultWithPostInfo;
use frame_support::ensure;
use frame_support::pallet_prelude::Get;
use frame_support::pallet_prelude::RuntimeDebug;
use frame_support::pallet_prelude::Weight;
use frame_system::ensure_signed;
use frame_system::pallet_prelude::OriginFor;
use scale_info::TypeInfo;
use sp_runtime::traits::AtLeast32BitUnsigned;
use sp_runtime::traits::IsMember;
use sp_std::fmt::Debug;
use sp_std::prelude::*;
use crate::traits::OnSmithDelete;
pub use crate::weights::WeightInfo;
pub use pallet::*;
use pallet_authority_members::SessionIndex;
pub use types::*;
#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo)]
pub enum SmithRemovalReason {
LostMembership,
OfflineTooLong,
Blacklisted,
}
#[derive(Encode, Decode, Copy, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo)]
pub enum SmithStatus {
Invited,
Pending,
Smith,
Excluded,
}
#[frame_support::pallet]
pub mod pallet {
use super::*;
use frame_support::pallet_prelude::*;
use frame_support::traits::StorageVersion;
use pallet_authority_members::SessionIndex;
use sp_runtime::traits::{Convert, IsMember};
use sp_std::collections::btree_map::BTreeMap;
use sp_std::vec;
use sp_std::vec::Vec;
const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
#[pallet::pallet]
#[pallet::storage_version(STORAGE_VERSION)]
#[pallet::without_storage_info]
pub struct Pallet<T>(_);
#[pallet::config]
pub trait Config: frame_system::Config {
type IsWoTMember: IsMember<Self::IdtyIndex>;
type OnSmithDelete: traits::OnSmithDelete<Self::IdtyIndex>;
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
type IdtyIndex: Parameter
+ Member
+ AtLeast32BitUnsigned
+ Codec
+ Default
+ Copy
+ MaybeSerializeDeserialize
+ Debug
+ MaxEncodedLen;
type MemberId: Copy + Ord + MaybeSerializeDeserialize + Parameter;
type IdtyIdOf: Convert<Self::AccountId, Option<Self::IdtyIndex>>;
type OwnerKeyOf: Convert<Self::IdtyIndex, Option<Self::AccountId>>;
type IdtyIdOfAuthorityId: Convert<Self::MemberId, Option<Self::IdtyIndex>>;
#[pallet::constant]
type MaxByIssuer: Get<u32>;
#[pallet::constant]
type MinCertForMembership: Get<u32>;
#[pallet::constant]
type SmithInactivityMaxDuration: Get<u32>;
type WeightInfo: WeightInfo;
}
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
InvitationSent {
issuer: T::IdtyIndex,
receiver: T::IdtyIndex,
},
InvitationAccepted { idty_index: T::IdtyIndex },
SmithCertAdded {
issuer: T::IdtyIndex,
receiver: T::IdtyIndex,
},
SmithCertRemoved {
issuer: T::IdtyIndex,
receiver: T::IdtyIndex,
},
SmithMembershipAdded { idty_index: T::IdtyIndex },
SmithMembershipRemoved { idty_index: T::IdtyIndex },
}
#[pallet::genesis_config]
pub struct GenesisConfig<T: Config> {
pub initial_smiths: BTreeMap<T::IdtyIndex, (bool, Vec<T::IdtyIndex>)>,
}
impl<T: Config> Default for GenesisConfig<T> {
fn default() -> Self {
Self {
initial_smiths: Default::default(),
}
}
}
#[pallet::genesis_build]
impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
fn build(&self) {
CurrentSession::<T>::put(0);
let mut cert_meta_by_issuer = BTreeMap::<T::IdtyIndex, Vec<T::IdtyIndex>>::new();
for (receiver, (is_online, issuers)) in &self.initial_smiths {
assert!(
!issuers.contains(receiver),
"Identity cannot certify it-self."
);
let mut issuers_: Vec<_> = Vec::with_capacity(issuers.len());
for issuer in issuers {
cert_meta_by_issuer
.entry(*issuer)
.or_insert(vec![])
.push(*receiver);
issuers_.push(*issuer);
}
issuers_.sort();
let issuers_count = issuers_.len();
let smith_status = if issuers_count >= T::MinCertForMembership::get() as usize {
SmithStatus::Smith
} else {
SmithStatus::Pending
};
Smiths::<T>::insert(
receiver,
SmithMeta {
status: smith_status,
expires_on: if *is_online {
None
} else {
Some(CurrentSession::<T>::get() + T::SmithInactivityMaxDuration::get())
},
issued_certs: vec![],
received_certs: issuers_,
},
);
ExpiresOn::<T>::append(
CurrentSession::<T>::get() + T::SmithInactivityMaxDuration::get(),
receiver,
);
}
for (issuer, issued_certs) in cert_meta_by_issuer {
Smiths::<T>::mutate(issuer, |maybe_smith_meta| {
if let Some(smith_meta) = maybe_smith_meta {
smith_meta.issued_certs = issued_certs;
}
});
}
}
}
#[pallet::storage]
#[pallet::getter(fn smiths)]
pub type Smiths<T: Config> =
StorageMap<_, Twox64Concat, T::IdtyIndex, SmithMeta<T::IdtyIndex>, OptionQuery>;
#[pallet::storage]
#[pallet::getter(fn expires_on)]
pub type ExpiresOn<T: Config> =
StorageMap<_, Twox64Concat, SessionIndex, Vec<T::IdtyIndex>, OptionQuery>;
#[pallet::storage]
#[pallet::getter(fn current_session)]
pub type CurrentSession<T: Config> = StorageValue<_, SessionIndex, ValueQuery>;
#[pallet::error]
pub enum Error<T> {
OriginMustHaveAnIdentity,
OriginHasNeverBeenInvited,
InvitationIsASmithPrivilege,
InvitationIsAOnlineSmithPrivilege,
InvitationAlreadyAccepted,
InvitationOfExistingNonExcluded,
InvitationOfNonMember,
CertificationMustBeAgreed,
CertificationOnExcludedIsForbidden,
CertificationIsASmithPrivilege,
CertificationIsAOnlineSmithPrivilege,
CertificationOfSelfIsForbidden,
CertificationReceiverMustHaveBeenInvited,
CertificationAlreadyExists,
CertificationStockFullyConsumed,
}
#[pallet::call]
impl<T: Config> Pallet<T> {
#[pallet::call_index(0)]
#[pallet::weight(T::WeightInfo::invite_smith())]
pub fn invite_smith(
origin: OriginFor<T>,
receiver: T::IdtyIndex,
) -> DispatchResultWithPostInfo {
let who = ensure_signed(origin.clone())?;
let issuer =
T::IdtyIdOf::convert(who.clone()).ok_or(Error::<T>::OriginMustHaveAnIdentity)?;
Self::check_invite_smith(issuer, receiver)?;
Self::do_invite_smith(issuer, receiver);
Ok(().into())
}
#[pallet::call_index(1)]
#[pallet::weight(T::WeightInfo::accept_invitation())]
pub fn accept_invitation(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
let who = ensure_signed(origin.clone())?;
let receiver =
T::IdtyIdOf::convert(who.clone()).ok_or(Error::<T>::OriginMustHaveAnIdentity)?;
Self::check_accept_invitation(receiver)?;
Self::do_accept_invitation(receiver)?;
Ok(().into())
}
#[pallet::call_index(2)]
#[pallet::weight(T::WeightInfo::certify_smith())]
pub fn certify_smith(
origin: OriginFor<T>,
receiver: T::IdtyIndex,
) -> DispatchResultWithPostInfo {
let who = ensure_signed(origin)?;
let issuer =
T::IdtyIdOf::convert(who.clone()).ok_or(Error::<T>::OriginMustHaveAnIdentity)?;
Self::check_certify_smith(issuer, receiver)?;
Self::do_certify_smith(receiver, issuer);
Ok(().into())
}
}
}
impl<T: Config> Pallet<T> {
fn check_invite_smith(
issuer: T::IdtyIndex,
receiver: T::IdtyIndex,
) -> DispatchResultWithPostInfo {
let issuer = Smiths::<T>::get(issuer).ok_or(Error::<T>::OriginHasNeverBeenInvited)?;
ensure!(
issuer.status == SmithStatus::Smith,
Error::<T>::InvitationIsASmithPrivilege
);
ensure!(
issuer.expires_on.is_none(),
Error::<T>::InvitationIsAOnlineSmithPrivilege
);
if let Some(receiver_meta) = Smiths::<T>::get(receiver) {
ensure!(
receiver_meta.status == SmithStatus::Excluded,
Error::<T>::InvitationOfExistingNonExcluded
);
}
ensure!(
T::IsWoTMember::is_member(&receiver),
Error::<T>::InvitationOfNonMember
);
Ok(().into())
}
fn do_invite_smith(issuer: T::IdtyIndex, receiver: T::IdtyIndex) {
let new_expires_on = CurrentSession::<T>::get() + T::SmithInactivityMaxDuration::get();
let mut existing = Smiths::<T>::get(receiver).unwrap_or_default();
existing.status = SmithStatus::Invited;
existing.expires_on = Some(new_expires_on);
existing.received_certs = vec![];
Smiths::<T>::insert(receiver, existing);
ExpiresOn::<T>::append(new_expires_on, receiver);
Self::deposit_event(Event::<T>::InvitationSent { issuer, receiver });
}
fn check_accept_invitation(receiver: T::IdtyIndex) -> DispatchResultWithPostInfo {
let pretender_status = Smiths::<T>::get(receiver)
.ok_or(Error::<T>::OriginHasNeverBeenInvited)?
.status;
ensure!(
pretender_status == SmithStatus::Invited,
Error::<T>::InvitationAlreadyAccepted
);
Ok(().into())
}
fn do_accept_invitation(receiver: T::IdtyIndex) -> DispatchResultWithPostInfo {
Smiths::<T>::mutate(receiver, |maybe_smith_meta| {
if let Some(smith_meta) = maybe_smith_meta {
smith_meta.status = SmithStatus::Pending;
}
});
Self::deposit_event(Event::<T>::InvitationAccepted {
idty_index: receiver,
});
Ok(().into())
}
fn check_certify_smith(
issuer_index: T::IdtyIndex,
receiver_index: T::IdtyIndex,
) -> DispatchResultWithPostInfo {
ensure!(
issuer_index != receiver_index,
Error::<T>::CertificationOfSelfIsForbidden
);
let issuer = Smiths::<T>::get(issuer_index).ok_or(Error::<T>::OriginHasNeverBeenInvited)?;
ensure!(
issuer.status == SmithStatus::Smith,
Error::<T>::CertificationIsASmithPrivilege
);
ensure!(
issuer.expires_on.is_none(),
Error::<T>::CertificationIsAOnlineSmithPrivilege
);
let issued_certs = issuer.issued_certs.len();
ensure!(
issued_certs < T::MaxByIssuer::get() as usize,
Error::<T>::CertificationStockFullyConsumed
);
let receiver = Smiths::<T>::get(receiver_index)
.ok_or(Error::<T>::CertificationReceiverMustHaveBeenInvited)?;
ensure!(
receiver.status != SmithStatus::Invited,
Error::<T>::CertificationMustBeAgreed
);
ensure!(
receiver.status != SmithStatus::Excluded,
Error::<T>::CertificationOnExcludedIsForbidden
);
ensure!(
receiver
.received_certs
.binary_search(&issuer_index)
.is_err(),
Error::<T>::CertificationAlreadyExists
);
Ok(().into())
}
fn do_certify_smith(receiver: T::IdtyIndex, issuer: T::IdtyIndex) {
Smiths::<T>::mutate(issuer, |maybe_smith_meta| {
if let Some(smith_meta) = maybe_smith_meta {
smith_meta.issued_certs.push(receiver);
smith_meta.issued_certs.sort();
}
});
Smiths::<T>::mutate(receiver, |maybe_smith_meta| {
if let Some(smith_meta) = maybe_smith_meta {
smith_meta.received_certs.push(issuer);
smith_meta.received_certs.sort();
Self::deposit_event(Event::<T>::SmithCertAdded { issuer, receiver });
let previous_status = smith_meta.status;
smith_meta.status =
if smith_meta.received_certs.len() >= T::MinCertForMembership::get() as usize {
SmithStatus::Smith
} else {
SmithStatus::Pending
};
if previous_status != SmithStatus::Smith {
let new_expires_on =
CurrentSession::<T>::get() + T::SmithInactivityMaxDuration::get();
smith_meta.expires_on = Some(new_expires_on);
ExpiresOn::<T>::append(new_expires_on, receiver);
}
if smith_meta.status == SmithStatus::Smith && previous_status != SmithStatus::Smith
{
Self::deposit_event(Event::<T>::SmithMembershipAdded {
idty_index: receiver,
});
}
}
});
}
fn on_exclude_expired_smiths(at: SessionIndex) {
if let Some(smiths_to_remove) = ExpiresOn::<T>::get(at) {
for smith in smiths_to_remove {
if let Some(smith_meta) = Smiths::<T>::get(smith) {
if let Some(expires_on) = smith_meta.expires_on {
if expires_on == at {
Self::_do_exclude_smith(smith, SmithRemovalReason::OfflineTooLong);
}
}
}
}
}
}
pub fn on_removed_wot_member(idty_index: T::IdtyIndex) -> Weight {
let mut weight = T::WeightInfo::on_removed_wot_member_empty();
if Smiths::<T>::get(idty_index).is_some() {
Self::_do_exclude_smith(idty_index, SmithRemovalReason::LostMembership);
weight = weight.saturating_add(T::WeightInfo::on_removed_wot_member());
}
weight
}
fn _do_exclude_smith(receiver: T::IdtyIndex, reason: SmithRemovalReason) {
let mut lost_certs = vec![];
Smiths::<T>::mutate(receiver, |maybe_smith_meta| {
if let Some(smith_meta) = maybe_smith_meta {
smith_meta.expires_on = None;
smith_meta.status = SmithStatus::Excluded;
for cert in &smith_meta.received_certs {
lost_certs.push(*cert);
}
smith_meta.received_certs = vec![];
}
});
for lost_cert_issuer in lost_certs {
Smiths::<T>::mutate(lost_cert_issuer, |maybe_smith_meta| {
if let Some(smith_meta) = maybe_smith_meta {
if let Ok(index) = smith_meta.issued_certs.binary_search(&receiver) {
smith_meta.issued_certs.remove(index);
Self::deposit_event(Event::<T>::SmithCertRemoved {
issuer: lost_cert_issuer,
receiver,
});
}
}
});
}
T::OnSmithDelete::on_smith_delete(receiver, reason);
Self::deposit_event(Event::<T>::SmithMembershipRemoved {
idty_index: receiver,
});
}
pub fn on_smith_goes_online(idty_index: T::IdtyIndex) {
if let Some(smith_meta) = Smiths::<T>::get(idty_index) {
if smith_meta.expires_on.is_some() {
Smiths::<T>::mutate(idty_index, |maybe_smith_meta| {
if let Some(smith_meta) = maybe_smith_meta {
smith_meta.expires_on = None;
}
});
}
}
}
pub fn on_smith_goes_offline(idty_index: T::IdtyIndex) {
if let Some(smith_meta) = Smiths::<T>::get(idty_index) {
if smith_meta.expires_on.is_none() {
Smiths::<T>::mutate(idty_index, |maybe_smith_meta| {
if let Some(smith_meta) = maybe_smith_meta {
let new_expires_on =
CurrentSession::<T>::get() + T::SmithInactivityMaxDuration::get();
smith_meta.expires_on = Some(new_expires_on);
ExpiresOn::<T>::append(new_expires_on, idty_index);
}
});
}
}
}
fn provide_is_member(idty_id: &T::IdtyIndex) -> bool {
let Some(smith) = Smiths::<T>::get(idty_id) else {
return false;
};
smith.status == SmithStatus::Smith
}
}
impl<T: Config> sp_runtime::traits::IsMember<T::IdtyIndex> for Pallet<T> {
fn is_member(idty_id: &T::IdtyIndex) -> bool {
Self::provide_is_member(idty_id)
}
}