Skip to content
Snippets Groups Projects
lib.rs 22.02 KiB
// Copyright 2021-2023 Axiom-Team
//
// This file is part of Duniter-v2S.
//
// Duniter-v2S 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.
//
// Duniter-v2S 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 Duniter-v2S. If not, see <https://www.gnu.org/licenses/>.

#![cfg_attr(not(feature = "std"), no_std)]

#[cfg(test)]
mod mock;

#[cfg(test)]
mod tests;

mod impls;
pub mod traits;
mod types;
pub mod weights;

#[cfg(feature = "runtime-benchmarks")]
mod benchmarking;

use codec::{Codec, Decode, Encode};
use frame_support::dispatch::DispatchResultWithPostInfo;
use frame_support::ensure;
use frame_support::pallet_prelude::Get;
use frame_support::pallet_prelude::RuntimeDebug;
use frame_support::pallet_prelude::Weight;
use frame_system::ensure_signed;
use frame_system::pallet_prelude::OriginFor;
use scale_info::TypeInfo;
use sp_runtime::traits::AtLeast32BitUnsigned;
use sp_runtime::traits::IsMember;
use sp_std::fmt::Debug;
use sp_std::prelude::*;

use crate::traits::OnSmithDelete;
pub use crate::weights::WeightInfo;
pub use pallet::*;
use pallet_authority_members::SessionIndex;
pub use types::*;

#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo)]
pub enum SmithRemovalReason {
    LostMembership,
    OfflineTooLong,
    Blacklisted,
}

#[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,
    /// The identity has accepted to eventually become a smith
    Pending,
    /// The identity has reached the requirements to become a smith and can now goGoOnline() or invite/certify other smiths
    Smith,
    /// The identity has been removed from the smiths set but is kept to keep track of its certifications
    Excluded,
}

#[frame_support::pallet]
pub mod pallet {
    use super::*;
    use frame_support::pallet_prelude::*;
    use frame_support::traits::StorageVersion;
    use pallet_authority_members::SessionIndex;
    use sp_runtime::traits::{Convert, IsMember};
    use sp_std::collections::btree_map::BTreeMap;
    use sp_std::vec;
    use sp_std::vec::Vec;

    const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);

    #[pallet::pallet]
    #[pallet::storage_version(STORAGE_VERSION)]
    #[pallet::without_storage_info]
    pub struct Pallet<T>(_);

    /// 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.
        type IdtyIndex: Parameter
            + Member
            + AtLeast32BitUnsigned
            + Codec
            + Default
            + Copy
            + MaybeSerializeDeserialize
            + Debug
            + MaxEncodedLen;
        /// Identifier for an authority-member
        type MemberId: Copy + Ord + MaybeSerializeDeserialize + Parameter;
        /// Something that gives the IdtyId of an AccountId
        type IdtyIdOf: Convert<Self::AccountId, Option<Self::IdtyIndex>>;
        /// Something that give the owner key of an identity
        type OwnerKeyOf: Convert<Self::IdtyIndex, Option<Self::AccountId>>;
        /// Something that gives the IdtyId of an AccountId
        type IdtyIdOfAuthorityId: Convert<Self::MemberId, Option<Self::IdtyIndex>>;
        /// Maximum number of active certifications by issuer
        #[pallet::constant]
        type MaxByIssuer: Get<u32>;
        /// 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 SmithInactivityMaxDuration: Get<u32>;
        /// Type representing the weight of this pallet
        type WeightInfo: WeightInfo;
    }

    /// Events type.
    #[pallet::event]
    #[pallet::generate_deposit(pub(super) fn deposit_event)]
    pub enum Event<T: Config> {
        /// An identity is being inivited to become a smith.
        InvitationSent {
            issuer: T::IdtyIndex,
            receiver: T::IdtyIndex,
        },
        /// The invitation has been accepted.
        InvitationAccepted { idty_index: T::IdtyIndex },
        /// Certification received
        SmithCertAdded {
            issuer: T::IdtyIndex,
            receiver: T::IdtyIndex,
        },
        /// Certification lost
        SmithCertRemoved {
            issuer: T::IdtyIndex,
            receiver: T::IdtyIndex,
        },
        /// A smith gathered enough certifications to become an authority (can call `go_online()`).
        SmithMembershipAdded { idty_index: T::IdtyIndex },
        /// A smith has been removed from the smiths set.
        SmithMembershipRemoved { idty_index: T::IdtyIndex },
    }

    #[pallet::genesis_config]
    pub struct GenesisConfig<T: Config> {
        pub initial_smiths: BTreeMap<T::IdtyIndex, (bool, Vec<T::IdtyIndex>)>,
    }

    impl<T: Config> Default for GenesisConfig<T> {
        fn default() -> Self {
            Self {
                initial_smiths: Default::default(),
            }
        }
    }

    #[pallet::genesis_build]
    impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
        fn build(&self) {
            CurrentSession::<T>::put(0);
            let mut cert_meta_by_issuer = BTreeMap::<T::IdtyIndex, Vec<T::IdtyIndex>>::new();
            for (receiver, (is_online, issuers)) in &self.initial_smiths {
                // Forbid self-cert
                assert!(
                    !issuers.contains(receiver),
                    "Identity cannot certify it-self."
                );

                let mut issuers_: Vec<_> = Vec::with_capacity(issuers.len());
                for issuer in issuers {
                    // Count issued certs
                    cert_meta_by_issuer
                        .entry(*issuer)
                        .or_insert(vec![])
                        .push(*receiver);
                    issuers_.push(*issuer);
                }

                // Write CertsByReceiver
                issuers_.sort();
                let issuers_count = issuers_.len();
                let smith_status = if issuers_count >= T::MinCertForMembership::get() as usize {
                    SmithStatus::Smith
                } else {
                    SmithStatus::Pending
                };
                Smiths::<T>::insert(
                    receiver,
                    SmithMeta {
                        status: smith_status,
                        expires_on: if *is_online {
                            None
                        } else {
                            Some(CurrentSession::<T>::get() + T::SmithInactivityMaxDuration::get())
                        },
                        issued_certs: vec![],
                        received_certs: issuers_,
                    },
                );
                ExpiresOn::<T>::append(
                    CurrentSession::<T>::get() + T::SmithInactivityMaxDuration::get(),
                    receiver,
                );
            }

            for (issuer, issued_certs) in cert_meta_by_issuer {
                // Write CertsByIssuer
                Smiths::<T>::mutate(issuer, |maybe_smith_meta| {
                    if let Some(smith_meta) = maybe_smith_meta {
                        smith_meta.issued_certs = issued_certs;
                    }
                });
            }
        }
    }

    /// maps identity index to smith status
    #[pallet::storage]
    #[pallet::getter(fn smiths)]
    pub type Smiths<T: Config> =
        StorageMap<_, Twox64Concat, T::IdtyIndex, SmithMeta<T::IdtyIndex>, OptionQuery>;

    /// maps session index to possible smith removals
    #[pallet::storage]
    #[pallet::getter(fn expires_on)]
    pub type ExpiresOn<T: Config> =
        StorageMap<_, Twox64Concat, SessionIndex, Vec<T::IdtyIndex>, OptionQuery>;

    /// stores the current session index
    #[pallet::storage]
    #[pallet::getter(fn current_session)]
    pub type CurrentSession<T: Config> = StorageValue<_, SessionIndex, ValueQuery>;

    // ERRORS //

    #[pallet::error]
    pub enum Error<T> {
        /// Issuer of anything (invitation, acceptance, certification) must have an identity ID
        OriginMustHaveAnIdentity,
        /// Issuer must be known as a potential smith
        OriginHasNeverBeenInvited,
        /// Invitation is reseverd to smiths
        InvitationIsASmithPrivilege,
        /// Invitation is reseverd to online smiths
        InvitationIsAOnlineSmithPrivilege,
        /// Invitation must not have been accepted yet
        InvitationAlreadyAccepted,
        /// Invitation of an already known smith is forbidden except if it has been excluded
        InvitationOfExistingNonExcluded,
        /// Invitation of a non-member (of the WoT) is forbidden
        InvitationOfNonMember,
        /// Certification cannot be made on someone who has not accepted an invitation
        CertificationMustBeAgreed,
        /// Certification cannot be made on excluded
        CertificationOnExcludedIsForbidden,
        /// Issuer must be a smith
        CertificationIsASmithPrivilege,
        /// Only online smiths can certify
        CertificationIsAOnlineSmithPrivilege,
        /// Smith cannot certify itself
        CertificationOfSelfIsForbidden,
        /// Receiver must be invited by another smith
        CertificationReceiverMustHaveBeenInvited,
        /// Receiver must not already have this certification
        CertificationAlreadyExists,
        /// A smith has a limited stock of certifications
        CertificationStockFullyConsumed,
    }

    #[pallet::call]
    impl<T: Config> Pallet<T> {
        /// Invite a WoT member to try becoming a Smith
        #[pallet::call_index(0)]
        #[pallet::weight(T::WeightInfo::invite_smith())]
        pub fn invite_smith(
            origin: OriginFor<T>,
            receiver: T::IdtyIndex,
        ) -> DispatchResultWithPostInfo {
            let who = ensure_signed(origin.clone())?;
            let issuer =
                T::IdtyIdOf::convert(who.clone()).ok_or(Error::<T>::OriginMustHaveAnIdentity)?;
            Self::check_invite_smith(issuer, receiver)?;
            Self::do_invite_smith(issuer, receiver);
            Ok(().into())
        }

        /// Accept an invitation (must have been invited first)
        #[pallet::call_index(1)]
        #[pallet::weight(T::WeightInfo::accept_invitation())]
        pub fn accept_invitation(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
            let who = ensure_signed(origin.clone())?;
            let receiver =
                T::IdtyIdOf::convert(who.clone()).ok_or(Error::<T>::OriginMustHaveAnIdentity)?;
            Self::check_accept_invitation(receiver)?;
            Self::do_accept_invitation(receiver)?;
            Ok(().into())
        }

        /// Certify an invited smith which can lead the certified to become a Smith
        #[pallet::call_index(2)]
        #[pallet::weight(T::WeightInfo::certify_smith())]
        pub fn certify_smith(
            origin: OriginFor<T>,
            receiver: T::IdtyIndex,
        ) -> DispatchResultWithPostInfo {
            let who = ensure_signed(origin)?;
            let issuer =
                T::IdtyIdOf::convert(who.clone()).ok_or(Error::<T>::OriginMustHaveAnIdentity)?;
            Self::check_certify_smith(issuer, receiver)?;
            Self::do_certify_smith(receiver, issuer);
            Ok(().into())
        }
    }
}

impl<T: Config> Pallet<T> {
    fn check_invite_smith(
        issuer: T::IdtyIndex,
        receiver: T::IdtyIndex,
    ) -> DispatchResultWithPostInfo {
        let issuer = Smiths::<T>::get(issuer).ok_or(Error::<T>::OriginHasNeverBeenInvited)?;
        ensure!(
            issuer.status == SmithStatus::Smith,
            Error::<T>::InvitationIsASmithPrivilege
        );
        ensure!(
            issuer.expires_on.is_none(),
            Error::<T>::InvitationIsAOnlineSmithPrivilege
        );
        if let Some(receiver_meta) = Smiths::<T>::get(receiver) {
            ensure!(
                receiver_meta.status == SmithStatus::Excluded,
                Error::<T>::InvitationOfExistingNonExcluded
            );
        }
        ensure!(
            T::IsWoTMember::is_member(&receiver),
            Error::<T>::InvitationOfNonMember
        );

        Ok(().into())
    }

    fn do_invite_smith(issuer: T::IdtyIndex, receiver: T::IdtyIndex) {
        let new_expires_on = CurrentSession::<T>::get() + T::SmithInactivityMaxDuration::get();
        let mut existing = Smiths::<T>::get(receiver).unwrap_or_default();
        existing.status = SmithStatus::Invited;
        existing.expires_on = Some(new_expires_on);
        existing.received_certs = vec![];
        Smiths::<T>::insert(receiver, existing);
        ExpiresOn::<T>::append(new_expires_on, receiver);
        Self::deposit_event(Event::<T>::InvitationSent { issuer, receiver });
    }

    fn check_accept_invitation(receiver: T::IdtyIndex) -> DispatchResultWithPostInfo {
        let pretender_status = Smiths::<T>::get(receiver)
            .ok_or(Error::<T>::OriginHasNeverBeenInvited)?
            .status;
        ensure!(
            pretender_status == SmithStatus::Invited,
            Error::<T>::InvitationAlreadyAccepted
        );
        Ok(().into())
    }

    fn do_accept_invitation(receiver: T::IdtyIndex) -> DispatchResultWithPostInfo {
        Smiths::<T>::mutate(receiver, |maybe_smith_meta| {
            if let Some(smith_meta) = maybe_smith_meta {
                smith_meta.status = SmithStatus::Pending;
            }
        });
        Self::deposit_event(Event::<T>::InvitationAccepted {
            idty_index: receiver,
        });
        Ok(().into())
    }

    fn check_certify_smith(
        issuer_index: T::IdtyIndex,
        receiver_index: T::IdtyIndex,
    ) -> DispatchResultWithPostInfo {
        ensure!(
            issuer_index != receiver_index,
            Error::<T>::CertificationOfSelfIsForbidden
        );
        let issuer = Smiths::<T>::get(issuer_index).ok_or(Error::<T>::OriginHasNeverBeenInvited)?;
        ensure!(
            issuer.status == SmithStatus::Smith,
            Error::<T>::CertificationIsASmithPrivilege
        );
        ensure!(
            issuer.expires_on.is_none(),
            Error::<T>::CertificationIsAOnlineSmithPrivilege
        );
        let issued_certs = issuer.issued_certs.len();
        ensure!(
            issued_certs < T::MaxByIssuer::get() as usize,
            Error::<T>::CertificationStockFullyConsumed
        );
        let receiver = Smiths::<T>::get(receiver_index)
            .ok_or(Error::<T>::CertificationReceiverMustHaveBeenInvited)?;
        ensure!(
            receiver.status != SmithStatus::Invited,
            Error::<T>::CertificationMustBeAgreed
        );
        ensure!(
            receiver.status != SmithStatus::Excluded,
            Error::<T>::CertificationOnExcludedIsForbidden
        );
        ensure!(
            receiver
                .received_certs
                .binary_search(&issuer_index)
                .is_err(),
            Error::<T>::CertificationAlreadyExists
        );

        Ok(().into())
    }

    fn do_certify_smith(receiver: T::IdtyIndex, issuer: T::IdtyIndex) {
        // - adds a certification in issuer issued list
        Smiths::<T>::mutate(issuer, |maybe_smith_meta| {
            if let Some(smith_meta) = maybe_smith_meta {
                smith_meta.issued_certs.push(receiver);
                smith_meta.issued_certs.sort();
            }
        });
        Smiths::<T>::mutate(receiver, |maybe_smith_meta| {
            if let Some(smith_meta) = maybe_smith_meta {
                // - adds a certification in receiver received list
                smith_meta.received_certs.push(issuer);
                smith_meta.received_certs.sort();
                Self::deposit_event(Event::<T>::SmithCertAdded { issuer, receiver });

                // - receiving a certification either lead us to Pending or Smith status
                let previous_status = smith_meta.status;
                smith_meta.status =
                    if smith_meta.received_certs.len() >= T::MinCertForMembership::get() as usize {
                        // - if the number of certification received by the receiver is enough, win the Smith status (or keep it)
                        SmithStatus::Smith
                    } else {
                        // - otherwise we are (still) a pending smith
                        SmithStatus::Pending
                    };

                if previous_status != SmithStatus::Smith {
                    // - postpone the expiration: a Pending smith cannot do anything but wait
                    // this postponement is here to ease the process of becoming a smith
                    let new_expires_on =
                        CurrentSession::<T>::get() + T::SmithInactivityMaxDuration::get();
                    smith_meta.expires_on = Some(new_expires_on);
                    ExpiresOn::<T>::append(new_expires_on, receiver);
                }

                // - if the status is smith but wasn't, notify that smith gained membership
                if smith_meta.status == SmithStatus::Smith && previous_status != SmithStatus::Smith
                {
                    Self::deposit_event(Event::<T>::SmithMembershipAdded {
                        idty_index: receiver,
                    });
                }
                // TODO: (optimization) unschedule old expiry
            }
        });
    }

    fn on_exclude_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 {
                            Self::_do_exclude_smith(smith, SmithRemovalReason::OfflineTooLong);
                        }
                    }
                }
            }
        }
    }

    pub fn on_removed_wot_member(idty_index: T::IdtyIndex) -> Weight {
        let mut weight = T::WeightInfo::on_removed_wot_member_empty();
        if Smiths::<T>::get(idty_index).is_some() {
            Self::_do_exclude_smith(idty_index, SmithRemovalReason::LostMembership);
            weight = weight.saturating_add(T::WeightInfo::on_removed_wot_member());
        }
        weight
    }

    fn _do_exclude_smith(receiver: T::IdtyIndex, reason: SmithRemovalReason) {
        let mut lost_certs = vec![];
        Smiths::<T>::mutate(receiver, |maybe_smith_meta| {
            if let Some(smith_meta) = maybe_smith_meta {
                smith_meta.expires_on = None;
                smith_meta.status = SmithStatus::Excluded;
                for cert in &smith_meta.received_certs {
                    lost_certs.push(*cert);
                }
                smith_meta.received_certs = vec![];
                // N.B.: the issued certs are kept in case the smith joins back
            }
        });
        // We remove the lost certs from their issuer's stock
        for lost_cert_issuer in lost_certs {
            Smiths::<T>::mutate(lost_cert_issuer, |maybe_smith_meta| {
                if let Some(smith_meta) = maybe_smith_meta {
                    if let Ok(index) = smith_meta.issued_certs.binary_search(&receiver) {
                        smith_meta.issued_certs.remove(index);
                        Self::deposit_event(Event::<T>::SmithCertRemoved {
                            issuer: lost_cert_issuer,
                            receiver,
                        });
                    }
                }
            });
        }
        // Deletion done: notify (authority-members) for cascading
        T::OnSmithDelete::on_smith_delete(receiver, reason);
        Self::deposit_event(Event::<T>::SmithMembershipRemoved {
            idty_index: receiver,
        });
    }

    pub fn on_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| {
                    if let Some(smith_meta) = maybe_smith_meta {
                        // As long as the smith is online, it cannot expire
                        smith_meta.expires_on = None;
                        // FIXME: unschedule old expiry
                    }
                });
            }
        }
    }

    pub fn on_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| {
                    if let Some(smith_meta) = maybe_smith_meta {
                        // As long as the smith is online, it cannot expire
                        let new_expires_on =
                            CurrentSession::<T>::get() + T::SmithInactivityMaxDuration::get();
                        smith_meta.expires_on = Some(new_expires_on);
                        ExpiresOn::<T>::append(new_expires_on, idty_index);
                    }
                });
            }
        }
    }

    fn provide_is_member(idty_id: &T::IdtyIndex) -> bool {
        let Some(smith) = Smiths::<T>::get(idty_id) else {
            return false;
        };
        smith.status == SmithStatus::Smith
    }
}

impl<T: Config> sp_runtime::traits::IsMember<T::IdtyIndex> for Pallet<T> {
    fn is_member(idty_id: &T::IdtyIndex) -> bool {
        Self::provide_is_member(idty_id)
    }
}