diff --git a/Cargo.lock b/Cargo.lock index 5cdd858ebbd3c1af72ffcbdc2e92bb5bbb82ef1a..3e2a94d0166146ae78fe9cec37ea4203a9db37be 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1923,6 +1923,7 @@ dependencies = [ "frame-system-rpc-runtime-api", "hex-literal", "pallet-authority-discovery", + "pallet-authority-members", "pallet-authorship", "pallet-babe", "pallet-balances", @@ -4165,6 +4166,26 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-authority-members" +version = "3.0.0" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "maplit", + "pallet-session", + "parity-scale-codec", + "scale-info", + "serde", + "sp-core", + "sp-io", + "sp-membership", + "sp-runtime", + "sp-staking", + "sp-std", +] + [[package]] name = "pallet-authorship" version = "4.0.0-dev" diff --git a/Cargo.toml b/Cargo.toml index daa258c22ddd50ac61b5ee8645b0b52b25ca425a..caeb104c209013002d0f791d0dde0fba2b0e5d20 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -104,6 +104,7 @@ members = [ 'pallets/duniter-wot', 'pallets/identity', 'pallets/membership', + 'pallets/authority-members', 'pallets/ud-accounts-storage', 'pallets/universal-dividend', 'primitives/membership', diff --git a/pallets/authority-members/Cargo.toml b/pallets/authority-members/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..c6deae9112a89c06c570b96fd7f20598430f7594 --- /dev/null +++ b/pallets/authority-members/Cargo.toml @@ -0,0 +1,103 @@ +[package] +authors = ['librelois <c@elo.tf>'] +description = 'FRAME pallet authority members.' +edition = '2018' +homepage = 'https://substrate.dev' +license = 'AGPL-3.0' +name = 'pallet-authority-members' +readme = 'README.md' +repository = 'https://git.duniter.org/nodes/rust/duniter-v2s' +version = '3.0.0' + +[features] +default = ['std'] +runtime-benchmarks = ['frame-benchmarking'] +std = [ + 'codec/std', + 'frame-support/std', + 'frame-system/std', + 'frame-benchmarking/std', + 'pallet-session/std', + 'serde', + 'sp-core/std', + 'sp-membership/std', + 'sp-runtime/std', + 'sp-staking/std', + 'sp-std/std', +] +try-runtime = ['frame-support/try-runtime'] + +[dependencies] +sp-membership = { path = "../../primitives/membership", default-features = false } + +# substrate +scale-info = { version = "1.0", default-features = false, features = ["derive"] } + +[dependencies.codec] +default-features = false +features = ['derive'] +package = 'parity-scale-codec' +version = '2.3.1' + +[dependencies.frame-benchmarking] +default-features = false +git = 'https://github.com/librelois/substrate.git' +optional = true +branch = 'duniter-monthly-2022-01' + +[dependencies.frame-support] +default-features = false +git = 'https://github.com/librelois/substrate.git' +branch = 'duniter-monthly-2022-01' + +[dependencies.frame-system] +default-features = false +git = 'https://github.com/librelois/substrate.git' +branch = 'duniter-monthly-2022-01' + +[dependencies.pallet-session] +default-features = false +git = 'https://github.com/librelois/substrate.git' +branch = 'duniter-monthly-2022-01' + +[dependencies.serde] +version = "1.0.101" +optional = true +features = ["derive"] + +[dependencies.sp-core] +default-features = false +git = 'https://github.com/librelois/substrate.git' +branch = 'duniter-monthly-2022-01' + +[dependencies.sp-runtime] +default-features = false +git = 'https://github.com/librelois/substrate.git' +branch = 'duniter-monthly-2022-01' + +[dependencies.sp-staking] +default-features = false +git = 'https://github.com/librelois/substrate.git' +branch = 'duniter-monthly-2022-01' + +[dependencies.sp-std] +default-features = false +git = 'https://github.com/librelois/substrate.git' +branch = 'duniter-monthly-2022-01' + +### DOC ### + +[package.metadata.docs.rs] +targets = ['x86_64-unknown-linux-gnu'] + +### DEV ### + +[dev-dependencies.maplit] +version = '1.0.2' + +[dev-dependencies.serde] +version = '1.0.119' + +[dev-dependencies.sp-io] +git = 'https://github.com/librelois/substrate.git' +branch = 'duniter-monthly-2022-01' diff --git a/pallets/authority-members/src/lib.rs b/pallets/authority-members/src/lib.rs new file mode 100644 index 0000000000000000000000000000000000000000..16ea999fa09880536b168c757f6f207bf2e8e3cc --- /dev/null +++ b/pallets/authority-members/src/lib.rs @@ -0,0 +1,511 @@ +// Copyright 2021 Axiom-Team +// +// This file is part of Substrate-Libre-Currency. +// +// Substrate-Libre-Currency is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, version 3 of the License. +// +// Substrate-Libre-Currency is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with Substrate-Libre-Currency. If not, see <https://www.gnu.org/licenses/>. + +#![cfg_attr(not(feature = "std"), no_std)] +#![allow(clippy::type_complexity)] + +pub mod traits; +mod types; + +#[cfg(test)] +mod mock; + +#[cfg(test)] +mod tests; + +/*#[cfg(feature = "runtime-benchmarks")] +mod benchmarking;*/ + +pub use pallet::*; +pub use types::*; + +use frame_support::traits::Get; +use sp_staking::SessionIndex; +use sp_std::prelude::*; + +#[frame_support::pallet] +pub mod pallet { + use super::*; + use crate::traits::OnRemovedMember; + use frame_support::pallet_prelude::*; + use frame_support::traits::{StorageVersion, UnfilteredDispatchable}; + use frame_system::pallet_prelude::*; + use sp_runtime::traits::{Convert, IsMember}; + use sp_std::collections::btree_map::BTreeMap; + + /// The current storage version. + const STORAGE_VERSION: StorageVersion = StorageVersion::new(1); + + #[pallet::pallet] + #[pallet::generate_store(pub(super) trait Store)] + #[pallet::storage_version(STORAGE_VERSION)] + pub struct Pallet<T>(_); + + // CONFIG // + + #[pallet::config] + pub trait Config: frame_system::Config + pallet_session::Config { + type Event: From<Event<Self>> + IsType<<Self as frame_system::Config>::Event>; + type IsMember: IsMember<Self::MemberId>; + type OnRemovedMember: OnRemovedMember<Self::MemberId>; + type OwnerKeyOf: Convert<Self::MemberId, Option<Self::AccountId>>; + type MemberId: Copy + MaybeSerializeDeserialize + Parameter + Ord; + #[pallet::constant] + type MaxOfflineSessions: Get<SessionIndex>; + type RefreshValidatorIdOrigin: EnsureOrigin<Self::Origin>; + type RemoveMemberOrigin: EnsureOrigin<Self::Origin>; + } + + // GENESIS STUFF // + + #[pallet::genesis_config] + pub struct GenesisConfig<T: Config> { + pub initial_authorities: BTreeMap<T::MemberId, T::ValidatorId>, + } + + #[cfg(feature = "std")] + impl<T: Config> Default for GenesisConfig<T> { + fn default() -> Self { + Self { + initial_authorities: BTreeMap::new(), + } + } + } + + #[pallet::genesis_build] + impl<T: Config> GenesisBuild<T> for GenesisConfig<T> { + fn build(&self) { + for (member_id, validator_id) in &self.initial_authorities { + Members::<T>::insert(member_id, MemberData::new_genesis(validator_id.clone())); + } + let mut members_ids = self + .initial_authorities + .keys() + .copied() + .collect::<Vec<T::MemberId>>(); + members_ids.sort(); + + OnlineAuthorities::<T>::put(members_ids); + } + } + + // STORAGE // + + #[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<_, Blake2_128Concat, T::MemberId, MemberData<T::ValidatorId>, OptionQuery>; + + #[pallet::storage] + #[pallet::getter(fn members_expire_on)] + pub type MembersExpireOn<T: Config> = + StorageMap<_, Blake2_128Concat, SessionIndex, Vec<T::MemberId>, ValueQuery>; + + // HOOKS // + + // EVENTS // + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event<T: Config> { + /// List of members who will enter the set of authorities at the next session. + /// [Vec<member_id>] + IncomingAuthorities(Vec<T::MemberId>), + /// List of members who will leave the set of authorities at the next session. + /// [Vec<member_id>] + OutgoingAuthorities(Vec<T::MemberId>), + /// A member will leave the set of authorities in 2 sessions. + /// [member_id] + MemberGoOffline(T::MemberId), + /// A member will enter the set of authorities in 2 sessions. + /// [member_id] + MemberGoOnline(T::MemberId), + /// A member has lost the right to be part of the authorities, he will be removed from + //// the authority set in 2 sessions. + /// [member_id] + MemberRemoved(T::MemberId), + } + + // ERRORS // + + #[pallet::error] + pub enum Error<T> { + /// Already incoming + AlreadyIncoming, + /// Already online + AlreadyOnline, + /// Already outgoing + AlreadyOutgoing, + /// Not found owner key + OwnerKeyNotFound, + /// Neither online nor scheduled + NotOnlineNorIncoming, + /// Not owner + NotOwner, + /// Not member + NotMember, + /// Session keys not provided + SessionKeysNotProvided, + } + + // CALLS // + + #[pallet::call] + impl<T: Config> Pallet<T> { + #[pallet::weight(0)] + pub fn go_offline( + origin: OriginFor<T>, + member_id: T::MemberId, + ) -> DispatchResultWithPostInfo { + // Verification phase // + let who = ensure_signed(origin)?; + Self::verify_ownership_and_membership(&who, member_id)?; + + let member_data = + Members::<T>::try_get(member_id).map_err(|_| Error::<T>::SessionKeysNotProvided)?; + if !member_data.session_keys_provided { + return Err(Error::<T>::SessionKeysNotProvided.into()); + } + if !Self::is_online(member_id) && !Self::is_incoming(member_id) { + return Err(Error::<T>::NotOnlineNorIncoming.into()); + } + + // Apply phase // + if !Self::insert_out(member_id) { + Err(Error::<T>::AlreadyOutgoing.into()) + } else { + Self::remove_in(member_id); + Ok(().into()) + } + } + #[pallet::weight(0)] + pub fn go_online( + origin: OriginFor<T>, + member_id: T::MemberId, + ) -> DispatchResultWithPostInfo { + // Verification phase // + let who = ensure_signed(origin)?; + Self::verify_ownership_and_membership(&who, member_id)?; + + let member_data = + Members::<T>::try_get(member_id).map_err(|_| Error::<T>::SessionKeysNotProvided)?; + if !member_data.session_keys_provided { + return Err(Error::<T>::SessionKeysNotProvided.into()); + } + + // Apply phase // + if Self::is_online(member_id) { + if Self::is_outgoing(member_id) { + Self::remove_out(member_id); + Ok(().into()) + } else { + Err(Error::<T>::AlreadyOnline.into()) + } + } else if Self::is_outgoing(member_id) { + Self::remove_out(member_id); + Ok(().into()) + } else if !Self::insert_in(member_id) { + Err(Error::<T>::AlreadyIncoming.into()) + } else { + Ok(().into()) + } + } + + #[pallet::weight(0)] + pub fn set_session_keys( + origin: OriginFor<T>, + member_id: T::MemberId, + keys: T::Keys, + ) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin.clone())?; + Self::verify_ownership_and_membership(&who, member_id)?; + + let validator_id = T::ValidatorIdOf::convert(who) + .ok_or(pallet_session::Error::<T>::NoAssociatedValidatorId)?; + + let _post_info = pallet_session::Call::<T>::set_keys { + keys, + proof: vec![], + } + .dispatch_bypass_filter(origin)?; + + let expire_on_session = pallet_session::Pallet::<T>::current_index() + .saturating_add(T::MaxOfflineSessions::get()); + + Members::<T>::mutate_exists(member_id, |member_data_opt| { + let mut member_data = member_data_opt.get_or_insert(MemberData { + expire_on_session, + session_keys_provided: true, + validator_id: validator_id.clone(), + }); + member_data.session_keys_provided = true; + member_data.validator_id = validator_id; + }); + + Ok(().into()) + } + #[pallet::weight(0)] + pub fn refresh_validator_id( + origin: OriginFor<T>, + member_id: T::MemberId, + ) -> DispatchResultWithPostInfo { + T::RefreshValidatorIdOrigin::ensure_origin(origin)?; + + let owner = T::OwnerKeyOf::convert(member_id).ok_or(Error::<T>::OwnerKeyNotFound)?; + let validator_id = T::ValidatorIdOf::convert(owner) + .ok_or(pallet_session::Error::<T>::NoAssociatedValidatorId)?; + + if !T::IsMember::is_member(&member_id) { + return Err(Error::<T>::NotMember.into()); + } + + let expire_on_session = pallet_session::Pallet::<T>::current_index() + .saturating_add(T::MaxOfflineSessions::get()); + + Members::<T>::mutate(member_id, |member_data_opt| { + let validator_id_clone = validator_id.clone(); + member_data_opt + .get_or_insert(MemberData::new(validator_id, expire_on_session)) + .validator_id = validator_id_clone; + }); + + Ok(().into()) + } + #[pallet::weight(0)] + pub fn remove_member( + origin: OriginFor<T>, + member_id: T::MemberId, + ) -> DispatchResultWithPostInfo { + T::RemoveMemberOrigin::ensure_origin(origin)?; + + if !T::IsMember::is_member(&member_id) { + return Err(Error::<T>::NotMember.into()); + } + + if let Some(owner) = T::OwnerKeyOf::convert(member_id) { + let _post_info = pallet_session::Call::<T>::purge_keys {} + .dispatch_bypass_filter(frame_system::Origin::<T>::Signed(owner).into())?; + } + + Self::do_remove_member(member_id); + + Ok(().into()) + } + } + + // INTERNAL FUNCTIONS // + + impl<T: Config> Pallet<T> { + fn do_remove_member(member_id: T::MemberId) -> Weight { + if Self::is_online(member_id) { + // Trigger the member deletion for next session + Self::insert_out(member_id); + } + + // remove all member data + Self::remove_in(member_id); + Self::remove_online(member_id); + Members::<T>::remove(member_id); + + Self::deposit_event(Event::MemberRemoved(member_id)); + let _ = T::OnRemovedMember::on_removed_member(member_id); + + 0 + } + pub(super) fn expire_memberships(current_session_index: SessionIndex) { + for member_id in MembersExpireOn::<T>::take(current_session_index) { + if let Some(member_data) = Members::<T>::get(member_id) { + if member_data.expire_on_session == current_session_index { + Self::do_remove_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_id)); + } + not_already_inserted + } + 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_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 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, + member_id: T::MemberId, + ) -> Result<(), DispatchError> { + if let Some(owner) = T::OwnerKeyOf::convert(member_id) { + if who != &owner { + return Err(Error::<T>::NotOwner.into()); + } + } else { + return Err(Error::<T>::OwnerKeyNotFound.into()); + } + + if !T::IsMember::is_member(&member_id) { + return Err(Error::<T>::NotMember.into()); + } + + Ok(()) + } + } +} + +impl<T: Config> pallet_session::SessionManager<T::ValidatorId> for Pallet<T> { + /// Plan a new session, and optionally provide the new validator set. + /// + /// Even if the validator-set is the same as before, if any underlying economic conditions have + /// changed (i.e. stake-weights), the new validator set must be returned. This is necessary for + /// consensus engines making use of the session pallet to issue a validator-set change so + /// misbehavior can be provably associated with the new economic conditions as opposed to the + /// old. The returned validator set, if any, will not be applied until `new_index`. `new_index` + /// is strictly greater than from previous call. + /// + /// The first session start at index 0. + /// + /// `new_session(session)` is guaranteed to be called before `end_session(session-1)`. In other + /// words, a new session must always be planned before an ongoing one can be finished. + 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() { + if members_ids_to_del.is_empty() { + return None; + } else { + // Apply MaxOfflineSessions rule + for member_id in &members_ids_to_del { + let expire_on_session = + session_index.saturating_add(T::MaxOfflineSessions::get()); + Members::<T>::mutate_exists(member_id, |member_data_opt| { + if let Some(ref mut member_data) = member_data_opt { + member_data.expire_on_session = expire_on_session; + } + }); + MembersExpireOn::<T>::append(expire_on_session, member_id); + } + Self::deposit_event(Event::OutgoingAuthorities(members_ids_to_del.clone())); + } + } else { + Self::deposit_event(Event::IncomingAuthorities(members_ids_to_add.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() + }) + .iter() + .filter_map(Members::<T>::get) + .map(|member_data| member_data.validator_id) + .collect(), + ) + } + /// Same as `new_session`, but it this should only be called at genesis. + /// + /// The session manager might decide to treat this in a different way. Default impl is simply + /// using [`new_session`](Self::new_session). + fn new_session_genesis(_new_index: SessionIndex) -> Option<Vec<T::ValidatorId>> { + None + } + /// End the session. + /// + /// Because the session pallet can queue validator set the ending session can be lower than the + /// last new session index. + fn end_session(_end_index: SessionIndex) {} + /// Start an already planned session. + /// + /// The session start to be used for validation. + fn start_session(start_index: SessionIndex) { + Self::expire_memberships(start_index); + } +} diff --git a/pallets/authority-members/src/mock.rs b/pallets/authority-members/src/mock.rs new file mode 100644 index 0000000000000000000000000000000000000000..a2b2faa495c6f4acb0e72ec3aa9e88d5a6d8580f --- /dev/null +++ b/pallets/authority-members/src/mock.rs @@ -0,0 +1,191 @@ +// Copyright 2021 Axiom-Team +// +// This file is part of Substrate-Libre-Currency. +// +// Substrate-Libre-Currency is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, version 3 of the License. +// +// Substrate-Libre-Currency is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with Substrate-Libre-Currency. If not, see <https://www.gnu.org/licenses/>. + +use super::*; +use crate::{self as pallet_authority_members}; +use frame_support::{ + pallet_prelude::*, + parameter_types, + traits::{Everything, GenesisBuild}, + BasicExternalities, +}; +use frame_system as system; +use pallet_session::ShouldEndSession; +use sp_core::{crypto::key_types::DUMMY, H256}; +use sp_runtime::{ + impl_opaque_keys, + testing::{Header, UintAuthorityId}, + traits::{BlakeTwo256, ConvertInto, IdentityLookup, IsMember, OpaqueKeys}, + KeyTypeId, +}; + +type AccountId = u64; +type Block = frame_system::mocking::MockBlock<Test>; +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic<Test>; + +impl_opaque_keys! { + pub struct MockSessionKeys { + pub dummy: UintAuthorityId, + } +} + +impl From<UintAuthorityId> for MockSessionKeys { + fn from(dummy: UintAuthorityId) -> Self { + Self { dummy } + } +} + +// Configure a mock runtime to test the pallet. +frame_support::construct_runtime!( + pub enum Test where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system::{Pallet, Call, Config, Storage, Event<T>}, + Session: pallet_session::{Pallet, Call, Storage, Config<T>, Event}, + AuthorityMembers: pallet_authority_members::{Pallet, Call, Storage, Config<T>, Event<T>}, + } +); + +parameter_types! { + pub const BlockHashCount: u64 = 250; + pub const SS58Prefix: u8 = 42; +} + +impl system::Config for Test { + type BaseCallFilter = Everything; + type BlockWeights = (); + type BlockLength = (); + type DbWeight = (); + type Origin = Origin; + type Call = Call; + type Index = u64; + type BlockNumber = u64; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup<Self::AccountId>; + type Header = Header; + type Event = Event; + type BlockHashCount = BlockHashCount; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = (); + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = SS58Prefix; + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; +} + +pub struct TestSessionHandler; +impl pallet_session::SessionHandler<u64> for TestSessionHandler { + const KEY_TYPE_IDS: &'static [KeyTypeId] = &[DUMMY]; + + fn on_new_session<Ks: OpaqueKeys>( + _changed: bool, + _validators: &[(u64, Ks)], + _queued_validators: &[(u64, Ks)], + ) { + } + + fn on_disabled(_validator_index: u32) {} + + fn on_genesis_session<Ks: OpaqueKeys>(_validators: &[(u64, Ks)]) {} +} + +const SESSION_LENGTH: u64 = 5; +pub struct TestShouldEndSession; +impl ShouldEndSession<u64> for TestShouldEndSession { + fn should_end_session(now: u64) -> bool { + now % SESSION_LENGTH == 0 + } +} + +impl pallet_session::Config for Test { + type Event = Event; + type ValidatorId = u64; + type ValidatorIdOf = sp_runtime::traits::ConvertInto; + type ShouldEndSession = TestShouldEndSession; + type NextSessionRotation = (); + type SessionManager = AuthorityMembers; + type SessionHandler = TestSessionHandler; + type Keys = MockSessionKeys; + type WeightInfo = (); +} + +pub struct TestIsSmithMember; +impl IsMember<u64> for TestIsSmithMember { + fn is_member(member_id: &u64) -> bool { + member_id % 3 == 0 + } +} + +impl pallet_authority_members::Config for Test { + type Event = Event; + type IsMember = TestIsSmithMember; + type MaxOfflineSessions = ConstU32<2>; + type MemberId = u64; + type OnRemovedMember = (); + type OwnerKeyOf = ConvertInto; + type RefreshValidatorIdOrigin = system::EnsureRoot<u64>; + type RemoveMemberOrigin = system::EnsureRoot<u64>; +} + +// Build genesis storage according to the mock runtime. +pub fn new_test_ext(initial_authorities_len: u64) -> sp_io::TestExternalities { + let initial_authorities = (1..=initial_authorities_len) + .map(|i| (i * 3, i * 3)) + .collect(); + let keys: Vec<_> = (1..=initial_authorities_len) + .map(|i| (i * 3, i * 3, UintAuthorityId(i * 3).into())) + .collect(); + + let mut t = frame_system::GenesisConfig::default() + .build_storage::<Test>() + .unwrap(); + BasicExternalities::execute_with_storage(&mut t, || { + for (ref k, ..) in &keys { + frame_system::Pallet::<Test>::inc_providers(k); + } + // A dedicated test account + frame_system::Pallet::<Test>::inc_providers(&12); + }); + pallet_authority_members::GenesisConfig::<Test> { + initial_authorities, + } + .assimilate_storage(&mut t) + .unwrap(); + pallet_session::GenesisConfig::<Test> { keys } + .assimilate_storage(&mut t) + .unwrap(); + sp_io::TestExternalities::new(t) +} + +pub fn run_to_block(n: u64) { + while System::block_number() < n { + Session::on_finalize(System::block_number()); + AuthorityMembers::on_initialize(System::block_number()); + System::on_finalize(System::block_number()); + System::reset_events(); + System::set_block_number(System::block_number() + 1); + System::on_initialize(System::block_number()); + AuthorityMembers::on_initialize(System::block_number()); + Session::on_initialize(System::block_number()); + } +} diff --git a/pallets/authority-members/src/tests.rs b/pallets/authority-members/src/tests.rs new file mode 100644 index 0000000000000000000000000000000000000000..7674aa8d72d628d146c62fefe7d528fdcdeaa2fb --- /dev/null +++ b/pallets/authority-members/src/tests.rs @@ -0,0 +1,239 @@ +// Copyright 2021 Axiom-Team +// +// This file is part of Substrate-Libre-Currency. +// +// Substrate-Libre-Currency is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, version 3 of the License. +// +// Substrate-Libre-Currency is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with Substrate-Libre-Currency. If not, see <https://www.gnu.org/licenses/>. + +use super::*; +use crate::mock::*; +use crate::MemberData; +use frame_support::assert_ok; +use sp_runtime::testing::UintAuthorityId; + +const EMPTY: Vec<u64> = Vec::new(); + +#[test] +fn test_genesis_build() { + new_test_ext(3).execute_with(|| { + run_to_block(1); + // Verify AuthorityMembers state + assert_eq!(AuthorityMembers::incoming(), EMPTY); + assert_eq!(AuthorityMembers::online(), vec![3, 6, 9]); + assert_eq!(AuthorityMembers::outgoing(), EMPTY); + assert_eq!( + AuthorityMembers::member(3), + Some(MemberData { + expire_on_session: 0, + session_keys_provided: true, + validator_id: 3, + }) + ); + assert_eq!( + AuthorityMembers::member(6), + Some(MemberData { + expire_on_session: 0, + session_keys_provided: true, + validator_id: 6, + }) + ); + assert_eq!( + AuthorityMembers::member(9), + Some(MemberData { + expire_on_session: 0, + session_keys_provided: true, + validator_id: 9, + }) + ); + + // Verify Session state + assert_eq!(Session::current_index(), 0); + assert_eq!(Session::validators(), vec![3, 6, 9]); + }); +} + +#[test] +fn test_new_session_shoud_not_change_authorities_set() { + new_test_ext(3).execute_with(|| { + run_to_block(6); + + assert_eq!(Session::current_index(), 1); + assert_eq!(Session::validators(), vec![3, 6, 9]); + }); +} + +#[test] +fn test_go_offline() { + new_test_ext(3).execute_with(|| { + run_to_block(1); + + // Member 9 should be able to go offline + assert_ok!(AuthorityMembers::go_offline(Origin::signed(9), 9),); + + // Verify state + assert_eq!(AuthorityMembers::incoming(), EMPTY); + assert_eq!(AuthorityMembers::online(), vec![3, 6, 9]); + assert_eq!(AuthorityMembers::outgoing(), vec![9]); + assert_eq!( + AuthorityMembers::member(9), + Some(MemberData { + expire_on_session: 0, + session_keys_provided: true, + validator_id: 9, + }) + ); + + // Member 9 should be "deprogrammed" at the next session + run_to_block(5); + assert_eq!( + AuthorityMembers::member(9), + Some(MemberData { + expire_on_session: 4, + session_keys_provided: true, + validator_id: 9, + }) + ); + assert_eq!(AuthorityMembers::members_expire_on(4), vec![9],); + assert_eq!(Session::current_index(), 1); + assert_eq!(Session::validators(), vec![3, 6, 9]); + assert_eq!(Session::queued_keys().len(), 2); + assert_eq!(Session::queued_keys()[0].0, 3); + assert_eq!(Session::queued_keys()[1].0, 6); + + // Member 9 should be **effectively** out at session 2 + run_to_block(10); + assert_eq!(Session::current_index(), 2); + assert_eq!(Session::validators(), vec![3, 6]); + + // Member 9 should be removed at session 4 + run_to_block(20); + assert_eq!(Session::current_index(), 4); + assert_eq!(Session::validators(), vec![3, 6]); + assert_eq!(AuthorityMembers::members_expire_on(4), EMPTY); + assert_eq!(AuthorityMembers::member(9), None); + }); +} + +#[test] +fn test_go_online() { + new_test_ext(3).execute_with(|| { + run_to_block(1); + + // Member 12 should be able to set his session keys + assert_ok!(AuthorityMembers::set_session_keys( + Origin::signed(12), + 12, + UintAuthorityId(12).into(), + )); + assert_eq!( + AuthorityMembers::member(12), + Some(MemberData { + expire_on_session: 2, + session_keys_provided: true, + validator_id: 12, + }) + ); + + // Member 12 should be able to go online + assert_ok!(AuthorityMembers::go_online(Origin::signed(12), 12),); + + // Verify state + assert_eq!(AuthorityMembers::incoming(), vec![12]); + assert_eq!(AuthorityMembers::online(), vec![3, 6, 9]); + assert_eq!(AuthorityMembers::outgoing(), EMPTY); + assert_eq!( + AuthorityMembers::member(12), + Some(MemberData { + expire_on_session: 2, + session_keys_provided: true, + validator_id: 12, + }) + ); + + // Member 12 should be "programmed" at the next session + run_to_block(5); + assert_eq!(Session::current_index(), 1); + assert_eq!(Session::validators(), vec![3, 6, 9]); + assert_eq!(Session::queued_keys().len(), 4); + assert_eq!(Session::queued_keys()[0].0, 3); + assert_eq!(Session::queued_keys()[1].0, 6); + assert_eq!(Session::queued_keys()[2].0, 9); + assert_eq!(Session::queued_keys()[3].0, 12); + + // Member 12 should be **effectively** in the authorities set in 2 sessions + run_to_block(10); + assert_eq!(Session::current_index(), 2); + assert_eq!(Session::validators(), vec![3, 6, 9, 12]); + }); +} + +#[test] +fn test_go_online_then_go_offline_in_same_session() { + new_test_ext(3).execute_with(|| { + run_to_block(1); + + // Member 12 set his session keys & go online + assert_ok!(AuthorityMembers::set_session_keys( + Origin::signed(12), + 12, + UintAuthorityId(12).into(), + )); + assert_ok!(AuthorityMembers::go_online(Origin::signed(12), 12),); + + run_to_block(2); + + // Member 12 should be able to go offline at the same session to "cancel" his previous + // action + assert_ok!(AuthorityMembers::go_offline(Origin::signed(12), 12),); + + // Verify state + assert_eq!(AuthorityMembers::incoming(), EMPTY); + assert_eq!(AuthorityMembers::online(), vec![3, 6, 9]); + assert_eq!(AuthorityMembers::outgoing(), vec![12]); + assert_eq!( + AuthorityMembers::member(12), + Some(MemberData { + expire_on_session: 2, + session_keys_provided: true, + validator_id: 12, + }) + ); + }); +} + +#[test] +fn test_go_offline_then_go_online_in_same_session() { + new_test_ext(3).execute_with(|| { + run_to_block(6); + + // Member 9 go offline + assert_ok!(AuthorityMembers::go_offline(Origin::signed(9), 9),); + + run_to_block(7); + + // Member 9 should be able to go online at the same session to "cancel" his previous action + assert_ok!(AuthorityMembers::go_online(Origin::signed(9), 9),); + + // Verify state + assert_eq!(AuthorityMembers::incoming(), EMPTY); + assert_eq!(AuthorityMembers::online(), vec![3, 6, 9]); + assert_eq!(AuthorityMembers::outgoing(), EMPTY); + assert_eq!( + AuthorityMembers::member(9), + Some(MemberData { + expire_on_session: 0, + session_keys_provided: true, + validator_id: 9, + }) + ); + }); +} diff --git a/pallets/authority-members/src/traits.rs b/pallets/authority-members/src/traits.rs new file mode 100644 index 0000000000000000000000000000000000000000..1d6b055557a8ffc96d854b25005147afa59f13e4 --- /dev/null +++ b/pallets/authority-members/src/traits.rs @@ -0,0 +1,27 @@ +// Copyright 2021 Axiom-Team +// +// This file is part of Substrate-Libre-Currency. +// +// Substrate-Libre-Currency is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, version 3 of the License. +// +// Substrate-Libre-Currency is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with Substrate-Libre-Currency. If not, see <https://www.gnu.org/licenses/>. + +use frame_support::pallet_prelude::Weight; + +pub trait OnRemovedMember<MemberId> { + fn on_removed_member(member_id: MemberId) -> Weight; +} + +impl<MemberId> OnRemovedMember<MemberId> for () { + fn on_removed_member(_: MemberId) -> Weight { + 0 + } +} diff --git a/pallets/authority-members/src/types.rs b/pallets/authority-members/src/types.rs new file mode 100644 index 0000000000000000000000000000000000000000..dd04dc8626be42ecb2575f2c8f09af948ad9ffe5 --- /dev/null +++ b/pallets/authority-members/src/types.rs @@ -0,0 +1,48 @@ +// Copyright 2021 Axiom-Team +// +// This file is part of Substrate-Libre-Currency. +// +// Substrate-Libre-Currency is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, version 3 of the License. +// +// Substrate-Libre-Currency is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with Substrate-Libre-Currency. If not, see <https://www.gnu.org/licenses/>. + +//! Various basic types for use in the certification pallet. + +use codec::{Decode, Encode}; +use scale_info::TypeInfo; +#[cfg(feature = "std")] +use serde::{Deserialize, Serialize}; +use sp_staking::SessionIndex; + +#[cfg_attr(feature = "std", derive(Debug, Deserialize, Serialize))] +#[derive(Encode, Decode, Clone, PartialEq, Eq, TypeInfo)] +pub struct MemberData<ValidatorId: Decode + Encode + TypeInfo> { + pub expire_on_session: SessionIndex, + pub session_keys_provided: bool, + pub validator_id: ValidatorId, +} + +impl<ValidatorId: Decode + Encode + TypeInfo> MemberData<ValidatorId> { + pub fn new(validator_id: ValidatorId, expire_on_session: SessionIndex) -> Self { + MemberData { + expire_on_session, + session_keys_provided: false, + validator_id, + } + } + pub fn new_genesis(validator_id: ValidatorId) -> Self { + MemberData { + expire_on_session: 0, + session_keys_provided: true, + validator_id, + } + } +} diff --git a/pallets/identity/src/lib.rs b/pallets/identity/src/lib.rs index 73bbeb3b42ab3f4142a51bd1ec235744e11fb569..0cfa26002ce4e74963f633285771f6f0874ebaf7 100644 --- a/pallets/identity/src/lib.rs +++ b/pallets/identity/src/lib.rs @@ -96,6 +96,7 @@ pub mod pallet { } // GENESIS STUFF // + #[pallet::genesis_config] pub struct GenesisConfig<T: Config> { pub identities: Vec<IdtyValue<T::AccountId, T::BlockNumber, T::IdtyData>>, diff --git a/runtime/gdev/Cargo.toml b/runtime/gdev/Cargo.toml index 3fc51dc0ec56e2ad73289007bb3e02f12098e534..8d88706dce4833d78581d8fd11cfdb5645476dd7 100644 --- a/runtime/gdev/Cargo.toml +++ b/runtime/gdev/Cargo.toml @@ -34,6 +34,7 @@ std = [ 'frame-system-rpc-runtime-api/std', 'frame-system/std', 'pallet-authority-discovery/std', + 'pallet-authority-members/std', 'pallet-babe/std', 'pallet-balances/std', 'pallet-certification/std', @@ -70,6 +71,7 @@ std = [ [dependencies] common-runtime = { path = "../common", default-features = false } +pallet-authority-members = { path = '../../pallets/authority-members', default-features = false } pallet-certification = { path = '../../pallets/certification', default-features = false } pallet-duniter-test-parameters = { path = '../../pallets/duniter-test-parameters', default-features = false } pallet-duniter-wot = { path = '../../pallets/duniter-wot', default-features = false }