#![cfg_attr(not(feature = "std"), no_std)]
#![allow(clippy::manual_inspect)]
#![doc = simple_mermaid::mermaid!("../docs/member_calls.mmd")]
#![doc = simple_mermaid::mermaid!("../docs/member_hooks.mmd")]
pub use pallet::*;
#[cfg(feature = "runtime-benchmarks")]
mod benchmarking;
#[cfg(test)]
mod mock;
#[cfg(test)]
mod tests;
#[polkadot_sdk::frame_support::pallet]
pub mod pallet {
use polkadot_sdk::{frame_support, frame_system, sp_runtime, sp_std};
use frame_support::pallet_prelude::*;
use frame_system::pallet_prelude::*;
use sp_runtime::traits::Zero;
use sp_std::collections::btree_map::BTreeMap;
use sp_std::vec;
use sp_std::vec::Vec;
use polkadot_sdk::pallet_balances;
use time_primitives::{
AccountId, Balance, ElectionsInterface, MembersInterface, NetworkId, PeerId,
ShardsInterface,
};
pub trait WeightInfo {
fn register_member() -> Weight;
fn send_heartbeat() -> Weight;
fn unregister_member() -> Weight;
fn timeout_heartbeats(n: u32) -> Weight;
fn is_member() -> Weight;
}
impl WeightInfo for () {
fn register_member() -> Weight {
Weight::default()
}
fn send_heartbeat() -> Weight {
Weight::default()
}
fn unregister_member() -> Weight {
Weight::default()
}
fn timeout_heartbeats(_: u32) -> Weight {
Weight::default()
}
fn is_member() -> Weight {
Weight::default()
}
}
#[pallet::pallet]
#[pallet::without_storage_info]
pub struct Pallet<T>(_);
pub type BalanceOf<T> = <T as pallet_balances::Config>::Balance;
#[pallet::config]
pub trait Config:
polkadot_sdk::frame_system::Config<AccountId = AccountId>
+ pallet_balances::Config<Balance = Balance>
{
type RuntimeEvent: From<Event<Self>>
+ IsType<<Self as polkadot_sdk::frame_system::Config>::RuntimeEvent>;
type WeightInfo: WeightInfo;
type Shards: ShardsInterface;
type Elections: ElectionsInterface;
type AdminOrigin: EnsureOrigin<Self::RuntimeOrigin>;
#[pallet::constant]
type HeartbeatTimeout: Get<BlockNumberFor<Self>>;
#[pallet::constant]
type MaxTimeoutsPerBlock: Get<u32>;
}
#[pallet::storage]
pub type MemberRegistered<T: Config> =
StorageMap<_, Blake2_128Concat, AccountId, (), OptionQuery>;
#[pallet::storage]
pub type MemberNetwork<T: Config> =
StorageMap<_, Blake2_128Concat, AccountId, NetworkId, OptionQuery>;
#[pallet::storage]
pub type MemberPeerId<T: Config> =
StorageMap<_, Blake2_128Concat, AccountId, PeerId, OptionQuery>;
#[pallet::storage]
pub type MemberOnline<T: Config> = StorageMap<_, Blake2_128Concat, AccountId, (), OptionQuery>;
#[pallet::storage]
pub type Heartbeat<T: Config> = StorageMap<_, Blake2_128Concat, AccountId, (), OptionQuery>;
#[pallet::storage]
pub type TimedOut<T: Config> = StorageValue<_, Vec<AccountId>, ValueQuery>;
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
RegisteredMember(AccountId, NetworkId, PeerId),
HeartbeatReceived(AccountId),
MemberOnline(AccountId),
MembersOffline(Vec<AccountId>),
UnRegisteredMember(AccountId, NetworkId),
}
#[pallet::error]
pub enum Error<T> {
NotMember,
NotRegistered,
AlreadySubmittedHeartbeat,
}
#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
fn on_initialize(n: BlockNumberFor<T>) -> Weight {
log::info!("on_initialize begin");
let weight = if (n % T::HeartbeatTimeout::get()).is_zero() {
Self::timeout_heartbeats()
} else {
Weight::default()
};
log::info!("on_initialize end");
weight
}
}
#[pallet::call]
impl<T: Config> Pallet<T> {
#[pallet::call_index(0)]
#[pallet::weight(<T as Config>::WeightInfo::register_member())]
pub fn register_member(
origin: OriginFor<T>,
network: NetworkId,
member: AccountId,
peer_id: PeerId,
) -> DispatchResult {
T::AdminOrigin::ensure_origin(origin)?;
MemberNetwork::<T>::insert(&member, network);
MemberPeerId::<T>::insert(&member, peer_id);
MemberRegistered::<T>::insert(&member, ());
Self::deposit_event(Event::RegisteredMember(member, network, peer_id));
Ok(())
}
#[pallet::call_index(1)]
#[pallet::weight(<T as Config>::WeightInfo::unregister_member())]
pub fn unregister_member(origin: OriginFor<T>, member: AccountId) -> DispatchResult {
T::AdminOrigin::ensure_origin(origin)?;
let network = MemberNetwork::<T>::get(&member).ok_or(Error::<T>::NotMember)?;
ensure!(MemberRegistered::<T>::take(&member).is_some(), Error::<T>::NotRegistered);
MemberNetwork::<T>::remove(&member);
MemberPeerId::<T>::remove(&member);
Heartbeat::<T>::remove(&member);
MemberOnline::<T>::remove(&member);
TimedOut::<T>::mutate(|members| members.retain(|m| *m != member));
Self::deposit_event(Event::UnRegisteredMember(member.clone(), network));
Self::members_offline(vec![member], network);
Ok(())
}
#[pallet::call_index(2)]
#[pallet::weight((
<T as Config>::WeightInfo::send_heartbeat(),
DispatchClass::Operational,
Pays::No
))]
pub fn send_heartbeat(origin: OriginFor<T>) -> DispatchResult {
let member = ensure_signed(origin)?;
ensure!(MemberRegistered::<T>::contains_key(&member), Error::<T>::NotRegistered);
let network = MemberNetwork::<T>::get(&member).ok_or(Error::<T>::NotMember)?;
ensure!(Heartbeat::<T>::get(&member).is_none(), Error::<T>::AlreadySubmittedHeartbeat);
if !Self::is_member_online(&member) {
Self::member_online(&member, network);
}
Heartbeat::<T>::insert(&member, ());
TimedOut::<T>::mutate(|members| members.retain(|m| *m != member));
Self::deposit_event(Event::HeartbeatReceived(member));
Ok(())
}
}
impl<T: Config> Pallet<T> {
pub(crate) fn timeout_heartbeats() -> Weight {
let timed_out_members = TimedOut::<T>::take();
let heartbeats = Heartbeat::<T>::drain();
let mut next_timed_out =
Vec::with_capacity(timed_out_members.len() + heartbeats.size_hint().0);
let mut num_timeouts = 0u32;
let mut current_timed_out = BTreeMap::<NetworkId, Vec<AccountId>>::new();
for member in timed_out_members.into_iter() {
if num_timeouts >= T::MaxTimeoutsPerBlock::get() {
next_timed_out.push(member);
continue;
}
if let Some(network) = MemberNetwork::<T>::get(&member) {
current_timed_out.entry(network).or_default().push(member);
num_timeouts += 1;
} else {
next_timed_out.push(member);
}
}
for (network, members) in current_timed_out {
Self::members_offline(members, network);
}
next_timed_out.extend(heartbeats.map(|(m, _)| m));
TimedOut::<T>::put(next_timed_out);
<T as Config>::WeightInfo::timeout_heartbeats(num_timeouts)
}
fn member_online(member: &AccountId, network: NetworkId) {
MemberOnline::<T>::insert(member.clone(), ());
Self::deposit_event(Event::MemberOnline(member.clone()));
T::Elections::member_online(member, network);
}
fn members_offline(members: Vec<AccountId>, network: NetworkId) {
for m in &members {
MemberOnline::<T>::remove(m);
}
Self::deposit_event(Event::MembersOffline(members.clone()));
T::Elections::members_offline(members, network);
}
pub fn heartbeat_timeout() -> BlockNumberFor<T> {
T::HeartbeatTimeout::get()
}
}
impl<T: Config> MembersInterface for Pallet<T> {
fn member_peer_id(account: &AccountId) -> Option<PeerId> {
MemberPeerId::<T>::get(account)
}
fn is_member_online(account: &AccountId) -> bool {
MemberOnline::<T>::contains_key(account)
}
fn is_member_registered(account: &AccountId) -> bool {
MemberRegistered::<T>::contains_key(account)
}
}
}