diff --git a/pallets/distance/src/lib.rs b/pallets/distance/src/lib.rs index 0a13a751c08fc503f07c50da2dca6b399582f4f9..23277e89b1bda102ccab6ba2b94d2caac5c42d7b 100644 --- a/pallets/distance/src/lib.rs +++ b/pallets/distance/src/lib.rs @@ -17,7 +17,7 @@ #![cfg_attr(not(feature = "std"), no_std)] mod median; -mod traits; +pub mod traits; mod types; mod weights; @@ -66,6 +66,7 @@ pub mod pallet { + pallet_identity::Config<IdtyIndex = IdtyIndex> + pallet_session::Config { + /// Currency type used in this pallet (used for reserve/slash) type Currency: ReservableCurrency<Self::AccountId>; /// Amount reserved during evaluation #[pallet::constant] @@ -81,6 +82,8 @@ pub mod pallet { type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>; /// Type representing the weight of this pallet type WeightInfo: WeightInfo; + /// Handler for successful distance evaluation + type OnValidDistanceStatus: OnValidDistanceStatus<Self>; } // STORAGE // @@ -139,20 +142,6 @@ pub mod pallet { OptionQuery, >; - /// Identities by distance status expiration session index - #[pallet::storage] - #[pallet::getter(fn distance_status_expire_on)] - pub type DistanceStatusExpireOn<T: Config> = StorageMap< - _, - Twox64Concat, - u32, - BoundedVec< - <T as pallet_identity::Config>::IdtyIndex, - ConstU32<MAX_EVALUATIONS_PER_SESSION>, - >, - ValueQuery, - >; - /// Did evaluation get updated in this block? #[pallet::storage] pub(super) type DidUpdate<T: Config> = StorageValue<_, bool, ValueQuery>; @@ -224,7 +213,9 @@ pub mod pallet { #[pallet::call] impl<T: Config> Pallet<T> { - /// Request an identity to be evaluated + /// Request caller identity to be evaluated + /// positive evaluation will result in claim/renew membership + /// negative evaluation will result in slash for caller #[pallet::call_index(0)] #[pallet::weight(<T as pallet::Config>::WeightInfo::request_distance_evaluation())] pub fn request_distance_evaluation(origin: OriginFor<T>) -> DispatchResultWithPostInfo { @@ -233,6 +224,8 @@ pub mod pallet { let idty = pallet_identity::IdentityIndexOf::<T>::get(&who).ok_or(Error::<T>::NoIdentity)?; + // TODO is it necessary to check that the same account performed the request? + // TODO what if the distance status is existing but valid? ensure!( IdentityDistanceStatus::<T>::get(idty) != Some((who.clone(), DistanceStatus::Pending)), @@ -243,13 +236,49 @@ pub mod pallet { Ok(().into()) } + /// Request target identity to be evaluated + /// only possible for unconfirmed identity + #[pallet::call_index(4)] + #[pallet::weight(<T as pallet::Config>::WeightInfo::request_distance_evaluation())] + pub fn request_distance_evaluation_for( + origin: OriginFor<T>, + target: T::IdtyIndex, + ) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + + // check that the caller has an identity (TODO is this necessary ?) + // let _ = + // pallet_identity::IdentityIndexOf::<T>::get(&who).ok_or(Error::<T>::NoIdentity)?; + + // get the identity value of the target + let target_idty = + pallet_identity::Identities::<T>::get(target).ok_or(Error::<T>::NoIdentity)?; + + // check that target is unconfirmed + ensure!( + target_idty.status == pallet_identity::IdtyStatus::Unconfirmed, + Error::<T>::NoIdentity + ); + + // check that no distance status is already there + ensure!( + IdentityDistanceStatus::<T>::get(target).is_none(), + Error::<T>::AlreadyInEvaluation + ); + + Pallet::<T>::do_request_distance_evaluation(who, target)?; + Ok(().into()) + } + /// (Inherent) Push an evaluation result to the pool + /// this is called internally by validators (= inherent) #[pallet::call_index(1)] #[pallet::weight(<T as pallet::Config>::WeightInfo::update_evaluation(MAX_EVALUATIONS_PER_SESSION))] pub fn update_evaluation( origin: OriginFor<T>, computation_result: ComputationResult, ) -> DispatchResult { + // no origin = inherent ensure_none(origin)?; ensure!( !DidUpdate::<T>::exists(), @@ -263,7 +292,8 @@ pub mod pallet { Ok(()) } - /// Push an evaluation result to the pool + /// Force push an evaluation result to the pool + // (it is convenient to have this call in end2end tests) #[pallet::call_index(2)] #[pallet::weight(<T as pallet::Config>::WeightInfo::force_update_evaluation(MAX_EVALUATIONS_PER_SESSION))] pub fn force_update_evaluation( @@ -276,7 +306,8 @@ pub mod pallet { Pallet::<T>::do_update_evaluation(evaluator, computation_result) } - /// Set the distance evaluation status of an identity + /// Force set the distance evaluation status of an identity + // (it is convenient to have this in test network) /// /// Removes the status if `status` is `None`. /// @@ -292,15 +323,7 @@ pub mod pallet { ) -> DispatchResult { ensure_root(origin)?; - IdentityDistanceStatus::<T>::set(identity, status.clone()); - DistanceStatusExpireOn::<T>::mutate( - pallet_session::CurrentIndex::<T>::get() + T::ResultExpiration::get(), - move |identities| { - identities - .try_push(identity) - .map_err(|_| Error::<T>::TooManyEvaluationsInBlock) - }, - )?; + Self::do_set_distance_status(identity, status.clone()); Self::deposit_event(Event::EvaluationStatusForced { idty_index: identity, status, @@ -309,27 +332,6 @@ pub mod pallet { } } - // BENCHMARK FUNCTIONS // - - impl<T: Config> Pallet<T> { - /// Force the distance status using IdtyIndex and AccountId - /// only to prepare identity for benchmarking. - pub fn set_distance_status( - identity: <T as pallet_identity::Config>::IdtyIndex, - status: Option<(<T as frame_system::Config>::AccountId, DistanceStatus)>, - ) -> DispatchResult { - IdentityDistanceStatus::<T>::set(identity, status); - DistanceStatusExpireOn::<T>::mutate( - pallet_session::CurrentIndex::<T>::get() + T::ResultExpiration::get(), - move |identities| { - identities - .try_push(identity) - .map_err(|_| Error::<T>::TooManyEvaluationsInBlock.into()) - }, - ) - } - } - // INTERNAL FUNCTIONS // impl<T: Config> Pallet<T> { @@ -394,6 +396,7 @@ pub mod pallet { } } + /// request distance evaluation in current pool fn do_request_distance_evaluation( who: T::AccountId, idty_index: <T as pallet_identity::Config>::IdtyIndex, @@ -418,31 +421,31 @@ pub mod pallet { (&who, DistanceStatus::Pending), ); - DistanceStatusExpireOn::<T>::mutate( - pallet_session::CurrentIndex::<T>::get() + T::ResultExpiration::get(), - move |identities| identities.try_push(idty_index).ok(), - ); Self::deposit_event(Event::EvaluationRequested { idty_index, who }); Ok(()) }, ) } + /// update distance evaluation in next pool fn do_update_evaluation( evaluator: <T as frame_system::Config>::AccountId, computation_result: ComputationResult, ) -> DispatchResult { Pallet::<T>::mutate_next_pool(pallet_session::CurrentIndex::<T>::get(), |result_pool| { + // evaluation must be provided for all identities (no more, no less) ensure!( computation_result.distances.len() == result_pool.evaluations.len(), Error::<T>::WrongResultLength ); + // insert the evaluator if not already there if result_pool .evaluators .try_insert(evaluator.clone()) .map_err(|_| Error::<T>::TooManyEvaluators)? { + // update the median accumulator with the new result for (distance_value, (_identity, median_acc)) in computation_result .distances .into_iter() @@ -454,19 +457,28 @@ pub mod pallet { Self::deposit_event(Event::EvaluationUpdated { evaluator }); Ok(()) } else { + // one author can only submit one evaluation Err(Error::<T>::TooManyEvaluationsByAuthor.into()) } }) } + + /// Set the distance status using IdtyIndex and AccountId + pub fn do_set_distance_status( + identity: <T as pallet_identity::Config>::IdtyIndex, + status: Option<(<T as frame_system::Config>::AccountId, DistanceStatus)>, + ) { + IdentityDistanceStatus::<T>::set(identity, status.clone()); + if let Some((_, DistanceStatus::Valid)) = status { + T::OnValidDistanceStatus::on_valid_distance_status(identity); + } + } } impl<T: Config> pallet_authority_members::OnNewSession for Pallet<T> { fn on_new_session(index: SessionIndex) { EvaluationBlock::<T>::set(frame_system::Pallet::<T>::parent_hash()); - // Make results expire - DistanceStatusExpireOn::<T>::remove(index); - // Apply the results from the current pool (which was previous session's result pool) // We take the results so the pool is left empty for the new session. #[allow(clippy::type_complexity)] diff --git a/pallets/distance/src/mock.rs b/pallets/distance/src/mock.rs index a05d58577b4ff3e72e8cfa9f150222e6518793cc..4c227159d72d5935659bd007fd190763c060a8ee 100644 --- a/pallets/distance/src/mock.rs +++ b/pallets/distance/src/mock.rs @@ -257,6 +257,7 @@ impl pallet_distance::Config for Test { type ResultExpiration = frame_support::traits::ConstU32<720>; type RuntimeEvent = RuntimeEvent; type WeightInfo = (); + type OnValidDistanceStatus = (); } // Build genesis storage according to the mock runtime. diff --git a/pallets/distance/src/traits.rs b/pallets/distance/src/traits.rs index de9d532d64f4ce64a16fd20fab28455523f18e69..eafb5451c1136bb3a1709d5370a88211fdf07fdb 100644 --- a/pallets/distance/src/traits.rs +++ b/pallets/distance/src/traits.rs @@ -14,8 +14,13 @@ // 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/>. -pub trait HandleNegativeEvaluation<T: crate::Config> { - /// Do something with the reserved amount on the account, - /// when distance evaluation result is negative. - fn handle_negative_evaluation(account_id: T::AccountId); +use crate::*; + +pub trait OnValidDistanceStatus<T: Config> { + /// Handler for valid distance evaluation + fn on_valid_distance_status(idty_index: T::IdtyIndex); +} + +impl<T: Config> OnValidDistanceStatus<T> for () { + fn on_valid_distance_status(_idty_index: T::IdtyIndex) {} } diff --git a/pallets/duniter-wot/src/lib.rs b/pallets/duniter-wot/src/lib.rs index 41ad8db138320591f987b6a4c00870164d03bd62..04a02e808c373dc24b1c407abac3dc15da46297a 100644 --- a/pallets/duniter-wot/src/lib.rs +++ b/pallets/duniter-wot/src/lib.rs @@ -356,3 +356,28 @@ impl<T: Config<I>, I: 'static> pallet_certification::traits::OnRemovedCert<IdtyI } } } + +/// valid distance status handler +impl<T: Config<I> + pallet_distance::Config, I: 'static> + pallet_distance::traits::OnValidDistanceStatus<T> for Pallet<T, I> +{ + fn on_valid_distance_status(idty_index: IdtyIndex) { + if let Some(identity) = pallet_identity::Identities::<T>::get(idty_index) { + match identity.status { + IdtyStatus::Unconfirmed => { /* should not happen */ } + IdtyStatus::Unvalidated | IdtyStatus::NotMember => { + // ok to fail + let _ = pallet_membership::Pallet::<T, I>::try_claim_membership(idty_index); + } + IdtyStatus::Member => { + // ok to fail + let _ = pallet_membership::Pallet::<T, I>::try_renew_membership(idty_index); + } + IdtyStatus::Revoked => { /* should not happen */ } + } + } else { + // identity was removed before distance status was found + // so it's ok to do nothing + } + } +} diff --git a/pallets/membership/src/lib.rs b/pallets/membership/src/lib.rs index 08bbb81ad94295f46691fde78233a633b42ad687..435d6b42d06e5db9d4134131d79d83b184d71ee4 100644 --- a/pallets/membership/src/lib.rs +++ b/pallets/membership/src/lib.rs @@ -203,8 +203,7 @@ pub mod pallet { // get identity let idty_id = Self::get_idty_id(origin)?; - Self::check_allowed_to_claim(idty_id)?; - Self::do_add_membership(idty_id); + Self::try_claim_membership(idty_id)?; Ok(().into()) } @@ -214,16 +213,8 @@ pub mod pallet { pub fn renew_membership(origin: OriginFor<T>) -> DispatchResultWithPostInfo { // Verify phase let idty_id = Self::get_idty_id(origin)?; - let membership_data = - Membership::<T, I>::get(idty_id).ok_or(Error::<T, I>::MembershipNotFound)?; - - T::CheckMembershipCallAllowed::check_idty_allowed_to_renew_membership(&idty_id)?; - - // apply phase - Self::unschedule_membership_expiry(idty_id, membership_data.expire_on); - Self::insert_membership_and_schedule_expiry(idty_id); - T::OnEvent::on_event(&sp_membership::Event::MembershipRenewed(idty_id)); + Self::try_renew_membership(idty_id)?; Ok(().into()) } @@ -274,12 +265,48 @@ pub mod pallet { Ok(()) } + /// check that membership can be renewed + pub fn check_allowed_to_renew( + idty_id: T::IdtyId, + ) -> Result<MembershipData<T::BlockNumber>, DispatchError> { + let membership_data = + Membership::<T, I>::get(idty_id).ok_or(Error::<T, I>::MembershipNotFound)?; + + // enough certifications and distance rule for example + T::CheckMembershipCallAllowed::check_idty_allowed_to_renew_membership(&idty_id)?; + Ok(membership_data) + } + + /// try claim membership + pub fn try_claim_membership(idty_id: T::IdtyId) -> Result<(), DispatchError> { + Self::check_allowed_to_claim(idty_id)?; + Self::do_add_membership(idty_id); + Ok(()) + } + + /// try renew membership + pub fn try_renew_membership(idty_id: T::IdtyId) -> Result<(), DispatchError> { + let membership_data = Self::check_allowed_to_renew(idty_id)?; + Self::do_renew_membership(idty_id, membership_data); + Ok(()) + } + /// perform membership addition fn do_add_membership(idty_id: T::IdtyId) { Self::insert_membership_and_schedule_expiry(idty_id); T::OnEvent::on_event(&sp_membership::Event::MembershipAdded(idty_id)); } + /// perform membership renewal + fn do_renew_membership( + idty_id: T::IdtyId, + membership_data: MembershipData<T::BlockNumber>, + ) { + Self::unschedule_membership_expiry(idty_id, membership_data.expire_on); + Self::insert_membership_and_schedule_expiry(idty_id); + T::OnEvent::on_event(&sp_membership::Event::MembershipRenewed(idty_id)); + } + /// perform membership removal pub fn do_remove_membership(idty_id: T::IdtyId, reason: MembershipRemovalReason) { if let Some(membership_data) = Membership::<T, I>::take(idty_id) { diff --git a/runtime/common/src/pallets_config.rs b/runtime/common/src/pallets_config.rs index a9eedceef109638c2d9ec9ecb73807990abcd15e..9de85321c0291f15f7063cc79fde44a34980d5ae 100644 --- a/runtime/common/src/pallets_config.rs +++ b/runtime/common/src/pallets_config.rs @@ -522,6 +522,7 @@ macro_rules! pallets_config { type ResultExpiration = frame_support::traits::ConstU32<720>; type RuntimeEvent = RuntimeEvent; type WeightInfo = common_runtime::weights::pallet_distance::WeightInfo<Runtime>; + type OnValidDistanceStatus = Wot; } // SMITHS SUB-WOT //