diff --git a/pallets/authority-members/src/lib.rs b/pallets/authority-members/src/lib.rs index c60df43b7dde52cc9da701caf577ff90aef1403e..9284bb25c88a8d02111a972a59f910fc7ab7f14a 100644 --- a/pallets/authority-members/src/lib.rs +++ b/pallets/authority-members/src/lib.rs @@ -69,6 +69,8 @@ pub mod pallet { type IsMember: IsMember<Self::MemberId>; type OnNewSession: OnNewSession; type OnRemovedMember: OnRemovedMember<Self::MemberId>; + type OnOutgoingMember: OnOutgoingMember<Self::MemberId>; + type OnIncomingMember: OnIncomingMember<Self::MemberId>; /// Max number of authorities allowed #[pallet::constant] type MaxAuthorities: Get<u32>; @@ -542,6 +544,13 @@ impl<T: Config> pallet_session::SessionManager<T::ValidatorId> for Pallet<T> { }); } + for member_id in members_ids_to_add.iter() { + T::OnIncomingMember::on_incoming_member(member_id.clone()); + } + for member_id in members_ids_to_del.iter() { + T::OnOutgoingMember::on_outgoing_member(member_id.clone()); + } + // updates the list of OnlineAuthorities and returns the list of their key Some( OnlineAuthorities::<T>::mutate(|members_ids| { diff --git a/pallets/authority-members/src/traits.rs b/pallets/authority-members/src/traits.rs index eb3452b58af951f88da5ad940aaef19e5263e577..639ef95df0b0bfc94434137df489064b4dd5a7d7 100644 --- a/pallets/authority-members/src/traits.rs +++ b/pallets/authority-members/src/traits.rs @@ -41,6 +41,16 @@ impl<MemberId> OnBlacklistedMember<MemberId> for () { fn on_blacklisted_member(_: MemberId) {} } +/// Handle the consequences of going in the authority set for other pallets. +/// Typically, a smith won't expire as long as he is in the authority set. +pub trait OnIncomingMember<MemberId> { + fn on_incoming_member(member_id: MemberId); +} +/// By default: no consequences +impl<MemberId> OnIncomingMember<MemberId> for () { + fn on_incoming_member(_: MemberId) {} +} + /// Handle the consequences of going out of authority set for other pallets. /// Typically, the smiths are not allowed to stay offline for a too long time. pub trait OnOutgoingMember<MemberId> { diff --git a/pallets/smith-members/src/impls.rs b/pallets/smith-members/src/impls.rs index d1a576043d9c2e30e1786a1576dbdf5b5aba99b4..be6180f05c9523ce5c916dc52c4dcb0bd07ebc26 100644 --- a/pallets/smith-members/src/impls.rs +++ b/pallets/smith-members/src/impls.rs @@ -1,4 +1,7 @@ -use crate::{Config, Pallet}; +use crate::{Config, CurrentSession, Pallet}; +use frame_support::StorageValue; +use pallet_authority_members::SessionIndex; +use sp_runtime::traits::Convert; /// We want to remove a Smith when he is removed from the higher level set of "authorities". impl<T: Config> pallet_authority_members::OnRemovedMember<T> for Pallet<T> { @@ -19,8 +22,27 @@ impl<T: Config> pallet_authority_members::OnBlacklistedMember<T> for Pallet<T> { } /// -impl<T: Config> pallet_authority_members::OnOutgoingMember<T> for Pallet<T> { - fn on_outgoing_member(member_id: T) { - todo!("Has gone out of authority set, start offline counter") +impl<T: Config> pallet_authority_members::OnOutgoingMember<T::MemberId> for Pallet<T> { + fn on_outgoing_member(member_id: T::MemberId) { + Pallet::<T>::smith_goes_offline( + T::IdtyIdOfAuthorityId::convert(member_id).expect("convertion should be ok"), + ); + } +} + +/// As long as a Smith is in the authority set, he will not expire. +impl<T: Config> pallet_authority_members::OnIncomingMember<T::MemberId> for Pallet<T> { + fn on_incoming_member(member_id: T::MemberId) { + Pallet::<T>::smith_goes_online( + T::IdtyIdOfAuthorityId::convert(member_id).expect("convertion should be ok"), + ); + } +} + +/// +impl<T: Config> pallet_authority_members::OnNewSession for Pallet<T> { + fn on_new_session(index: SessionIndex) { + CurrentSession::<T>::put(index); + Pallet::<T>::remove_expired_smiths(index); } } diff --git a/pallets/smith-members/src/lib.rs b/pallets/smith-members/src/lib.rs index c34deaccbe6a1e77ebea21230d97407adf1f6094..040e4fb039eefa4af66fcd6790a98fa1aae69e58 100644 --- a/pallets/smith-members/src/lib.rs +++ b/pallets/smith-members/src/lib.rs @@ -19,9 +19,10 @@ #[cfg(test)] mod mock; -mod impls; #[cfg(test)] mod tests; + +mod impls; mod traits; mod types; @@ -33,7 +34,9 @@ use sp_runtime::traits::AtLeast32BitUnsigned; use sp_std::fmt::Debug; use sp_std::prelude::*; +use crate::traits::OnSmithDelete; pub use pallet::*; +use pallet_authority_members::SessionIndex; pub use types::*; #[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo)] @@ -43,7 +46,7 @@ pub enum SmithRemovalReason { Blacklisted, } -#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo)] +#[derive(Encode, Decode, Copy, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo)] pub enum SmithStatus { /// The identity has been invited by a smith but has not accepted yet Invited, @@ -63,6 +66,7 @@ pub mod pallet { use frame_support::{pallet_prelude::*, traits::ReservableCurrency}; use frame_system::pallet_prelude::*; use frame_system::pallet_prelude::*; + use pallet_authority_members::SessionIndex; use sp_runtime::traits::{Convert, IsMember}; use std::collections::BTreeMap; @@ -76,7 +80,10 @@ pub mod pallet { /// The pallet's config trait. #[pallet::config] pub trait Config: frame_system::Config { + /// To only allow WoT members to be invited type IsWoTMember: IsMember<Self::IdtyIndex>; + /// Notify when a smith is removed (for authority-members to react) + type OnSmithDelete: traits::OnSmithDelete<Self::IdtyIndex>; /// The overarching event type. type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>; /// A short identity index. @@ -89,9 +96,10 @@ pub mod pallet { + MaybeSerializeDeserialize + Debug + MaxEncodedLen; - /// Duration of validity of a certification - type ValidityPeriod: Get<Self::BlockNumber>; + /// Identifier for an authority-member + type MemberId: Copy + Ord + MaybeSerializeDeserialize + Parameter; /// Maximum number of active certifications by issuer + #[pallet::constant] type MaxByIssuer: Get<u32>; /// Minimum number of certifications to be able to create a smith #[pallet::constant] @@ -99,8 +107,13 @@ pub mod pallet { /// Minimum number of certifications to become a Smith #[pallet::constant] type MinCertForMembership: Get<u32>; + /// Maximum duration of inactivity before a smith is removed + #[pallet::constant] + type InactivityMaxDuration: Get<u32>; /// Something that gives the IdtyId of an AccountId type IdtyIdOf: Convert<Self::AccountId, Option<Self::IdtyIndex>>; + /// Something that gives the IdtyId of an AccountId + type IdtyIdOfAuthorityId: Convert<Self::MemberId, Option<Self::IdtyIndex>>; } /// Events type. @@ -132,6 +145,7 @@ pub mod pallet { #[pallet::genesis_build] impl<T: Config> GenesisBuild<T> for GenesisConfig<T> { fn build(&self) { + CurrentSession::<T>::put(0); let mut cert_meta_by_issuer = BTreeMap::<T::IdtyIndex, SmithCertMeta<T::BlockNumber>>::new(); for (receiver, issuers) in &self.certs_by_receiver { @@ -165,7 +179,19 @@ pub mod pallet { } else { SmithStatus::Pending }; - Smiths::<T>::insert(receiver, smith_status); + Smiths::<T>::insert( + receiver, + SmithMeta { + status: smith_status, + expires_on: Some( + CurrentSession::<T>::get() + T::InactivityMaxDuration::get(), + ), + }, + ); + ExpiresOn::<T>::append( + CurrentSession::<T>::get() + T::InactivityMaxDuration::get(), + receiver, + ); } } } @@ -177,8 +203,16 @@ pub mod pallet { /// maps identity index to smith status #[pallet::storage] - pub type Smiths<T: Config> = - StorageMap<_, Twox64Concat, T::IdtyIndex, SmithStatus, OptionQuery>; + pub type Smiths<T: Config> = StorageMap<_, Twox64Concat, T::IdtyIndex, SmithMeta, OptionQuery>; + + /// maps session index to possible smith removals + #[pallet::storage] + pub type ExpiresOn<T: Config> = + StorageMap<_, Twox64Concat, SessionIndex, Vec<T::IdtyIndex>, OptionQuery>; + + /// maps session index to possible smith removals + #[pallet::storage] + pub type CurrentSession<T: Config> = StorageValue<_, SessionIndex, ValueQuery>; // ERRORS // @@ -218,8 +252,9 @@ pub mod pallet { ) -> DispatchResultWithPostInfo { let who = ensure_signed(origin.clone())?; let issuer = T::IdtyIdOf::convert(who.clone()).ok_or(Error::<T>::UnknownIssuer)?; - let issuer_status = - Smiths::<T>::get(issuer).ok_or(Error::<T>::IssuerHasNoSmithStatus)?; + let issuer_status = Smiths::<T>::get(issuer) + .ok_or(Error::<T>::IssuerHasNoSmithStatus)? + .status; ensure!( issuer_status == SmithStatus::Smith, Error::<T>::CannotInvite @@ -229,7 +264,15 @@ pub mod pallet { Error::<T>::ReceveirAlreadyHasSmithStatus ); - Smiths::<T>::insert(receiver, SmithStatus::Invited); + let new_expires_on = CurrentSession::<T>::get() + T::InactivityMaxDuration::get(); + Smiths::<T>::insert( + receiver, + SmithMeta { + status: SmithStatus::Invited, + expires_on: Some(new_expires_on), + }, + ); + ExpiresOn::<T>::append(new_expires_on, receiver); Ok(().into()) } @@ -239,13 +282,18 @@ pub mod pallet { pub fn accept_invitation(origin: OriginFor<T>) -> DispatchResultWithPostInfo { let who = ensure_signed(origin.clone())?; let pretender = T::IdtyIdOf::convert(who.clone()).ok_or(Error::<T>::UnknownIssuer)?; - let pretender_status = Smiths::<T>::get(pretender).ok_or(Error::<T>::NotInvited)?; + let pretender_status = Smiths::<T>::get(pretender) + .ok_or(Error::<T>::NotInvited)? + .status; ensure!( pretender_status == SmithStatus::Invited, Error::<T>::AlreadyAcceptedInvitation ); - Smiths::<T>::insert(pretender, SmithStatus::Pending); + Smiths::<T>::mutate(pretender, |maybe_smith_meta| { + let maybe_smith_meta = maybe_smith_meta.as_mut().expect("status checked earlier"); + maybe_smith_meta.status = SmithStatus::Pending; + }); Ok(().into()) } @@ -259,14 +307,16 @@ pub mod pallet { let who = ensure_signed(origin)?; let issuer = T::IdtyIdOf::convert(who.clone()).ok_or(Error::<T>::UnknownIssuer)?; ensure!(issuer != receiver, Error::<T>::CannotCertifySelf); - let issuer_status = - Smiths::<T>::get(issuer).ok_or(Error::<T>::IssuerHasNoSmithStatus)?; + let issuer_status = Smiths::<T>::get(issuer) + .ok_or(Error::<T>::IssuerHasNoSmithStatus)? + .status; ensure!( issuer_status == SmithStatus::Smith, Error::<T>::IssuerIsNotASmith ); - let receiver_status = - Smiths::<T>::get(receiver).ok_or(Error::<T>::ReceiverHasNotBeenInvited)?; + let receiver_status = Smiths::<T>::get(receiver) + .ok_or(Error::<T>::ReceiverHasNotBeenInvited)? + .status; ensure!( receiver_status != SmithStatus::Invited, Error::<T>::ReceveirMustAcceptInvitation @@ -275,23 +325,73 @@ pub mod pallet { CertsByReceiver::<T>::mutate(receiver, |certs| { let certs = certs.get_or_insert(vec![]); certs.push(issuer); - // TODO: "as u32" allowed? - if certs.len() as u32 >= T::MinCertForMembership::get() { - Smiths::<T>::insert(receiver, SmithStatus::Smith); - } else { - Smiths::<T>::insert(receiver, SmithStatus::Pending); - } + Smiths::<T>::mutate(receiver, |maybe_smith_meta| { + let maybe_smith_meta = + maybe_smith_meta.as_mut().expect("status checked earlier"); + // TODO: "as u32" allowed? + maybe_smith_meta.status = + if certs.len() as u32 >= T::MinCertForMembership::get() { + SmithStatus::Smith + } else { + SmithStatus::Pending + }; + }); }); Ok(().into()) } } +} + +impl<T: Config> Pallet<T> { + // TODO: return what? + fn remove_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 { + Smiths::<T>::remove(smith); + T::OnSmithDelete::on_smith_delete( + smith, + SmithRemovalReason::OfflineTooLong, + ); + } + } + } + } + } + } - // TODO: on confirm - // ensure!( - // T::IsWoTMember::is_member(&receiver), - // Error::<T>::NotAMember - // ); + // TODO: return what? + fn 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| { + let maybe_smith_meta = maybe_smith_meta.as_mut().expect("checked earlier"); + // As long as the smith is online, it cannot expire + maybe_smith_meta.expires_on = None; + // FIXME: also update ExpiresOn storage + }); + } + } + } + + // TODO: return what? + fn 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| { + let maybe_smith_meta = maybe_smith_meta.as_mut().expect("checked earlier"); + // As long as the smith is online, it cannot expire + let new_expires_on = + CurrentSession::<T>::get() + T::InactivityMaxDuration::get(); + maybe_smith_meta.expires_on = Some(new_expires_on); + ExpiresOn::<T>::append(new_expires_on, idty_index); + }); + } + } + } } impl<T: Config> Pallet<T> { diff --git a/pallets/smith-members/src/mock.rs b/pallets/smith-members/src/mock.rs index 36fca3cd989c0db444b88171c7a7b905e88db5b8..b32cf9b0ffc4999c9d63d9cad0eb8ff3120afc41 100644 --- a/pallets/smith-members/src/mock.rs +++ b/pallets/smith-members/src/mock.rs @@ -16,8 +16,9 @@ #![cfg(test)] +use crate::traits::OnSmithDelete; use crate::{self as pallet_smith_members}; -use frame_support::pallet_prelude::GenesisBuild; +use frame_support::pallet_prelude::{GenesisBuild, Hooks}; use frame_support::{ parameter_types, traits::{ConstU32, ConstU64}, @@ -92,10 +93,13 @@ impl pallet_smith_members::Config for Runtime { type IdtyIndex = u64; type IsWoTMember = EveryoneExceptIdZero; type IdtyIdOf = ConvertInto; - type ValidityPeriod = ConstU64<1>; type MinCertForMembership = ConstU32<2>; type MinCertForCreateIdtyRight = ConstU32<3>; type MaxByIssuer = ConstU32<5>; + type InactivityMaxDuration = ConstU32<5>; + type OnSmithDelete = (); + type IdtyIdOfAuthorityId = ConvertInto; + type MemberId = u64; } pub fn new_test_ext( diff --git a/pallets/smith-members/src/tests.rs b/pallets/smith-members/src/tests.rs index c2868d5148edfbfb270f682c4c3ef0d7acfd1a93..a88d5e7062f3777c36a91bd47df4cb66c0bede52 100644 --- a/pallets/smith-members/src/tests.rs +++ b/pallets/smith-members/src/tests.rs @@ -22,9 +22,10 @@ use frame_support::{assert_err, assert_ok}; #[cfg(test)] use maplit::btreemap; +use pallet_authority_members::OnNewSession; #[test] -fn process_to_become_a_smith() { +fn process_to_become_a_smith_and_lose_it() { new_test_ext(GenesisConfig { certs_by_receiver: btreemap![ 1 => btreemap![ @@ -51,21 +52,41 @@ fn process_to_become_a_smith() { ))); // State after assert_eq!(CertsByReceiver::<Runtime>::get(5), None); - assert_eq!(Smiths::<Runtime>::get(5).unwrap(), SmithStatus::Pending); + assert_eq!( + Smiths::<Runtime>::get(5).unwrap().status, + SmithStatus::Pending + ); // Then certification 1/2 assert_ok!(Pallet::<Runtime>::certify_smith( RuntimeOrigin::signed(1), 5 )); assert_eq!(CertsByReceiver::<Runtime>::get(5).unwrap().len(), 1); - assert_eq!(Smiths::<Runtime>::get(5).unwrap(), SmithStatus::Pending); + assert_eq!( + Smiths::<Runtime>::get(5).unwrap().status, + SmithStatus::Pending + ); // Then certification 2/2 assert_ok!(Pallet::<Runtime>::certify_smith( RuntimeOrigin::signed(2), 5 )); assert_eq!(CertsByReceiver::<Runtime>::get(5).unwrap().len(), 2); - assert_eq!(Smiths::<Runtime>::get(5).unwrap(), SmithStatus::Smith); + assert_eq!( + Smiths::<Runtime>::get(5).unwrap().status, + SmithStatus::Smith + ); + + // On session 4 everything if fine + Pallet::<Runtime>::on_new_session(4); + assert_eq!(Smiths::<Runtime>::get(1).is_some(), true); + assert_eq!(Smiths::<Runtime>::get(2).is_some(), true); + assert_eq!(Smiths::<Runtime>::get(5).is_some(), true); + // On session 5 no more smiths because of lack of activity + Pallet::<Runtime>::on_new_session(5); + assert_eq!(Smiths::<Runtime>::get(1), None); + assert_eq!(Smiths::<Runtime>::get(2), None); + assert_eq!(Smiths::<Runtime>::get(5), None); }); } @@ -94,10 +115,22 @@ fn should_have_checks_on_certify() { .execute_with(|| { // Initially assert_eq!(CertsByReceiver::<Runtime>::iter().count(), 4); - assert_eq!(Smiths::<Runtime>::get(1).unwrap(), SmithStatus::Smith); - assert_eq!(Smiths::<Runtime>::get(2).unwrap(), SmithStatus::Smith); - assert_eq!(Smiths::<Runtime>::get(3).unwrap(), SmithStatus::Pending); - assert_eq!(Smiths::<Runtime>::get(4).unwrap(), SmithStatus::Smith); + assert_eq!( + Smiths::<Runtime>::get(1).unwrap().status, + SmithStatus::Smith + ); + assert_eq!( + Smiths::<Runtime>::get(2).unwrap().status, + SmithStatus::Smith + ); + assert_eq!( + Smiths::<Runtime>::get(3).unwrap().status, + SmithStatus::Pending + ); + assert_eq!( + Smiths::<Runtime>::get(4).unwrap().status, + SmithStatus::Smith + ); assert_eq!(CertsByReceiver::<Runtime>::get(1).unwrap().len(), 3); assert_eq!(CertsByReceiver::<Runtime>::get(2).unwrap().len(), 2); assert_eq!(CertsByReceiver::<Runtime>::get(3).unwrap().len(), 1); @@ -123,7 +156,10 @@ fn should_have_checks_on_certify() { // #3: state before assert_eq!(CertsByReceiver::<Runtime>::get(3).unwrap().len(), 1); - assert_eq!(Smiths::<Runtime>::get(3).unwrap(), SmithStatus::Pending); + assert_eq!( + Smiths::<Runtime>::get(3).unwrap().status, + SmithStatus::Pending + ); // Try to certify #3 assert_ok!(Pallet::<Runtime>::certify_smith( RuntimeOrigin::signed(1), @@ -131,6 +167,73 @@ fn should_have_checks_on_certify() { )); // #3: state after assert_eq!(CertsByReceiver::<Runtime>::get(3).unwrap().len(), 2); - assert_eq!(Smiths::<Runtime>::get(3).unwrap(), SmithStatus::Smith); + assert_eq!( + Smiths::<Runtime>::get(3).unwrap().status, + SmithStatus::Smith + ); + }); +} + +#[test] +fn smith_activity_postpones_expiration() { + new_test_ext(GenesisConfig { + certs_by_receiver: btreemap![ + 1 => btreemap![ + 2 => None, + 3 => None, + 4 => None, + ], + 2 => btreemap![ + 4 => None, + 4 => None, + 5 => None, + ] + ], + }) + .execute_with(|| { + // On session 4 everything is fine + Pallet::<Runtime>::on_new_session(4); + assert_eq!(Smiths::<Runtime>::get(1).is_some(), true); + assert_eq!(Smiths::<Runtime>::get(2).is_some(), true); + + // Smith #2 is online but not #1 + Pallet::<Runtime>::smith_goes_online(2); + + // On session 5 no more smiths because of lack of activity + Pallet::<Runtime>::on_new_session(5); + assert_eq!(Smiths::<Runtime>::get(1), None); + assert_eq!( + Smiths::<Runtime>::get(2), + Some(SmithMeta { + status: SmithStatus::Smith, + expires_on: None + }) + ); + + // Smith #2 goes offline + Pallet::<Runtime>::on_new_session(6); + Pallet::<Runtime>::smith_goes_offline(2); + assert_eq!(Smiths::<Runtime>::get(1), None); + assert_eq!( + Smiths::<Runtime>::get(2), + Some(SmithMeta { + status: SmithStatus::Smith, + expires_on: Some(11) + }) + ); + // Still not expired on session 10 + Pallet::<Runtime>::on_new_session(10); + assert_eq!(Smiths::<Runtime>::get(1), None); + assert_eq!( + Smiths::<Runtime>::get(2), + Some(SmithMeta { + status: SmithStatus::Smith, + expires_on: Some(11) + }) + ); + // Finally expired on session 11 + Pallet::<Runtime>::on_new_session(11); + assert_eq!(Smiths::<Runtime>::get(1), None); + assert_eq!(Smiths::<Runtime>::get(2), None); }); } diff --git a/pallets/smith-members/src/traits.rs b/pallets/smith-members/src/traits.rs index 7b15e78b98dc77eaca5d38f1cf88aea2a6393420..f59b6fc1c775e15ba69e32704da9238516a18db4 100644 --- a/pallets/smith-members/src/traits.rs +++ b/pallets/smith-members/src/traits.rs @@ -1,8 +1,8 @@ use crate::SmithRemovalReason; -pub trait OnSmithDelete { - fn on_smith_delete(reason: SmithRemovalReason); +pub trait OnSmithDelete<IdtyIndex> { + fn on_smith_delete(idty_index: IdtyIndex, reason: SmithRemovalReason); } -impl OnSmithDelete for () { - fn on_smith_delete(_: SmithRemovalReason) {} +impl<IdtyIndex> OnSmithDelete<IdtyIndex> for () { + fn on_smith_delete(_: IdtyIndex, _: SmithRemovalReason) {} } diff --git a/pallets/smith-members/src/types.rs b/pallets/smith-members/src/types.rs index d47d5704ed2128ebdbbf5375697adcdc023ae936..f1298055190fa798a3e670ff83a476fc249d30ee 100644 --- a/pallets/smith-members/src/types.rs +++ b/pallets/smith-members/src/types.rs @@ -16,11 +16,21 @@ //! Various basic types for use in the identity pallet. +use crate::SmithStatus; use codec::{Decode, Encode}; use frame_support::pallet_prelude::*; -use scale_info::TypeInfo; #[cfg(feature = "std")] -use serde::{Deserialize, Serialize}; +use pallet_authority_members::SessionIndex; +use scale_info::TypeInfo; + +/// certification metadata attached to an identity +#[derive(Encode, Decode, Clone, Copy, PartialEq, Eq, RuntimeDebug, TypeInfo)] +pub struct SmithMeta { + /// current status of the smith + pub status: SmithStatus, + /// the session at which the smith will expire (for lack of validation activity) + pub expires_on: Option<SessionIndex>, +} /// certification metadata attached to an identity #[derive(Encode, Decode, Clone, Copy, PartialEq, Eq, RuntimeDebug, TypeInfo)]