diff --git a/Cargo.lock b/Cargo.lock index bf0582be32d2d0d02b857de5d58174d8833d8a15..9c56b56a52e47e3850f5ebd6cb2589c25c2db092 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4572,6 +4572,23 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-membership" +version = "3.0.0" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "maplit", + "parity-scale-codec", + "scale-info", + "serde", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", +] + [[package]] name = "pallet-multisig" version = "4.0.0-dev" diff --git a/Cargo.toml b/Cargo.toml index f896880d77cac757d088c91a6c5c81927d5570f3..2e4c71d5e414d89f9a366e0aac03a57b6b76557a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = [ 'node', 'pallets/certification', 'pallets/identity', + 'pallets/membership', 'pallets/ud-accounts-storage', 'pallets/universal-dividend', ] diff --git a/integration-tests/Cargo.toml b/integration-tests/Cargo.toml index c4971d2c64a14134b14c79913e6ebbc6528201cf..9bcb1a675ebd346ad4ccdf1f77e6db7a22ff4f52 100644 --- a/integration-tests/Cargo.toml +++ b/integration-tests/Cargo.toml @@ -5,7 +5,7 @@ edition = '2018' homepage = 'https://substrate.dev' license = 'AGPL-3.0' name = 'duniter-integration-tests' -repository = 'https://git.duniter.org/nodes/rust/duniter-substrate' +repository = 'https://git.duniter.org/nodes/rust/duniter-v2s' version = '3.0.0' [dev-dependencies] diff --git a/node/Cargo.toml b/node/Cargo.toml index fe76594fbe555bd236514cbcd09433b3c7443541..313c494a377790d669cefef3ba5ed3bfb5b79891 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -6,7 +6,7 @@ edition = '2018' homepage = 'https://substrate.dev' license = 'AGPL-3.0' name = 'duniter' -repository = 'https://git.duniter.org/nodes/rust/duniter-substrate' +repository = 'https://git.duniter.org/nodes/rust/duniter-v2s' version = '3.0.0' [package.metadata.docs.rs] diff --git a/pallets/certification/Cargo.toml b/pallets/certification/Cargo.toml index 59964b9ba053d1a53bf8c85fad12dfc5458782a6..c327cd2810b184658a58118ec0b79435dc64368b 100644 --- a/pallets/certification/Cargo.toml +++ b/pallets/certification/Cargo.toml @@ -6,7 +6,7 @@ homepage = 'https://substrate.dev' license = 'AGPL-3.0' name = 'pallet-certification' readme = 'README.md' -repository = 'https://git.duniter.org/nodes/rust/duniter-substrate' +repository = 'https://git.duniter.org/nodes/rust/duniter-v2s' version = '3.0.0' [features] diff --git a/pallets/certification/src/lib.rs b/pallets/certification/src/lib.rs index de8784028f34c63c955a42f3ccc8c2d2f0dfebb1..a129d24e9edfe843a9f95eccac338d9a65abc681 100644 --- a/pallets/certification/src/lib.rs +++ b/pallets/certification/src/lib.rs @@ -85,6 +85,8 @@ pub mod pallet { type ValidityPeriod: Get<Self::BlockNumber>; } + // GENESIS STUFFĂ‚ // + #[pallet::genesis_config] pub struct GenesisConfig<T: Config<I>, I: 'static = ()> { pub certs_by_issuer: BTreeMap<T::IdtyIndex, BTreeMap<T::IdtyIndex, T::BlockNumber>>, diff --git a/pallets/identity/Cargo.toml b/pallets/identity/Cargo.toml index 84202ac65ad74fcfa839fe10c1f10d36f18f67dd..0fbcb113af30a5822a592fc16b3a46e203f4b077 100644 --- a/pallets/identity/Cargo.toml +++ b/pallets/identity/Cargo.toml @@ -6,7 +6,7 @@ homepage = 'https://substrate.dev' license = 'AGPL-3.0' name = 'pallet-identity' readme = 'README.md' -repository = 'https://git.duniter.org/nodes/rust/duniter-substrate' +repository = 'https://git.duniter.org/nodes/rust/duniter-v2s' version = '3.0.0' [features] diff --git a/pallets/membership/Cargo.toml b/pallets/membership/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..1b49766c4b4babe7a1b9776937a7b4f7228f1705 --- /dev/null +++ b/pallets/membership/Cargo.toml @@ -0,0 +1,89 @@ +[package] +authors = ['librelois <c@elo.tf>'] +description = 'FRAME pallet membership.' +edition = '2018' +homepage = 'https://substrate.dev' +license = 'AGPL-3.0' +name = 'pallet-membership' +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', + 'serde', + 'sp-core/std', + 'sp-runtime/std', + 'sp-std/std', +] +try-runtime = ['frame-support/try-runtime'] + +[dependencies] + +# 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.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-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-dependencies.serde] +version = '1.0.119' + +### DEV ### + +[dev-dependencies.maplit] +version = '1.0.2' + +[dev-dependencies.sp-io] +default-features = false +git = 'https://github.com/librelois/substrate.git' +branch = 'duniter-monthly-2022-01' diff --git a/pallets/membership/src/lib.rs b/pallets/membership/src/lib.rs new file mode 100644 index 0000000000000000000000000000000000000000..7ecfdec32ec56ae4188ca808e38d3c841602b425 --- /dev/null +++ b/pallets/membership/src/lib.rs @@ -0,0 +1,509 @@ +// 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; +pub mod types; + +#[cfg(test)] +mod mock; + +#[cfg(test)] +mod tests; + +/*#[cfg(feature = "runtime-benchmarks")] +mod benchmarking;*/ + +pub use pallet::*; + +use crate::traits::*; +use crate::types::{MembershipData, OriginPermission}; +use frame_support::dispatch::Weight; +use frame_support::pallet_prelude::DispatchResultWithPostInfo; +use sp_runtime::traits::Zero; +use sp_std::prelude::*; +#[cfg(feature = "std")] +use std::collections::BTreeMap; + +#[frame_support::pallet] +pub mod pallet { + use super::*; + use frame_support::pallet_prelude::*; + use frame_support::traits::StorageVersion; + use frame_system::pallet_prelude::*; + + /// 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, I = ()>(_); + + // CONFIG // + + #[pallet::config] + pub trait Config<I: 'static = ()>: frame_system::Config { + type IsIdtyAllowedToClaimMembership: IsIdtyAllowedToClaimMembership<Self::IdtyId>; + type IsIdtyAllowedToRenewMembership: IsIdtyAllowedToRenewMembership<Self::IdtyId>; + type IsIdtyAllowedToRequestMembership: IsIdtyAllowedToRequestMembership<Self::IdtyId>; + type IsOriginAllowedToUseIdty: IsOriginAllowedToUseIdty<Self::Origin, Self::IdtyId>; + /// Because this pallet emits events, it depends on the runtime's definition of an event. + type Event: From<Event<Self, I>> + IsType<<Self as frame_system::Config>::Event>; + /// Specify true if you want to externalize the storage of memberships, but in this case + /// you must provide an implementation of `MembershipExternalStorage` + type ExternalizeMembershipStorage: Get<bool>; + /// Something that identifies an identity + type IdtyId: Copy + MaybeSerializeDeserialize + Parameter + Ord; + /// On event handler + type OnEvent: OnEvent<Self::IdtyId>; + /// Provide your implementation of membership storage here, if you want the pallet to + /// handle the storage for you, specify `()` and set `ExternalizeMembershipStorage` to + /// `false`. + type MembershipExternalStorage: MembershipExternalStorage<Self::BlockNumber, Self::IdtyId>; + #[pallet::constant] + /// Maximum life span of a non-renewable membership (in number of blocks) + type MembershipPeriod: Get<Self::BlockNumber>; + #[pallet::constant] + /// Maximum period (in number of blocks), where an identity can remain pending subscription. + type PendingMembershipPeriod: Get<Self::BlockNumber>; + #[pallet::constant] + /// Duration after which a membership is renewable + type RenewablePeriod: Get<Self::BlockNumber>; + #[pallet::constant] + /// Minimum duration (in number of blocks between a revocation and a new entry request + type RevocationPeriod: Get<Self::BlockNumber>; + } + + // GENESIS STUFFĂ‚ // + + #[pallet::genesis_config] + pub struct GenesisConfig<T: Config<I>, I: 'static = ()> { + pub memberships: BTreeMap<T::IdtyId, MembershipData<T::BlockNumber>>, + } + + #[cfg(feature = "std")] + impl<T: Config<I>, I: 'static> Default for GenesisConfig<T, I> { + fn default() -> Self { + Self { + memberships: Default::default(), + } + } + } + + #[pallet::genesis_build] + impl<T: Config<I>, I: 'static> GenesisBuild<T, I> for GenesisConfig<T, I> { + fn build(&self) { + for (idty_id, membership_data) in &self.memberships { + MembershipsExpireOn::<T, I>::append(membership_data.expire_on, idty_id); + if T::ExternalizeMembershipStorage::get() { + T::MembershipExternalStorage::insert(*idty_id, *membership_data); + } else { + Membership::<T, I>::insert(idty_id, membership_data); + } + } + } + } + + // STORAGE // + + #[pallet::storage] + #[pallet::getter(fn membership)] + pub type Membership<T: Config<I>, I: 'static = ()> = + StorageMap<_, Blake2_128Concat, T::IdtyId, MembershipData<T::BlockNumber>, OptionQuery>; + + #[pallet::storage] + #[pallet::getter(fn memberships_expire_on)] + pub type MembershipsExpireOn<T: Config<I>, I: 'static = ()> = + StorageMap<_, Blake2_128Concat, T::BlockNumber, Vec<T::IdtyId>, OptionQuery>; + + #[pallet::storage] + #[pallet::getter(fn pending_membership)] + pub type PendingMembership<T: Config<I>, I: 'static = ()> = + StorageMap<_, Blake2_128Concat, T::IdtyId, (), OptionQuery>; + + #[pallet::storage] + #[pallet::getter(fn pending_memberships_expire_on)] + pub type PendingMembershipsExpireOn<T: Config<I>, I: 'static = ()> = + StorageMap<_, Blake2_128Concat, T::BlockNumber, Vec<T::IdtyId>, OptionQuery>; + + #[pallet::storage] + #[pallet::getter(fn revoked_membership)] + pub type RevokedMembership<T: Config<I>, I: 'static = ()> = + StorageMap<_, Blake2_128Concat, T::IdtyId, (), OptionQuery>; + + #[pallet::storage] + #[pallet::getter(fn revoked_memberships_pruned_on)] + pub type RevokedMembershipsPrunedOn<T: Config<I>, I: 'static = ()> = + StorageMap<_, Blake2_128Concat, T::BlockNumber, Vec<T::IdtyId>, OptionQuery>; + + // EVENTS // + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event<T: Config<I>, I: 'static = ()> { + /// A membership has acquired + /// [idty_id] + MembershipAcquired(T::IdtyId), + /// A membership has expired + /// [idty_id] + MembershipExpired(T::IdtyId), + /// A membership has renewed + /// [idty_id] + MembershipRenewed(T::IdtyId), + /// An identity requested membership + /// [idty_id] + MembershipRequested(T::IdtyId), + /// A membership has revoked + /// [idty_id] + MembershipRevoked(T::IdtyId), + /// A pending membership request has expired + /// [idty_id] + PendingMembershipExpired(T::IdtyId), + } + + // ERRORS// + + #[pallet::error] + pub enum Error<T, I = ()> { + /// Identity not allowed to claim membership + IdtyNotAllowedToClaimMembership, + /// Identity not allowed to renew membership + IdtyNotAllowedToRenewMembership, + /// Identity not allowed to request membership + IdtyNotAllowedToRequestMembership, + /// Origin not allowed to use this identity + /// Membership not yet renewable + MembershipNotYetRenewable, + /// Membership not found + MembershipNotFound, + OriginNotAllowedToUseIdty, + /// Membership request not found + MembershipRequestNotFound, + /// Membership revoked recently + MembershipRevokedRecently, + } + + // HOOKS // + + #[pallet::hooks] + impl<T: Config<I>, I: 'static> Hooks<BlockNumberFor<T>> for Pallet<T, I> { + fn on_initialize(n: T::BlockNumber) -> Weight { + if n > T::BlockNumber::zero() { + Self::expire_pending_memberships(n) + + Self::expire_memberships(n) + + Self::prune_revoked_memberships(n) + } else { + 0 + } + } + } + + // CALLS // + + #[pallet::call] + impl<T: Config<I>, I: 'static> Pallet<T, I> { + #[pallet::weight(0)] + pub fn request_membership( + origin: OriginFor<T>, + idty_id: T::IdtyId, + ) -> DispatchResultWithPostInfo { + let allowed = + match T::IsOriginAllowedToUseIdty::is_origin_allowed_to_use_idty(&origin, &idty_id) + { + OriginPermission::Forbidden => { + return Err(Error::<T, I>::OriginNotAllowedToUseIdty.into()) + } + OriginPermission::Allowed => { + T::IsIdtyAllowedToRequestMembership::is_idty_allowed_to_request_membership( + &idty_id, + ) + } + OriginPermission::Root => true, + }; + if !allowed { + return Err(Error::<T, I>::IdtyNotAllowedToRequestMembership.into()); + } + if RevokedMembership::<T, I>::contains_key(&idty_id) { + return Err(Error::<T, I>::MembershipRevokedRecently.into()); + } + + let block_number = frame_system::pallet::Pallet::<T>::block_number(); + let expire_on = block_number + T::PendingMembershipPeriod::get(); + + PendingMembership::<T, I>::insert(idty_id, ()); + PendingMembershipsExpireOn::<T, I>::append(expire_on, idty_id); + Self::deposit_event(Event::MembershipRequested(idty_id)); + T::OnEvent::on_event(crate::types::Event::MembershipRequested(idty_id)); + + Ok(().into()) + } + + #[pallet::weight(0)] + pub fn claim_membership( + origin: OriginFor<T>, + idty_id: T::IdtyId, + ) -> DispatchResultWithPostInfo { + let allowed = + match T::IsOriginAllowedToUseIdty::is_origin_allowed_to_use_idty(&origin, &idty_id) + { + OriginPermission::Forbidden => { + return Err(Error::<T, I>::OriginNotAllowedToUseIdty.into()) + } + OriginPermission::Allowed => { + T::IsIdtyAllowedToClaimMembership::is_idty_allowed_to_claim_membership( + &idty_id, + ) + } + OriginPermission::Root => true, + }; + if !allowed { + return Err(Error::<T, I>::IdtyNotAllowedToClaimMembership.into()); + } + + if !PendingMembership::<T, I>::contains_key(&idty_id) { + return Err(Error::<T, I>::MembershipRequestNotFound.into()); + } + + let _ = Self::do_claim_membership(idty_id); + + Ok(().into()) + } + + #[pallet::weight(0)] + pub fn renew_membership( + origin: OriginFor<T>, + idty_id: T::IdtyId, + ) -> DispatchResultWithPostInfo { + let allowed = + match T::IsOriginAllowedToUseIdty::is_origin_allowed_to_use_idty(&origin, &idty_id) + { + OriginPermission::Forbidden => { + return Err(Error::<T, I>::OriginNotAllowedToUseIdty.into()) + } + OriginPermission::Allowed => { + T::IsIdtyAllowedToRenewMembership::is_idty_allowed_to_renew_membership( + &idty_id, + ) + } + OriginPermission::Root => true, + }; + if !allowed { + return Err(Error::<T, I>::IdtyNotAllowedToRenewMembership.into()); + } + + if let Some(membership_data) = Self::get_membership(&idty_id) { + let block_number = frame_system::pallet::Pallet::<T>::block_number(); + if membership_data.renewable_on > block_number { + return Err(Error::<T, I>::MembershipNotYetRenewable.into()); + } + } else { + return Err(Error::<T, I>::MembershipNotFound.into()); + } + + let _ = Self::do_renew_membership(idty_id); + + Ok(().into()) + } + + #[pallet::weight(0)] + pub fn revoke_membership( + origin: OriginFor<T>, + idty_id: T::IdtyId, + ) -> DispatchResultWithPostInfo { + if T::IsOriginAllowedToUseIdty::is_origin_allowed_to_use_idty(&origin, &idty_id) + == OriginPermission::Forbidden + { + return Err(Error::<T, I>::OriginNotAllowedToUseIdty.into()); + } + + let _ = Self::do_revoke_membership(idty_id); + + Ok(().into()) + } + } + + // INTERNAL FUNCTIONS // + + impl<T: Config<I>, I: 'static> Pallet<T, I> { + pub(super) fn do_claim_membership(idty_id: T::IdtyId) -> Weight { + let mut total_weight = 1; + PendingMembership::<T, I>::remove(&idty_id); + total_weight += Self::do_renew_membership_inner(idty_id); + Self::deposit_event(Event::MembershipAcquired(idty_id)); + T::OnEvent::on_event(crate::types::Event::MembershipAcquired(idty_id)); + total_weight + } + pub(super) fn do_renew_membership(idty_id: T::IdtyId) -> Weight { + let total_weight = Self::do_renew_membership_inner(idty_id); + Self::deposit_event(Event::MembershipRenewed(idty_id)); + T::OnEvent::on_event(crate::types::Event::MembershipRenewed(idty_id)); + total_weight + } + fn do_renew_membership_inner(idty_id: T::IdtyId) -> Weight { + let block_number = frame_system::pallet::Pallet::<T>::block_number(); + let expire_on = block_number + T::MembershipPeriod::get(); + let renewable_on = block_number + T::RenewablePeriod::get(); + + Self::insert_membership( + idty_id, + MembershipData { + expire_on, + renewable_on, + }, + ); + MembershipsExpireOn::<T, I>::append(expire_on, idty_id); + 0 + } + pub(super) fn do_revoke_membership(idty_id: T::IdtyId) -> Weight { + let block_number = frame_system::pallet::Pallet::<T>::block_number(); + let pruned_on = block_number + T::RevocationPeriod::get(); + + Self::remove_membership(&idty_id); + RevokedMembership::<T, I>::insert(idty_id, ()); + RevokedMembershipsPrunedOn::<T, I>::append(pruned_on, idty_id); + Self::deposit_event(Event::MembershipRevoked(idty_id)); + T::OnEvent::on_event(crate::types::Event::MembershipRevoked(idty_id)); + 0 + } + fn expire_memberships(block_number: T::BlockNumber) -> Weight { + let mut total_weight: Weight = 0; + + use frame_support::storage::generator::StorageMap as _; + if let Some(identities_ids) = MembershipsExpireOn::<T, I>::from_query_to_optional_value( + MembershipsExpireOn::<T, I>::take(block_number), + ) { + for idty_id in identities_ids { + if let Some(member_data) = Self::get_membership(&idty_id) { + if member_data.expire_on == block_number { + Self::remove_membership(&idty_id); + Self::deposit_event(Event::MembershipExpired(idty_id)); + total_weight += T::OnEvent::on_event( + crate::types::Event::MembershipExpired(idty_id), + ); + } + } + } + } + + total_weight + } + fn expire_pending_memberships(block_number: T::BlockNumber) -> Weight { + let mut total_weight: Weight = 0; + + use frame_support::storage::generator::StorageMap as _; + if let Some(identities_ids) = + PendingMembershipsExpireOn::<T, I>::from_query_to_optional_value( + PendingMembershipsExpireOn::<T, I>::take(block_number), + ) + { + for idty_id in identities_ids { + PendingMembership::<T, I>::remove(&idty_id); + Self::deposit_event(Event::PendingMembershipExpired(idty_id)); + total_weight += T::OnEvent::on_event( + crate::types::Event::PendingMembershipExpired(idty_id), + ); + } + } + + total_weight + } + fn prune_revoked_memberships(block_number: T::BlockNumber) -> Weight { + let total_weight: Weight = 0; + + use frame_support::storage::generator::StorageMap as _; + if let Some(identities_ids) = + RevokedMembershipsPrunedOn::<T, I>::from_query_to_optional_value( + RevokedMembershipsPrunedOn::<T, I>::take(block_number), + ) + { + for idty_id in identities_ids { + RevokedMembership::<T, I>::remove(idty_id); + } + } + + total_weight + } + + pub(super) fn is_member_inner(idty_id: &T::IdtyId) -> bool { + if T::ExternalizeMembershipStorage::get() { + T::MembershipExternalStorage::is_member(idty_id) + } else { + Membership::<T, I>::contains_key(idty_id) + } + } + fn insert_membership(idty_id: T::IdtyId, membership_data: MembershipData<T::BlockNumber>) { + if T::ExternalizeMembershipStorage::get() { + T::MembershipExternalStorage::insert(idty_id, membership_data); + } else { + Membership::<T, I>::insert(idty_id, membership_data); + } + } + fn get_membership(idty_id: &T::IdtyId) -> Option<MembershipData<T::BlockNumber>> { + if T::ExternalizeMembershipStorage::get() { + T::MembershipExternalStorage::get(idty_id) + } else { + Membership::<T, I>::try_get(idty_id).ok() + } + } + fn remove_membership(idty_id: &T::IdtyId) { + if T::ExternalizeMembershipStorage::get() { + T::MembershipExternalStorage::remove(idty_id); + } else { + Membership::<T, I>::remove(idty_id); + } + } + } +} + +impl<T: Config<I>, I: 'static> IsInPendingMemberships<T::IdtyId> for Pallet<T, I> { + fn is_in_pending_memberships(idty_id: T::IdtyId) -> bool { + PendingMembership::<T, I>::contains_key(idty_id) + } +} + +impl<T: Config<I>, I: 'static> IsMember<T::IdtyId> for Pallet<T, I> { + fn is_member(idty_id: &T::IdtyId) -> bool { + Self::is_member_inner(&idty_id) + } +} + +impl<T: Config<I>, I: 'static> MembershipAction<T::IdtyId, T::Origin> for Pallet<T, I> { + fn request_membership_(origin: T::Origin, idty_id: T::IdtyId) -> DispatchResultWithPostInfo { + Pallet::<T, I>::request_membership(origin, idty_id) + } + fn claim_membership_(origin: T::Origin, idty_id: T::IdtyId) -> DispatchResultWithPostInfo { + Pallet::<T, I>::claim_membership(origin, idty_id) + } + fn renew_membership_(origin: T::Origin, idty_id: T::IdtyId) -> DispatchResultWithPostInfo { + Pallet::<T, I>::renew_membership(origin, idty_id) + } + fn revoke_membership_(origin: T::Origin, idty_id: T::IdtyId) -> DispatchResultWithPostInfo { + Pallet::<T, I>::revoke_membership(origin, idty_id) + } + + fn force_claim_membership(idty_id: T::IdtyId) -> Weight { + Self::do_claim_membership(idty_id) + } + fn force_renew_membership(idty_id: T::IdtyId) -> Weight { + Self::do_renew_membership(idty_id) + } + fn force_revoke_membership(idty_id: T::IdtyId) -> Weight { + Self::do_revoke_membership(idty_id) + } +} diff --git a/pallets/membership/src/mock.rs b/pallets/membership/src/mock.rs new file mode 100644 index 0000000000000000000000000000000000000000..237d93a2794154501e2c06ab7d2aa9afe6d87703 --- /dev/null +++ b/pallets/membership/src/mock.rs @@ -0,0 +1,139 @@ +// 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 crate::traits::IsOriginAllowedToUseIdty; +use crate::types::OriginPermission; +use crate::{self as pallet_membership}; +use frame_support::{ + parameter_types, + traits::{Everything, OnFinalize, OnInitialize}, +}; +use frame_system as system; +use sp_core::H256; +use sp_runtime::{ + testing::Header, + traits::{BlakeTwo256, IdentityLookup}, + BuildStorage, +}; + +type AccountId = u64; +type BlockNumber = u64; +type Block = frame_system::mocking::MockBlock<Test>; +pub type IdtyId = u64; +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic<Test>; + +// 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>}, + DefaultMembership: pallet_membership::{Pallet, Call, Event<T>, Storage, Config<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 = BlockNumber; + 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 IsOriginAllowedToUseIdtyImpl; +impl IsOriginAllowedToUseIdty<Origin, IdtyId> for IsOriginAllowedToUseIdtyImpl { + fn is_origin_allowed_to_use_idty(o: &Origin, idty_id: &IdtyId) -> OriginPermission { + match o.clone().into() { + Ok(system::RawOrigin::Root) => OriginPermission::Root, + Ok(system::RawOrigin::Signed(account_id)) if account_id == *idty_id => { + OriginPermission::Allowed + } + _ => OriginPermission::Forbidden, + } + } +} + +parameter_types! { + pub const ExternalizeMembershipStorage: bool = false; + pub const MembershipPeriod: BlockNumber = 5; + pub const PendingMembershipPeriod: BlockNumber = 3; + pub const RenewablePeriod: BlockNumber = 2; + pub const RevocationPeriod: BlockNumber = 4; +} + +impl pallet_membership::Config for Test { + type IsIdtyAllowedToClaimMembership = (); + type IsIdtyAllowedToRenewMembership = (); + type IsIdtyAllowedToRequestMembership = (); + type IsOriginAllowedToUseIdty = IsOriginAllowedToUseIdtyImpl; + type Event = Event; + type ExternalizeMembershipStorage = ExternalizeMembershipStorage; + type IdtyId = IdtyId; + type OnEvent = (); + type MembershipExternalStorage = (); + type MembershipPeriod = MembershipPeriod; + type PendingMembershipPeriod = PendingMembershipPeriod; + type RenewablePeriod = RenewablePeriod; + type RevocationPeriod = RevocationPeriod; +} + +// Build genesis storage according to the mock runtime. +pub fn new_test_ext(gen_conf: pallet_membership::GenesisConfig<Test>) -> sp_io::TestExternalities { + GenesisConfig { + system: SystemConfig::default(), + default_membership: gen_conf, + } + .build_storage() + .unwrap() + .into() +} + +pub fn run_to_block(n: u64) { + while System::block_number() < n { + DefaultMembership::on_finalize(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()); + DefaultMembership::on_initialize(System::block_number()); + } +} diff --git a/pallets/membership/src/tests.rs b/pallets/membership/src/tests.rs new file mode 100644 index 0000000000000000000000000000000000000000..f98e49909f25194f0fc3af5a6dee85eb21c086cb --- /dev/null +++ b/pallets/membership/src/tests.rs @@ -0,0 +1,199 @@ +// 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 crate::mock::Event as RuntimeEvent; +use crate::mock::*; +use crate::traits::{IsInPendingMemberships, IsMember}; +use crate::types::MembershipData; +use crate::{Error, Event}; +use frame_support::assert_ok; +use maplit::btreemap; + +fn default_gen_conf() -> DefaultMembershipConfig { + DefaultMembershipConfig { + memberships: btreemap![ + 0 => MembershipData { + expire_on: 3, + renewable_on: 2 + } + ], + } +} + +#[test] +fn test_genesis_build() { + new_test_ext(default_gen_conf()).execute_with(|| { + run_to_block(1); + // Verify state + assert_eq!( + DefaultMembership::membership(0), + Some(MembershipData { + expire_on: 3, + renewable_on: 2 + }) + ); + }); +} + +#[test] +fn test_membership_not_yet_renewable() { + new_test_ext(default_gen_conf()).execute_with(|| { + run_to_block(1); + // Merbership 0 cannot be renewed before #2 + assert_eq!( + DefaultMembership::renew_membership(Origin::signed(0), 0), + Err(Error::<Test, _>::MembershipNotYetRenewable.into()) + ); + }); +} + +#[test] +fn test_membership_request_not_found() { + new_test_ext(default_gen_conf()).execute_with(|| { + run_to_block(1); + // Merbership 0 cannot be reclaimed + assert_eq!( + DefaultMembership::claim_membership(Origin::signed(0), 0), + Err(Error::<Test, _>::MembershipRequestNotFound.into()) + ); + }); +} + +#[test] +fn test_membership_renewal() { + new_test_ext(default_gen_conf()).execute_with(|| { + run_to_block(2); + // Merbership 0 can be renewable on block #2 + assert_ok!(DefaultMembership::renew_membership(Origin::signed(0), 0),); + assert_eq!( + System::events()[0].event, + RuntimeEvent::DefaultMembership(Event::MembershipRenewed(0)) + ); + }); +} + +#[test] +fn test_membership_expiration() { + new_test_ext(default_gen_conf()).execute_with(|| { + // Merbership 0 should not expired on block #2 + run_to_block(2); + assert!(DefaultMembership::is_member(&0),); + // Merbership 0 should expire on block #3 + run_to_block(3); + assert!(!DefaultMembership::is_member(&0),); + assert_eq!( + System::events()[0].event, + RuntimeEvent::DefaultMembership(Event::MembershipExpired(0)) + ); + }); +} + +#[test] +fn test_membership_revocation() { + new_test_ext(default_gen_conf()).execute_with(|| { + run_to_block(1); + // Merbership 0 can be revocable on block #1 + assert_ok!(DefaultMembership::revoke_membership(Origin::signed(0), 0),); + assert_eq!( + System::events()[0].event, + RuntimeEvent::DefaultMembership(Event::MembershipRevoked(0)) + ); + + // Membership 0 can't request membership before the end of RevokePeriod (1 + 4 = 5) + run_to_block(2); + assert_eq!( + DefaultMembership::request_membership(Origin::signed(0), 0), + Err(Error::<Test, _>::MembershipRevokedRecently.into()) + ); + + // Membership 0 can request membership after the end of RevokePeriod (1 + 4 = 5) + run_to_block(5); + assert_ok!(DefaultMembership::request_membership(Origin::signed(0), 0),); + assert_eq!( + System::events()[0].event, + RuntimeEvent::DefaultMembership(Event::MembershipRequested(0)) + ); + }); +} + +#[test] +fn test_pending_membership_expiration() { + new_test_ext(Default::default()).execute_with(|| { + // Idty 0 request membership + run_to_block(1); + assert_ok!(DefaultMembership::request_membership(Origin::signed(0), 0),); + assert_eq!( + System::events()[0].event, + RuntimeEvent::DefaultMembership(Event::MembershipRequested(0)) + ); + + // Then, idty 0 shold still in pending memberships until PendingMembershipPeriod ended + run_to_block(PendingMembershipPeriod::get()); + assert!(DefaultMembership::is_in_pending_memberships(0),); + + // Then, idty 0 request should expire after PendingMembershipPeriod + run_to_block(1 + PendingMembershipPeriod::get()); + assert!(!DefaultMembership::is_in_pending_memberships(0),); + assert_eq!( + System::events()[0].event, + RuntimeEvent::DefaultMembership(Event::PendingMembershipExpired(0)) + ); + }) +} + +#[test] +fn test_membership_workflow() { + new_test_ext(Default::default()).execute_with(|| { + // Idty 0 request membership + run_to_block(1); + assert_ok!(DefaultMembership::request_membership(Origin::signed(0), 0),); + assert_eq!( + System::events()[0].event, + RuntimeEvent::DefaultMembership(Event::MembershipRequested(0)) + ); + + // Then, idty 0 claim membership + run_to_block(2); + assert_ok!(DefaultMembership::claim_membership(Origin::signed(0), 0),); + assert_eq!( + System::events()[0].event, + RuntimeEvent::DefaultMembership(Event::MembershipAcquired(0)) + ); + + // Then, idty 0 claim renewal, should fail + run_to_block(3); + assert_eq!( + DefaultMembership::renew_membership(Origin::signed(0), 0), + Err(Error::<Test, _>::MembershipNotYetRenewable.into()) + ); + + // Then, idty 0 claim renewal after renewable period, should success + run_to_block(2 + RenewablePeriod::get()); + assert_ok!(DefaultMembership::renew_membership(Origin::signed(0), 0),); + + // Then, idty 0 shoul still member until membership period ended + run_to_block(2 + RenewablePeriod::get() + MembershipPeriod::get() - 1); + assert!(DefaultMembership::is_member(&0)); + + // Then, idty 0 shoul expire after membership period + run_to_block(2 + RenewablePeriod::get() + MembershipPeriod::get()); + assert!(!DefaultMembership::is_member(&0),); + assert_eq!( + System::events()[0].event, + RuntimeEvent::DefaultMembership(Event::MembershipExpired(0)) + ); + }); +} diff --git a/pallets/membership/src/traits.rs b/pallets/membership/src/traits.rs new file mode 100644 index 0000000000000000000000000000000000000000..c0360b20157f2e9120a49a35d135349749269d7d --- /dev/null +++ b/pallets/membership/src/traits.rs @@ -0,0 +1,119 @@ +// 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 crate::types::OriginPermission; +use crate::*; +use frame_support::pallet_prelude::DispatchResultWithPostInfo; + +pub trait IsIdtyAllowedToClaimMembership<IdtyId> { + fn is_idty_allowed_to_claim_membership(idty_id: &IdtyId) -> bool; +} + +impl<IdtyId> IsIdtyAllowedToClaimMembership<IdtyId> for () { + fn is_idty_allowed_to_claim_membership(_: &IdtyId) -> bool { + true + } +} + +pub trait IsIdtyAllowedToRenewMembership<IdtyId> { + fn is_idty_allowed_to_renew_membership(idty_id: &IdtyId) -> bool; +} + +impl<IdtyId> IsIdtyAllowedToRenewMembership<IdtyId> for () { + fn is_idty_allowed_to_renew_membership(_: &IdtyId) -> bool { + true + } +} + +pub trait IsIdtyAllowedToRequestMembership<IdtyId> { + fn is_idty_allowed_to_request_membership(idty_id: &IdtyId) -> bool; +} + +impl<IdtyId> IsIdtyAllowedToRequestMembership<IdtyId> for () { + fn is_idty_allowed_to_request_membership(_: &IdtyId) -> bool { + true + } +} + +pub trait IsOriginAllowedToUseIdty<Origin, IdtyId> { + fn is_origin_allowed_to_use_idty(origin: &Origin, idty_id: &IdtyId) -> OriginPermission; +} + +impl<Origin, IdtyId> IsOriginAllowedToUseIdty<Origin, IdtyId> for () { + fn is_origin_allowed_to_use_idty(_: &Origin, _: &IdtyId) -> OriginPermission { + OriginPermission::Allowed + } +} + +pub trait IsInPendingMemberships<IdtyId> { + fn is_in_pending_memberships(idty_id: IdtyId) -> bool; +} + +pub trait IsMember<IdtyId> { + fn is_member(idty_id: &IdtyId) -> bool; +} + +impl<IdtyId> IsMember<IdtyId> for () { + fn is_member(_: &IdtyId) -> bool { + false + } +} + +pub trait OnEvent<IdtyId> { + fn on_event(event: crate::types::Event<IdtyId>) -> Weight; +} + +impl<IdtyId> OnEvent<IdtyId> for () { + fn on_event(_: crate::types::Event<IdtyId>) -> Weight { + 0 + } +} + +pub trait MembershipAction<IdtyId, Origin> { + fn request_membership_(origin: Origin, idty_id: IdtyId) -> DispatchResultWithPostInfo; + fn claim_membership_(origin: Origin, idty_id: IdtyId) -> DispatchResultWithPostInfo; + fn renew_membership_(origin: Origin, idty_id: IdtyId) -> DispatchResultWithPostInfo; + fn revoke_membership_(origin: Origin, idty_id: IdtyId) -> DispatchResultWithPostInfo; + fn force_claim_membership(idty_id: IdtyId) -> Weight; + fn force_renew_membership(idty_id: IdtyId) -> Weight; + fn force_revoke_membership(idty_id: IdtyId) -> Weight; +} + +pub trait MembershipExternalStorage<BlockNumber: Decode + Encode + TypeInfo, IdtyId>: + IsMember<IdtyId> +{ + fn insert(idty_id: IdtyId, membership_data: MembershipData<BlockNumber>); + fn get(idty_id: &IdtyId) -> Option<MembershipData<BlockNumber>>; + fn remove(idty_id: &IdtyId); +} + +use codec::{Decode, Encode}; +use frame_support::pallet_prelude::TypeInfo; +static INVALID_CONF_MSG: &str = "invalid pallet configuration: if `MembershipExternalStorage` = (), you must set `ExternalizeMembershipStorage` to `false`."; + +impl<BlockNumber: Decode + Encode + TypeInfo, IdtyId> MembershipExternalStorage<BlockNumber, IdtyId> + for () +{ + fn insert(_: IdtyId, _: MembershipData<BlockNumber>) { + panic!("{}", INVALID_CONF_MSG) + } + fn get(_: &IdtyId) -> Option<MembershipData<BlockNumber>> { + panic!("{}", INVALID_CONF_MSG) + } + fn remove(_: &IdtyId) { + panic!("{}", INVALID_CONF_MSG) + } +} diff --git a/pallets/membership/src/types.rs b/pallets/membership/src/types.rs new file mode 100644 index 0000000000000000000000000000000000000000..5eac495064d28f0bfd3c7bacd9f169d3dfef0590 --- /dev/null +++ b/pallets/membership/src/types.rs @@ -0,0 +1,58 @@ +// 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 membership pallet. + +use codec::{Decode, Encode}; +use frame_support::RuntimeDebug; +use scale_info::TypeInfo; +#[cfg(feature = "std")] +use serde::{Deserialize, Serialize}; + +pub enum Event<IdtyId> { + /// A membership has acquired + /// [idty_id] + MembershipAcquired(IdtyId), + /// A membership has expired + /// [idty_id] + MembershipExpired(IdtyId), + /// A membership has renewed + /// [idty_id] + MembershipRenewed(IdtyId), + /// An identity requested membership + /// [idty_id] + MembershipRequested(IdtyId), + /// A membership has revoked + /// [idty_id] + MembershipRevoked(IdtyId), + /// A pending membership request has expired + /// [idty_id] + PendingMembershipExpired(IdtyId), +} + +#[derive(PartialEq)] +pub enum OriginPermission { + Allowed, + Forbidden, + Root, +} + +#[cfg_attr(feature = "std", derive(Deserialize, Serialize))] +#[derive(Encode, Decode, Default, Clone, Copy, PartialEq, Eq, RuntimeDebug, TypeInfo)] +pub struct MembershipData<BlockNumber: Decode + Encode + TypeInfo> { + pub expire_on: BlockNumber, + pub renewable_on: BlockNumber, +} diff --git a/pallets/ud-accounts-storage/Cargo.toml b/pallets/ud-accounts-storage/Cargo.toml index 2d337b8b4dbad9907d441647e4ebf93f811e7a01..4bf8ce9422944b09dd4fe73febd58f192434edd4 100644 --- a/pallets/ud-accounts-storage/Cargo.toml +++ b/pallets/ud-accounts-storage/Cargo.toml @@ -6,7 +6,7 @@ homepage = 'https://substrate.dev' license = 'AGPL-3.0' name = 'pallet-ud-accounts-storage' readme = 'README.md' -repository = 'https://git.duniter.org/nodes/rust/duniter-substrate' +repository = 'https://git.duniter.org/nodes/rust/duniter-v2s' version = '3.0.0' [features] diff --git a/pallets/universal-dividend/Cargo.toml b/pallets/universal-dividend/Cargo.toml index 59e4e399a01e60d76ab9e94d469b374a3e57a013..9fc29a57ee9da3e9cd210f739bbeb44718950907 100644 --- a/pallets/universal-dividend/Cargo.toml +++ b/pallets/universal-dividend/Cargo.toml @@ -5,7 +5,7 @@ edition = '2018' homepage = 'https://substrate.dev' license = 'AGPL-3.0' name = 'pallet-universal-dividend' -repository = 'https://git.duniter.org/nodes/rust/duniter-substrate' +repository = 'https://git.duniter.org/nodes/rust/duniter-v2s' version = '3.0.0' [features] diff --git a/runtime/g1/Cargo.toml b/runtime/g1/Cargo.toml index 8750b74124d5f95ee8b2cbeacaba524b9965c461..6046607d31e72ac7cbffde8a15cea9e3a2a5825a 100644 --- a/runtime/g1/Cargo.toml +++ b/runtime/g1/Cargo.toml @@ -8,7 +8,7 @@ edition = '2018' homepage = 'https://substrate.dev' license = 'AGPL-3.0' name = 'g1-runtime' -repository = 'https://git.duniter.org/nodes/rust/duniter-substrate' +repository = 'https://git.duniter.org/nodes/rust/duniter-v2s' version = '3.0.0' [package.metadata.docs.rs] targets = ['x86_64-unknown-linux-gnu'] diff --git a/runtime/gdev/Cargo.toml b/runtime/gdev/Cargo.toml index 59bae58902beb370da1d847598df4f952f7fdcdb..d8d6ce509e25a1364669b0fde09cb261251b51b3 100644 --- a/runtime/gdev/Cargo.toml +++ b/runtime/gdev/Cargo.toml @@ -8,7 +8,7 @@ edition = '2018' homepage = 'https://substrate.dev' license = 'AGPL-3.0' name = 'gdev-runtime' -repository = 'https://git.duniter.org/nodes/rust/duniter-substrate' +repository = 'https://git.duniter.org/nodes/rust/duniter-v2s' version = '3.0.0' [package.metadata.docs.rs] targets = ['x86_64-unknown-linux-gnu'] diff --git a/runtime/gtest/Cargo.toml b/runtime/gtest/Cargo.toml index c102d90fa2ad3669e0f86e6d957261acf102b9f1..707a56c0a790663c15b15782ca358b3ebb584b23 100644 --- a/runtime/gtest/Cargo.toml +++ b/runtime/gtest/Cargo.toml @@ -8,7 +8,7 @@ edition = '2018' homepage = 'https://substrate.dev' license = 'AGPL-3.0' name = 'gtest-runtime' -repository = 'https://git.duniter.org/nodes/rust/duniter-substrate' +repository = 'https://git.duniter.org/nodes/rust/duniter-v2s' version = '3.0.0' [package.metadata.docs.rs] targets = ['x86_64-unknown-linux-gnu']