#![cfg_attr(not(feature = "std"), no_std)]
#![allow(clippy::type_complexity)]
pub mod impls;
pub mod traits;
mod types;
pub mod weights;
#[cfg(test)]
mod mock;
#[cfg(test)]
mod tests;
#[cfg(feature = "runtime-benchmarks")]
mod benchmarking;
pub use pallet::*;
pub use sp_staking::SessionIndex;
pub use traits::*;
pub use types::*;
pub use weights::WeightInfo;
use frame_support::traits::Get;
use sp_runtime::traits::Convert;
use sp_std::prelude::*;
#[frame_support::pallet]
pub mod pallet {
use super::*;
use frame_support::pallet_prelude::*;
use frame_support::traits::ValidatorRegistration;
use frame_support::traits::{StorageVersion, UnfilteredDispatchable};
use frame_system::pallet_prelude::*;
use sp_runtime::traits::{Convert, IsMember};
use sp_std::collections::btree_map::BTreeMap;
use sp_std::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 + pallet_session::Config + pallet_session::historical::Config
{
type IsMember: IsMember<Self::MemberId>;
type OnNewSession: OnNewSession;
type OnOutgoingMember: OnOutgoingMember<Self::MemberId>;
type OnIncomingMember: OnIncomingMember<Self::MemberId>;
#[pallet::constant]
type MaxAuthorities: Get<u32>;
type MemberId: Copy + Ord + MaybeSerializeDeserialize + Parameter;
type MemberIdOf: Convert<Self::AccountId, Option<Self::MemberId>>;
type RemoveMemberOrigin: EnsureOrigin<Self::RuntimeOrigin>;
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
type WeightInfo: WeightInfo;
}
#[pallet::genesis_config]
pub struct GenesisConfig<T: Config> {
pub initial_authorities: BTreeMap<T::MemberId, (T::AccountId, bool)>,
}
impl<T: Config> Default for GenesisConfig<T> {
fn default() -> Self {
Self {
initial_authorities: BTreeMap::new(),
}
}
}
#[pallet::genesis_build]
impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
fn build(&self) {
for (member_id, (account_id, _is_online)) in &self.initial_authorities {
Members::<T>::insert(member_id, MemberData::new_genesis(account_id.to_owned()));
}
let mut members_ids = self
.initial_authorities
.iter()
.filter_map(
|(member_id, (_account_id, is_online))| {
if *is_online {
Some(*member_id)
} else {
None
}
},
)
.collect::<Vec<T::MemberId>>();
members_ids.sort();
OnlineAuthorities::<T>::put(members_ids.clone());
}
}
#[pallet::storage]
#[pallet::getter(fn incoming)]
pub type IncomingAuthorities<T: Config> = StorageValue<_, Vec<T::MemberId>, ValueQuery>;
#[pallet::storage]
#[pallet::getter(fn online)]
pub type OnlineAuthorities<T: Config> = StorageValue<_, Vec<T::MemberId>, ValueQuery>;
#[pallet::storage]
#[pallet::getter(fn outgoing)]
pub type OutgoingAuthorities<T: Config> = StorageValue<_, Vec<T::MemberId>, ValueQuery>;
#[pallet::storage]
#[pallet::getter(fn member)]
pub type Members<T: Config> =
StorageMap<_, Twox64Concat, T::MemberId, MemberData<T::AccountId>, OptionQuery>;
#[pallet::storage]
#[pallet::getter(fn blacklist)]
pub type Blacklist<T: Config> = StorageValue<_, Vec<T::MemberId>, ValueQuery>;
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
IncomingAuthorities { members: Vec<T::MemberId> },
OutgoingAuthorities { members: Vec<T::MemberId> },
MemberGoOffline { member: T::MemberId },
MemberGoOnline { member: T::MemberId },
MemberRemoved { member: T::MemberId },
MemberRemovedFromBlacklist { member: T::MemberId },
MemberAddedToBlacklist { member: T::MemberId },
}
#[pallet::error]
pub enum Error<T> {
AlreadyIncoming,
AlreadyOnline,
AlreadyOutgoing,
MemberIdNotFound,
MemberBlacklisted,
MemberNotBlacklisted,
MemberNotFound,
NotOnlineNorIncoming,
NotMember,
SessionKeysNotProvided,
TooManyAuthorities,
}
#[pallet::call]
impl<T: Config> Pallet<T> {
#[pallet::call_index(0)]
#[pallet::weight(<T as pallet::Config>::WeightInfo::go_offline())]
pub fn go_offline(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
let who = ensure_signed(origin)?;
let member_id = Self::verify_ownership_and_membership(&who)?;
if !Members::<T>::contains_key(member_id) {
return Err(Error::<T>::MemberNotFound.into());
}
if Self::is_outgoing(member_id) {
return Err(Error::<T>::AlreadyOutgoing.into());
}
let is_incoming = Self::is_incoming(member_id);
if !is_incoming && !Self::is_online(member_id) {
return Err(Error::<T>::NotOnlineNorIncoming.into());
}
if is_incoming {
Self::remove_in(member_id);
} else {
Self::insert_out(member_id);
}
Ok(().into())
}
#[pallet::call_index(1)]
#[pallet::weight(<T as pallet::Config>::WeightInfo::go_online())]
pub fn go_online(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
let who = ensure_signed(origin)?;
let member_id = Self::verify_ownership_and_membership(&who)?;
if Self::is_blacklisted(member_id) {
return Err(Error::<T>::MemberBlacklisted.into());
}
if !Members::<T>::contains_key(member_id) {
return Err(Error::<T>::MemberNotFound.into());
}
let validator_id = T::ValidatorIdOf::convert(who)
.ok_or(pallet_session::Error::<T>::NoAssociatedValidatorId)?;
if !pallet_session::Pallet::<T>::is_registered(&validator_id) {
return Err(Error::<T>::SessionKeysNotProvided.into());
}
if Self::is_incoming(member_id) {
return Err(Error::<T>::AlreadyIncoming.into());
}
let is_outgoing = Self::is_outgoing(member_id);
if Self::is_online(member_id) && !is_outgoing {
return Err(Error::<T>::AlreadyOnline.into());
}
if Self::authorities_counter() >= T::MaxAuthorities::get() {
return Err(Error::<T>::TooManyAuthorities.into());
}
if is_outgoing {
Self::remove_out(member_id);
} else {
Self::insert_in(member_id);
}
Ok(().into())
}
#[pallet::call_index(2)]
#[pallet::weight(<T as pallet::Config>::WeightInfo::set_session_keys())]
pub fn set_session_keys(origin: OriginFor<T>, keys: T::Keys) -> DispatchResultWithPostInfo {
let who = ensure_signed(origin.clone())?;
let member_id = Self::verify_ownership_and_membership(&who)?;
let _post_info = pallet_session::Call::<T>::set_keys {
keys,
proof: vec![],
}
.dispatch_bypass_filter(origin)?;
Members::<T>::insert(member_id, MemberData { owner_key: who });
Ok(().into())
}
#[pallet::call_index(3)]
#[pallet::weight(<T as pallet::Config>::WeightInfo::remove_member())]
pub fn remove_member(
origin: OriginFor<T>,
member_id: T::MemberId,
) -> DispatchResultWithPostInfo {
T::RemoveMemberOrigin::ensure_origin(origin)?;
let member_data = Members::<T>::get(member_id).ok_or(Error::<T>::MemberNotFound)?;
Self::do_remove_member(member_id, member_data.owner_key);
Ok(().into())
}
#[pallet::call_index(4)]
#[pallet::weight(<T as pallet::Config>::WeightInfo::remove_member_from_blacklist())]
pub fn remove_member_from_blacklist(
origin: OriginFor<T>,
member_id: T::MemberId,
) -> DispatchResultWithPostInfo {
T::RemoveMemberOrigin::ensure_origin(origin)?;
Blacklist::<T>::mutate(|members_ids| {
if let Ok(index) = members_ids.binary_search(&member_id) {
members_ids.remove(index);
Self::deposit_event(Event::MemberRemovedFromBlacklist { member: member_id });
Ok(().into())
} else {
Err(Error::<T>::MemberNotBlacklisted.into())
}
})
}
}
impl<T: Config> Pallet<T> {
#[frame_support::transactional]
pub fn change_owner_key(
member_id: T::MemberId,
new_owner_key: T::AccountId,
) -> DispatchResultWithPostInfo {
let old_owner_key = Members::<T>::mutate_exists(member_id, |maybe_member_data| {
if let Some(ref mut member_data) = maybe_member_data {
Ok(core::mem::replace(
&mut member_data.owner_key,
new_owner_key.clone(),
))
} else {
Err(Error::<T>::MemberNotFound)
}
})?;
let validator_id = T::ValidatorIdOf::convert(old_owner_key.clone())
.ok_or(pallet_session::Error::<T>::NoAssociatedValidatorId)?;
let session_keys = pallet_session::NextKeys::<T>::get(validator_id)
.ok_or(Error::<T>::SessionKeysNotProvided)?;
let _post_info = pallet_session::Call::<T>::purge_keys {}
.dispatch_bypass_filter(frame_system::RawOrigin::Signed(old_owner_key).into())?;
let _post_info = pallet_session::Call::<T>::set_keys {
keys: session_keys,
proof: vec![],
}
.dispatch_bypass_filter(frame_system::RawOrigin::Signed(new_owner_key).into())?;
Ok(().into())
}
pub fn authorities_counter() -> u32 {
let count = OnlineAuthorities::<T>::get().len() + IncomingAuthorities::<T>::get().len()
- OutgoingAuthorities::<T>::get().len();
count as u32
}
}
impl<T: Config> Pallet<T> {
fn do_remove_member(member_id: T::MemberId, owner_key: T::AccountId) {
if Self::is_online(member_id) {
Self::insert_out(member_id);
}
Self::remove_in(member_id);
Self::remove_online(member_id);
Members::<T>::remove(member_id);
if let Err(e) = pallet_session::Pallet::<T>::purge_keys(
frame_system::Origin::<T>::Signed(owner_key).into(),
) {
log::error!(
target: "runtime::authority_members",
"Logic error: fail to purge session keys in do_remove_member(): {:?}",
e
);
}
Self::deposit_event(Event::MemberRemoved { member: member_id });
}
fn insert_in(member_id: T::MemberId) -> bool {
let not_already_inserted = IncomingAuthorities::<T>::mutate(|members_ids| {
if let Err(index) = members_ids.binary_search(&member_id) {
members_ids.insert(index, member_id);
true
} else {
false
}
});
if not_already_inserted {
Self::deposit_event(Event::MemberGoOnline { member: member_id });
}
not_already_inserted
}
pub fn insert_out(member_id: T::MemberId) -> bool {
let not_already_inserted = OutgoingAuthorities::<T>::mutate(|members_ids| {
if let Err(index) = members_ids.binary_search(&member_id) {
members_ids.insert(index, member_id);
true
} else {
false
}
});
if not_already_inserted {
Self::deposit_event(Event::MemberGoOffline { member: member_id });
}
not_already_inserted
}
fn is_incoming(member_id: T::MemberId) -> bool {
IncomingAuthorities::<T>::get()
.binary_search(&member_id)
.is_ok()
}
fn is_online(member_id: T::MemberId) -> bool {
OnlineAuthorities::<T>::get()
.binary_search(&member_id)
.is_ok()
}
fn is_outgoing(member_id: T::MemberId) -> bool {
OutgoingAuthorities::<T>::get()
.binary_search(&member_id)
.is_ok()
}
fn is_blacklisted(member_id: T::MemberId) -> bool {
Blacklist::<T>::get().contains(&member_id)
}
fn remove_in(member_id: T::MemberId) {
IncomingAuthorities::<T>::mutate(|members_ids| {
if let Ok(index) = members_ids.binary_search(&member_id) {
members_ids.remove(index);
}
})
}
fn remove_online(member_id: T::MemberId) {
OnlineAuthorities::<T>::mutate(|members_ids| {
if let Ok(index) = members_ids.binary_search(&member_id) {
members_ids.remove(index);
}
});
}
fn remove_out(member_id: T::MemberId) {
OutgoingAuthorities::<T>::mutate(|members_ids| {
if let Ok(index) = members_ids.binary_search(&member_id) {
members_ids.remove(index);
}
});
}
fn verify_ownership_and_membership(
who: &T::AccountId,
) -> Result<T::MemberId, DispatchError> {
let member_id =
T::MemberIdOf::convert(who.clone()).ok_or(Error::<T>::MemberIdNotFound)?;
if !T::IsMember::is_member(&member_id) {
return Err(Error::<T>::NotMember.into());
}
Ok(member_id)
}
}
}
impl<T: Config> pallet_session::SessionManager<T::ValidatorId> for Pallet<T> {
fn new_session(_session_index: SessionIndex) -> Option<Vec<T::ValidatorId>> {
let members_ids_to_add = IncomingAuthorities::<T>::take();
let members_ids_to_del = OutgoingAuthorities::<T>::take();
if members_ids_to_add.is_empty() && members_ids_to_del.is_empty() {
return None;
}
for member_id in members_ids_to_add.iter() {
T::OnIncomingMember::on_incoming_member(*member_id);
Self::deposit_event(Event::IncomingAuthorities {
members: members_ids_to_add.clone(),
});
}
for member_id in members_ids_to_del.iter() {
T::OnOutgoingMember::on_outgoing_member(*member_id);
Self::deposit_event(Event::OutgoingAuthorities {
members: members_ids_to_del.clone(),
});
}
Some(
OnlineAuthorities::<T>::mutate(|members_ids| {
for member_id in members_ids_to_del {
if let Ok(index) = members_ids.binary_search(&member_id) {
members_ids.remove(index);
}
}
for member_id in members_ids_to_add {
if let Err(index) = members_ids.binary_search(&member_id) {
members_ids.insert(index, member_id);
}
}
members_ids.clone()
})
.into_iter()
.filter_map(|member_id| {
if let Some(member_data) = Members::<T>::get(member_id) {
T::ValidatorIdOf::convert(member_data.owner_key)
} else {
None
}
})
.collect(),
)
}
fn new_session_genesis(_new_index: SessionIndex) -> Option<Vec<T::ValidatorId>> {
Some(
OnlineAuthorities::<T>::get()
.into_iter()
.filter_map(|member_id| {
if let Some(member_data) = Members::<T>::get(member_id) {
T::ValidatorIdOf::convert(member_data.owner_key)
} else {
None
}
})
.collect(),
)
}
fn end_session(_end_index: SessionIndex) {}
fn start_session(start_index: SessionIndex) {
T::OnNewSession::on_new_session(start_index);
}
}
fn add_full_identification<T: Config>(
validator_id: T::ValidatorId,
) -> Option<(T::ValidatorId, T::FullIdentification)> {
use sp_runtime::traits::Convert as _;
T::FullIdentificationOf::convert(validator_id.clone())
.map(|full_ident| (validator_id, full_ident))
}
impl<T: Config> pallet_session::historical::SessionManager<T::ValidatorId, T::FullIdentification>
for Pallet<T>
{
fn new_session(
new_index: SessionIndex,
) -> Option<sp_std::vec::Vec<(T::ValidatorId, T::FullIdentification)>> {
<Self as pallet_session::SessionManager<_>>::new_session(new_index).map(|validators_ids| {
validators_ids
.into_iter()
.filter_map(add_full_identification::<T>)
.collect()
})
}
fn new_session_genesis(
new_index: SessionIndex,
) -> Option<sp_std::vec::Vec<(T::ValidatorId, T::FullIdentification)>> {
<Self as pallet_session::SessionManager<_>>::new_session_genesis(new_index).map(
|validators_ids| {
validators_ids
.into_iter()
.filter_map(add_full_identification::<T>)
.collect()
},
)
}
fn start_session(start_index: SessionIndex) {
<Self as pallet_session::SessionManager<_>>::start_session(start_index)
}
fn end_session(end_index: SessionIndex) {
<Self as pallet_session::SessionManager<_>>::end_session(end_index)
}
}