// This file is part of Bizinikiwi. // Copyright (C) Parity Technologies (UK) Ltd. // SPDX-License-Identifier: Apache-2.0 // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //! # People Pezpallet //! //! A pezpallet managing the registry of proven individuals. //! //! ## Overview //! //! The People pezpallet stores and manages identifiers of individuals who have proven their //! personhood. It tracks their personal IDs, organizes their cryptographic keys into rings, and //! allows them to use contextual aliases through authentication in extensions. When transactions //! include cryptographic proofs of belonging to the people set, the pezpallet's transaction //! extension verifies these proofs before allowing the transaction to proceed. This enables other //! pallets to check if actions come from unique persons while preserving privacy through the //! ring-based structure. //! //! The pezpallet accepts new persons after they prove their uniqueness elsewhere, stores their //! information, and supports removing persons via suspensions. While other systems (e.g., wallets) //! generate the proofs, this pezpallet handles the storage of all necessary data and verifies the //! proofs when used. //! //! ## Key Features //! //! - **Stores Identity Data**: Tracks personal IDs and cryptographic keys of proven persons //! - **Organizes Keys**: Groups keys into rings to enable privacy-preserving proofs //! - **Verifies Proofs**: Checks personhood proofs attached to transactions //! - **Links Accounts**: Allows connecting blockchain accounts to contextual aliases //! - **Manages Registry**: Adds proven persons and will support removing them //! //! ## Interface //! //! ### Dispatchable Functions //! //! - `set_alias_account(origin, account)`: Link an account to a contextual alias Once linked, this //! allows the account to dispatch transactions as a person with the alias origin using a regular //! signed transaction with a nonce, providing a simpler alternative to attaching full proofs. //! - `unset_alias_account(origin)`: Remove an account-alias link. //! - `merge_rings`: Merge the people in two rings into a single, new ring. //! - `force_recognize_personhood`: Recognize a set of people without any additional checks. //! - `set_personal_id_account`: Set a personal id account. //! - `unset_personal_id_account`: Unset the personal id account. //! - `migrate_included_key`: Migrate the key for a person who was onboarded and is currently //! included in a ring. //! - `migrate_onboarding_key`: Migrate the key for a person who is currently onboarding. The //! operation is instant, replacing the old key in the onboarding queue. //! - `set_onboarding_size`: Force set the onboarding size for new people. This call requires root //! privileges. //! - `build_ring_manual`: Manually build a ring root by including registered people. The //! transaction fee is refunded on a successful call. //! - `onboard_people_manual`: Manually onboard people into a ring. The transaction fee is refunded //! on a successful call. //! //! ### Automated tasks performed by the pezpallet in hooks //! //! - Ring building: Build or update a ring's cryptographic commitment. This task processes queued //! keys into a ring commitment that enables proof generation and verification. Since ring //! construction, or rather adding keys to the ring, is computationally expensive, it's performed //! periodically in batches rather than processing each key immediately. The batch size needs to //! be reasonably large to enhance privacy by obscuring the exact timing of when individuals' keys //! were added to the ring, making it more difficult to correlate specific persons with their //! keys. //! - People onboarding: Onboard people from the onboarding queue into a ring. This task takes the //! unincluded keys of recognized people from the onboarding queue and registers them into the //! ring. People can be onboarded only in batches of at least `OnboardingSize` and when the //! remaining open slots in a ring are at least `OnboardingSize`. This does not compute the root, //! that is done using `build_ring`. //! - Cleaning of suspended people: Remove people's keys marked as suspended or inactive from rings. //! The keys are stored in the `PendingSuspensions` map and they are removed from rings and their //! roots are reset. The ring roots will subsequently be build in the ring building phase from //! scratch. sequentially. //! - Key migration: Migrate the keys for people who were onboarded and are currently included in //! rings. The migration is not instant as the key replacement and subsequent inclusion in a new //! ring root will happen only after the next mutation session. //! - Onboarding queue page merging: Merge the two pages at the front of the onboarding queue. After //! a round of suspensions, it is possible for the second page of the onboarding queue to be left //! with few members such that, if the first page also has few members, the total count is below //! the required onboarding size, thus stalling the queue. This function fixes this by moving the //! people from the first page to the front of the second page, defragmenting the queue. //! //! ### Transaction Extension //! //! The pezpallet provides the `AsPerson` transaction extension that allows transactions to be //! dispatched with special origins: `PersonalIdentity` and `PersonalAlias`. These origins prove the //! transaction comes from a unique person, either through their identity or through a contextual //! alias. To make use of the personhood system, other pallets should check for these origins. //! //! The extension verifies the proof of personhood during transaction validation and, if valid, //! transforms the transaction's origin into one of these special origins. //! //! ## Usage //! //! Other pallets can verify personhood through origin checks: //! //! - `EnsurePersonalIdentity`: Verifies the origin represents a specific person using their //! PersonalId //! - `EnsurePersonalAlias`: Verifies the origin has a valid alias for any context //! - `EnsurePersonalAliasInContext`: Verifies the origin has a valid alias for a specific context #![cfg_attr(not(feature = "std"), no_std)] #![recursion_limit = "128"] #![allow(clippy::borrowed_box)] extern crate alloc; use alloc::{boxed::Box, vec::Vec}; #[cfg(test)] mod mock; #[cfg(test)] mod tests; #[cfg(feature = "runtime-benchmarks")] pub mod benchmarking; pub mod extension; pub mod types; pub mod weights; pub use pezpallet::*; pub use types::*; pub use weights::WeightInfo; use codec::{Decode, Encode, MaxEncodedLen}; use core::{ cmp::{self}, ops::Range, }; use pezframe_support::{ dispatch::{ extract_actual_weight, DispatchInfo, DispatchResultWithPostInfo, GetDispatchInfo, PostDispatchInfo, }, storage::with_storage_layer, traits::{ reality::{ AddOnlyPeopleTrait, Context, ContextualAlias, CountedMembers, PeopleTrait, PersonalId, RingIndex, }, Defensive, EnsureOriginWithArg, IsSubType, OriginTrait, }, transactional, weights::WeightMeter, }; use pezsp_runtime::{ traits::{BadOrigin, Dispatchable}, ArithmeticError, RuntimeDebug, SaturatedConversion, Saturating, }; use scale_info::TypeInfo; use verifiable::{Alias, GenerateVerifiable}; #[cfg(feature = "runtime-benchmarks")] pub use benchmarking::BenchmarkHelper; #[pezframe_support::pezpallet] pub mod pezpallet { use super::*; use pezframe_support::{pezpallet_prelude::*, traits::Contains}; use pezframe_system::pezpallet_prelude::{BlockNumberFor, *}; const LOG_TARGET: &str = "runtime::people"; #[pezpallet::pezpallet] pub struct Pezpallet(_); #[pezpallet::config] pub trait Config: pezframe_system::Config< RuntimeOrigin: From + From<::PalletsOrigin> + OriginTrait< PalletsOrigin: From + TryInto< Origin, Error = ::PalletsOrigin, >, >, RuntimeCall: Parameter + GetDispatchInfo + IsSubType> + Dispatchable< RuntimeOrigin = Self::RuntimeOrigin, Info = DispatchInfo, PostInfo = PostDispatchInfo, >, > { /// Weight information for extrinsics in this pezpallet. type WeightInfo: WeightInfo; /// The runtime event type. #[allow(deprecated)] type RuntimeEvent: From> + IsType<::RuntimeEvent>; /// Trait allowing cryptographic proof of membership without exposing the underlying member. /// Normally a Ring-VRF. type Crypto: GenerateVerifiable< Proof: Send + Sync + DecodeWithMemTracking, Signature: Send + Sync + DecodeWithMemTracking, Member: DecodeWithMemTracking, >; /// Contexts which may validly have an account alias behind it for everyone. type AccountContexts: Contains; /// Number of chunks per page. #[pezpallet::constant] type ChunkPageSize: Get; /// Maximum number of people included in a ring before a new one is created. #[pezpallet::constant] type MaxRingSize: Get; /// Maximum number of people included in an onboarding queue page before a new one is /// created. #[pezpallet::constant] type OnboardingQueuePageSize: Get; /// Helper for benchmarks. #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper: BenchmarkHelper<::StaticChunk>; } /// The current individuals we recognise. #[pezpallet::storage] pub type Root = StorageMap<_, Blake2_128Concat, RingIndex, RingRoot>; /// Keeps track of the ring index currently being populated. #[pezpallet::storage] pub type CurrentRingIndex = StorageValue<_, u32, ValueQuery>; /// Maximum number of people queued before onboarding to a ring. #[pezpallet::storage] pub type OnboardingSize = StorageValue<_, u32, ValueQuery>; /// Hint for the maximum number of people that can be included in a ring through a single root /// building call. If no value is set, then the onboarding size will be used instead. #[pezpallet::storage] pub type RingBuildingPeopleLimit = StorageValue<_, u32, OptionQuery>; /// Both the keys that are included in built rings /// and the keys that will be used in future rings. #[pezpallet::storage] pub type RingKeys = StorageMap< _, Blake2_128Concat, RingIndex, BoundedVec, T::MaxRingSize>, ValueQuery, >; /// Stores the meta information for each ring, the number of keys and how many are actually /// included in the root. #[pezpallet::storage] pub type RingKeysStatus = StorageMap<_, Blake2_128Concat, RingIndex, RingStatus, ValueQuery>; /// A map of all rings which currently have pending suspensions and need cleaning, along with /// their respective number of suspended keys which need to be removed. #[pezpallet::storage] pub type PendingSuspensions = StorageMap<_, Twox64Concat, RingIndex, BoundedVec, ValueQuery>; /// The number of people currently included in a ring. #[pezpallet::storage] pub type ActiveMembers = StorageValue<_, u32, ValueQuery>; /// The current individuals we recognise, but not necessarily yet included in a ring. /// /// Look-up from the crypto (public) key to the immutable ID of the individual (`PersonalId`). A /// person can have two different entries in this map if they queued a key migration which /// hasn't been enacted yet. #[pezpallet::storage] pub type Keys = CountedStorageMap<_, Blake2_128Concat, MemberOf, PersonalId>; /// A map of all the people who have declared their intent to migrate their keys and are waiting /// for the next mutation session. #[pezpallet::storage] pub type KeyMigrationQueue = StorageMap<_, Blake2_128Concat, PersonalId, MemberOf>; /// The current individuals we recognise, but not necessarily yet included in a ring. /// /// Immutable ID of the individual (`PersonalId`) to information about their key and status. #[pezpallet::storage] pub type People = StorageMap<_, Blake2_128Concat, PersonalId, PersonRecord, T::AccountId>>; /// Conversion of a contextual alias to an account ID. #[pezpallet::storage] pub type AliasToAccount = StorageMap< _, Blake2_128Concat, ContextualAlias, ::AccountId, OptionQuery, >; /// Conversion of an account ID to a contextual alias. #[pezpallet::storage] pub type AccountToAlias = StorageMap< _, Blake2_128Concat, ::AccountId, RevisedContextualAlias, OptionQuery, >; /// Association of an account ID to a personal ID. /// /// Managed with `set_personal_id_account` and `unset_personal_id_account`. /// Reverse lookup is inside `People` storage, inside the record. #[pezpallet::storage] pub type AccountToPersonalId = StorageMap< _, Blake2_128Concat, ::AccountId, PersonalId, OptionQuery, >; /// Paginated collection of static chunks used by the verifiable crypto. #[pezpallet::storage] pub type Chunks = StorageMap<_, Twox64Concat, PageIndex, ChunksOf, OptionQuery>; /// The next free and never reserved personal ID. #[pezpallet::storage] pub type NextPersonalId = StorageValue<_, PersonalId, ValueQuery>; /// The state of the pezpallet regarding the actions that are currently allowed to be performed /// on all existing rings. #[pezpallet::storage] pub type RingsState = StorageValue<_, RingMembersState, ValueQuery>; /// Candidates' reserved identities which we track. #[pezpallet::storage] pub type ReservedPersonalId = StorageMap<_, Twox64Concat, PersonalId, (), OptionQuery>; /// Keeps track of the page indices of the head and tail of the onboarding queue. #[pezpallet::storage] pub type QueuePageIndices = StorageValue<_, (PageIndex, PageIndex), ValueQuery>; /// Paginated collection of people public keys ready to be included in a ring. #[pezpallet::storage] pub type OnboardingQueue = StorageMap< _, Twox64Concat, PageIndex, BoundedVec, ::OnboardingQueuePageSize>, ValueQuery, >; #[pezpallet::event] #[pezpallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { /// An individual has had their personhood recognised and indexed. PersonhoodRecognized { who: PersonalId, key: MemberOf }, /// An individual has had their personhood recognised again and indexed. PersonOnboarding { who: PersonalId, key: MemberOf }, } #[pezpallet::extra_constants] impl Pezpallet { /// The amount of block number tolerance we allow for a setup account transaction. /// /// `set_alias_account` and `set_personal_id_account` calls contains /// `call_valid_at` as a parameter, those calls are valid if the block number is within /// the tolerance period. pub fn account_setup_time_tolerance() -> BlockNumberFor { 600u32.into() } } #[pezpallet::error] pub enum Error { /// The supplied identifier does not represent a person. NotPerson, /// The given person has no associated key. NoKey, /// The context is not a member of those allowed to have account aliases held. InvalidContext, /// The account is not known. InvalidAccount, /// The account is already in use under another alias. AccountInUse, /// The proof is invalid. InvalidProof, /// The signature is invalid. InvalidSignature, /// There are not yet any members of our personhood set. NoMembers, /// The root cannot be finalized as there are still unpushed members. Incomplete, /// The root is still fresh. StillFresh, /// Too many members have been pushed. TooManyMembers, /// Key already in use by another person. KeyAlreadyInUse, /// The old key was not found when expected. KeyNotFound, /// Could not push member into the ring. CouldNotPush, /// The record is already using this key. SameKey, /// Personal Id was not reserved. PersonalIdNotReserved, /// Personal Id has never been reserved. PersonalIdReservationCannotRenew, /// Personal Id was not reserved or not already recognized. PersonalIdNotReservedOrNotRecognized, /// Ring cannot be merged if it's the top ring. InvalidRing, /// Ring cannot be built while there are suspensions pending. SuspensionsPending, /// Ring cannot be merged if it's not below 1/2 capacity. RingAboveMergeThreshold, /// Suspension indices provided are invalid. InvalidSuspensions, /// An mutating action was queued when there was no mutation session in progress. NoMutationSession, /// An mutating session could not be started. CouldNotStartMutationSession, /// Cannot merge rings while a suspension session is in progress. SuspensionSessionInProgress, /// Call is too late or too early. TimeOutOfRange, /// Alias <-> Account is already set and up to date. AliasAccountAlreadySet, /// Personhood cannot be resumed if it is not suspended. NotSuspended, /// Personhood is suspended. Suspended, /// Invalid state for attempted key migration. InvalidKeyMigration, /// Invalid suspension of a key belonging to a person whose index in the ring has already /// been included in the pending suspensions list. KeyAlreadySuspended, /// The onboarding size must not exceed the maximum ring size. InvalidOnboardingSize, } #[pezpallet::origin] #[derive( Clone, PartialEq, Eq, RuntimeDebug, Encode, Decode, MaxEncodedLen, TypeInfo, DecodeWithMemTracking, )] pub enum Origin { PersonalIdentity(PersonalId), PersonalAlias(RevisedContextualAlias), } #[pezpallet::hooks] impl Hooks> for Pezpallet { fn integrity_test() { assert!( ::ChunkPageSize::get() > 0, "chunk page size must hold at least one element" ); assert!(::MaxRingSize::get() > 0, "rings must hold at least one person"); assert!( ::MaxRingSize::get() <= ::OnboardingQueuePageSize::get(), "onboarding queue page size must greater than or equal to max ring size" ); } fn on_poll(_: BlockNumberFor, weight_meter: &mut WeightMeter) { // Check if there are any keys to migrate. if weight_meter.try_consume(T::WeightInfo::on_poll_base()).is_err() { return; } if RingsState::::get().key_migration() { Self::migrate_keys(weight_meter); } // Check if there are any rings with suspensions and try to clean the first one. if let Some(ring_index) = PendingSuspensions::::iter_keys().next() { if Self::should_remove_suspended_keys(ring_index, true) && weight_meter.can_consume(T::WeightInfo::remove_suspended_people( T::MaxRingSize::get(), )) { let actual = Self::remove_suspended_keys(ring_index); weight_meter.consume(actual) } } let merge_weight = T::WeightInfo::merge_queue_pages(); if !weight_meter.can_consume(merge_weight) { return; } let merge_action = Self::should_merge_queue_pages(); if let QueueMergeAction::Merge { initial_head, new_head, first_key_page, second_key_page, } = merge_action { Self::merge_queue_pages(initial_head, new_head, first_key_page, second_key_page); weight_meter.consume(merge_weight); } } fn on_idle(_block: BlockNumberFor, limit: Weight) -> Weight { let mut weight_meter = WeightMeter::with_limit(limit.saturating_div(2)); let on_idle_weight = T::WeightInfo::on_idle_base(); if !weight_meter.can_consume(on_idle_weight) { return weight_meter.consumed(); } weight_meter.consume(on_idle_weight); let max_ring_size = T::MaxRingSize::get(); let remove_people_weight = T::WeightInfo::remove_suspended_people(max_ring_size); let rings_state = RingsState::::get(); // Check if there are any rings with suspensions and try to clean as many as possible. // First check the state of the rings allow for removals. if !rings_state.append_only() { return weight_meter.consumed(); } // Account for the first iteration of the loop. let suspension_step_weight = T::WeightInfo::pending_suspensions_iteration(); if !weight_meter.can_consume(suspension_step_weight) { return weight_meter.consumed(); } // Always renew the iterator because in each iteration we remove a key, which would make // the old iterator unstable. while let Some(ring_index) = PendingSuspensions::::iter_keys().next() { weight_meter.consume(suspension_step_weight); // Break the loop if we run out of weight. if !weight_meter.can_consume(remove_people_weight) { return weight_meter.consumed(); } if Self::should_remove_suspended_keys(ring_index, false) { let actual = Self::remove_suspended_keys(ring_index); weight_meter.consume(actual) } // Break the loop if we run out of weight. if !weight_meter.can_consume(suspension_step_weight) { return weight_meter.consumed(); } } // Ring state must be append only for both onboarding and ring building, but it is // already checked above. let onboard_people_weight = T::WeightInfo::onboard_people(); if !weight_meter.can_consume(onboard_people_weight) { return weight_meter.consumed(); } let op_res = with_storage_layer::<(), DispatchError, _>(|| Self::onboard_people()); weight_meter.consume(onboard_people_weight); if let Err(e) = op_res { log::debug!(target: LOG_TARGET, "failed to onboard people: {:?}", e); } let current_ring = CurrentRingIndex::::get(); let should_build_ring_weight = T::WeightInfo::should_build_ring(max_ring_size); let build_ring_weight = T::WeightInfo::build_ring(max_ring_size); for ring_index in (0..=current_ring).rev() { if !weight_meter.can_consume(should_build_ring_weight) { return weight_meter.consumed(); } let maybe_to_include = Self::should_build_ring(ring_index, max_ring_size); weight_meter.consume(should_build_ring_weight); let Some(to_include) = maybe_to_include else { continue }; if !weight_meter.can_consume(build_ring_weight) { return weight_meter.consumed(); } let op_res = with_storage_layer::<(), DispatchError, _>(|| { Self::build_ring(ring_index, to_include) }); weight_meter.consume(build_ring_weight); if let Err(e) = op_res { log::error!(target: LOG_TARGET, "failed to build ring: {:?}", e); } } weight_meter.consumed() } } #[pezpallet::genesis_config] pub struct GenesisConfig { pub encoded_chunks: Vec, #[serde(skip)] pub _phantom_data: core::marker::PhantomData, pub onboarding_size: u32, } impl Default for GenesisConfig { fn default() -> Self { // The default genesis config will put in the chunks that pertain to the ring vrf // implementation in the `verifiable` crate. This default config will not work for other // custom `GenerateVerifiable` implementations. use verifiable::ring_vrf_impl::StaticChunk; let params = verifiable::ring_vrf_impl::ring_verifier_builder_params(); let chunks: Vec = params.0.iter().map(|c| StaticChunk(*c)).collect(); Self { encoded_chunks: chunks.encode(), _phantom_data: PhantomData, onboarding_size: T::MaxRingSize::get(), } } } #[pezpallet::genesis_build] impl BuildGenesisConfig for GenesisConfig { fn build(&self) { let chunks: Vec<<::Crypto as GenerateVerifiable>::StaticChunk> = Decode::decode(&mut &(self.encoded_chunks.clone())[..]) .expect("couldn't decode chunks"); assert_eq!(chunks.len(), 1 << 9); let page_size = ::ChunkPageSize::get(); let mut page_idx = 0; let mut chunk_idx = 0; while chunk_idx < chunks.len() { let chunk_idx_end = cmp::min(chunk_idx + page_size as usize, chunks.len()); let chunk_page: ChunksOf = chunks[chunk_idx..chunk_idx_end] .to_vec() .try_into() .expect("page size was checked against the array length; qed"); Chunks::::insert(page_idx, chunk_page); page_idx += 1; chunk_idx = chunk_idx_end; } OnboardingSize::::set(self.onboarding_size); } } #[pezpallet::call(weight = ::WeightInfo)] impl Pezpallet { /// Build a ring root by including registered people. /// /// This task is performed automatically by the pezpallet through the `on_idle` hook /// whenever there is leftover weight in a block. This call is meant to be a backup in /// case of extreme congestion and should be submitted by signed origins. #[pezpallet::weight( T::WeightInfo::should_build_ring( limit.unwrap_or_else(T::MaxRingSize::get) ).saturating_add(T::WeightInfo::build_ring(limit.unwrap_or_else(T::MaxRingSize::get))))] #[pezpallet::call_index(100)] pub fn build_ring_manual( origin: OriginFor, ring_index: RingIndex, limit: Option, ) -> DispatchResultWithPostInfo { ensure_signed(origin)?; // Get the keys for this ring, and make sure that the ring is full before we build it. let (keys, mut ring_status) = Self::ring_keys_and_info(ring_index); let to_include = Self::should_build_ring(ring_index, limit.unwrap_or_else(T::MaxRingSize::get)) .ok_or(Error::::StillFresh)?; // Get the current ring, and check it should be rebuilt. // Return the next revision. let (next_revision, mut intermediate) = if let Some(existing_root) = Root::::get(ring_index) { // We should build a new ring. Return the new revision number we should use. ( existing_root.revision.checked_add(1).ok_or(ArithmeticError::Overflow)?, existing_root.intermediate, ) } else { // No ring has been built at this index, so we start at revision 0. (0, T::Crypto::start_members()) }; // Push the members. T::Crypto::push_members( &mut intermediate, keys.iter() .skip(ring_status.included as usize) .take(to_include as usize) .cloned(), Self::fetch_chunks, ) .map_err(|_| Error::::CouldNotPush)?; // By the end of the loop, we have included the maximum number of keys in the vector. ring_status.included = ring_status.included.saturating_add(to_include); RingKeysStatus::::insert(ring_index, ring_status); // We create the root after pushing all members. let root = T::Crypto::finish_members(intermediate.clone()); let ring_root = RingRoot { root, revision: next_revision, intermediate }; Root::::insert(ring_index, ring_root); Ok(Pays::No.into()) } /// Onboard people into a ring by taking their keys from the onboarding queue and /// registering them into the ring. This does not compute the root, that is done using /// `build_ring`. /// /// This task is performed automatically by the pezpallet through the `on_idle` hook /// whenever there is leftover weight in a block. This call is meant to be a backup in /// case of extreme congestion and should be submitted by signed origins. #[pezpallet::weight(T::WeightInfo::onboard_people())] #[pezpallet::call_index(101)] pub fn onboard_people_manual(origin: OriginFor) -> DispatchResultWithPostInfo { ensure_signed(origin)?; // Get the keys for this ring, and make sure that the ring is full before we build it. let (top_ring_index, mut keys) = Self::available_ring(); let mut ring_status = RingKeysStatus::::get(top_ring_index); defensive_assert!( keys.len() == ring_status.total as usize, "Stored key count doesn't match the actual length" ); let keys_len = keys.len() as u32; let open_slots = T::MaxRingSize::get().saturating_sub(keys_len); let (mut head, tail) = QueuePageIndices::::get(); let old_head = head; let mut keys_to_include: Vec> = OnboardingQueue::::take(head).into_inner(); // A `head != tail` condition should mean that there is at least one key in the page // following this one. if keys_to_include.len() < open_slots as usize && head != tail { head = head.checked_add(1).unwrap_or(0); let second_key_page = OnboardingQueue::::take(head); defensive_assert!(!second_key_page.is_empty()); keys_to_include.extend(second_key_page.into_iter()); } let onboarding_size = OnboardingSize::::get(); let (to_include, ring_filled) = Self::should_onboard_people( top_ring_index, &ring_status, open_slots, keys_to_include.len().saturated_into(), onboarding_size, ) .ok_or(Error::::Incomplete)?; let mut remaining_keys = keys_to_include.split_off(to_include as usize); for key in keys_to_include.into_iter() { let personal_id = Keys::::get(&key).defensive().ok_or(Error::::NotPerson)?; let mut record = People::::get(personal_id).defensive().ok_or(Error::::KeyNotFound)?; record.position = RingPosition::Included { ring_index: top_ring_index, ring_position: keys.len().saturated_into(), scheduled_for_removal: false, }; People::::insert(personal_id, record); keys.try_push(key).map_err(|_| Error::::TooManyMembers)?; } RingKeys::::insert(top_ring_index, keys); ActiveMembers::::mutate(|active| *active = active.saturating_add(to_include)); ring_status.total = ring_status.total.saturating_add(to_include); RingKeysStatus::::insert(top_ring_index, ring_status); // Update the top ring index if this onboarding round filled the current ring. if ring_filled { CurrentRingIndex::::mutate(|i| i.saturating_inc()); } if remaining_keys.len() > T::OnboardingQueuePageSize::get() as usize { let split_idx = remaining_keys.len().saturating_sub(T::OnboardingQueuePageSize::get() as usize); let second_page_keys: BoundedVec, T::OnboardingQueuePageSize> = remaining_keys .split_off(split_idx) .try_into() .expect("the list shrunk so it must fit; qed"); let remaining_keys: BoundedVec, T::OnboardingQueuePageSize> = remaining_keys.try_into().expect("the list shrunk so it must fit; qed"); OnboardingQueue::::insert(old_head, remaining_keys); OnboardingQueue::::insert(head, second_page_keys); QueuePageIndices::::put((old_head, tail)); } else if !remaining_keys.is_empty() { let remaining_keys: BoundedVec, T::OnboardingQueuePageSize> = remaining_keys.try_into().expect("the list shrunk so it must fit; qed"); OnboardingQueue::::insert(head, remaining_keys); QueuePageIndices::::put((head, tail)); } else { // We have nothing to put back into the queue, so if this isn't the last page, move // the head to the next page of the queue. if head != tail { head = head.checked_add(1).unwrap_or(0); } QueuePageIndices::::put((head, tail)); } Ok(Pays::No.into()) } /// Merge the people in two rings into a single, new ring. In order for the rings to be /// eligible for merging, they must be below 1/2 of max capacity, have no pending /// suspensions and not be the top ring used for onboarding. #[pezpallet::call_index(102)] pub fn merge_rings( origin: OriginFor, base_ring_index: RingIndex, target_ring_index: RingIndex, ) -> DispatchResultWithPostInfo { let _ = ensure_signed(origin)?; ensure!(RingsState::::get().append_only(), Error::::SuspensionSessionInProgress); // Top ring that onboards new candidates cannot be merged. Identical rings cannot be // merged. let current_ring_index = CurrentRingIndex::::get(); ensure!( base_ring_index != target_ring_index && base_ring_index != current_ring_index && target_ring_index != current_ring_index, Error::::InvalidRing ); // Enforce eligibility criteria. let (mut base_keys, mut base_ring_status) = Self::ring_keys_and_info(base_ring_index); ensure!( base_keys.len() < T::MaxRingSize::get() as usize / 2, Error::::RingAboveMergeThreshold ); ensure!( PendingSuspensions::::decode_len(base_ring_index).unwrap_or(0) == 0, Error::::SuspensionsPending ); let target_keys = RingKeys::::get(target_ring_index); RingKeysStatus::::remove(target_ring_index); ensure!( target_keys.len() < T::MaxRingSize::get() as usize / 2, Error::::RingAboveMergeThreshold ); ensure!( PendingSuspensions::::decode_len(target_ring_index).unwrap_or(0) == 0, Error::::SuspensionsPending ); // Update the status of the ring to reflect the newly added keys. base_ring_status.total = base_ring_status.total.saturating_add(target_keys.len().saturated_into()); for key in target_keys { let personal_id = Keys::::get(&key).defensive().ok_or(Error::::KeyNotFound)?; let mut record = People::::get(personal_id).defensive().ok_or(Error::::NotPerson)?; record.position = RingPosition::Included { ring_index: base_ring_index, ring_position: base_keys.len().saturated_into(), scheduled_for_removal: false, }; base_keys.try_push(key).map_err(|_| Error::::TooManyMembers)?; People::::insert(personal_id, record) } // Newly added keys are not yet included. RingKeys::::insert(base_ring_index, base_keys); RingKeysStatus::::insert(base_ring_index, base_ring_status); // Remove the stale ring root of the target ring. The keys in the target ring will be // part of a valid ring root again when the base ring is rebuilt. Root::::remove(target_ring_index); RingKeys::::remove(target_ring_index); RingKeysStatus::::remove(target_ring_index); Ok(Pays::No.into()) } /// Dispatch a call under an alias using the `account <-> alias` mapping. /// /// This is a call version of the transaction extension `AsPersonalAliasWithAccount`. /// It is recommended to use the transaction extension instead when suitable. #[pezpallet::call_index(0)] #[pezpallet::weight(T::WeightInfo::under_alias().saturating_add(call.get_dispatch_info().call_weight))] pub fn under_alias( origin: OriginFor, call: Box<::RuntimeCall>, ) -> DispatchResultWithPostInfo { let account = ensure_signed(origin.clone())?; let rev_ca = AccountToAlias::::get(&account).ok_or(Error::::InvalidAccount)?; ensure!( Root::::get(rev_ca.ring).is_some_and(|ring| ring.revision == rev_ca.revision), DispatchError::BadOrigin, ); let derivation_weight = T::WeightInfo::under_alias(); let local_origin = Origin::PersonalAlias(rev_ca); Self::derivative_call(origin, local_origin, *call, derivation_weight) } /// This transaction is refunded if successful and no alias was previously set. /// /// The call is valid from `call_valid_at` until /// `call_valid_at + account_setup_time_tolerance`. /// `account_setup_time_tolerance` is a constant available in the metadata. /// /// Parameters: /// - `account`: The account to set the alias for. /// - `call_valid_at`: The block number when the call becomes valid. #[pezpallet::call_index(1)] pub fn set_alias_account( origin: OriginFor, account: T::AccountId, call_valid_at: BlockNumberFor, ) -> DispatchResultWithPostInfo { let rev_ca = Self::ensure_revised_personal_alias(origin)?; let now = pezframe_system::Pezpallet::::block_number(); let time_tolerance = Self::account_setup_time_tolerance(); ensure!( call_valid_at <= now && now <= call_valid_at.saturating_add(time_tolerance), Error::::TimeOutOfRange ); ensure!(T::AccountContexts::contains(&rev_ca.ca.context), Error::::InvalidContext); ensure!(!AccountToPersonalId::::contains_key(&account), Error::::AccountInUse); let old_account = AliasToAccount::::get(&rev_ca.ca); let old_rev_ca = old_account.as_ref().and_then(AccountToAlias::::get); let needs_revision = old_rev_ca.is_some_and(|old_rev_ca| { old_rev_ca.revision != rev_ca.revision || old_rev_ca.ring != rev_ca.ring }); // Ensure it changes the account associated, or it needs revision. ensure!( old_account.as_ref() != Some(&account) || needs_revision, Error::::AliasAccountAlreadySet ); // If the old account is different from the new one: // * decrease the sufficients of the old account // * increase the sufficients of the new account // * check new account is not already in use if old_account.as_ref() != Some(&account) { ensure!(!AccountToAlias::::contains_key(&account), Error::::AccountInUse); if let Some(old_account) = &old_account { pezframe_system::Pezpallet::::dec_sufficients(old_account); AccountToAlias::::remove(old_account); } pezframe_system::Pezpallet::::inc_sufficients(&account); } AccountToAlias::::insert(&account, &rev_ca); AliasToAccount::::insert(&rev_ca.ca, &account); if old_account.is_none() || needs_revision { Ok(Pays::No.into()) } else { Ok(Pays::Yes.into()) } } /// Remove the mapping from a particular alias to its registered account. #[pezpallet::call_index(2)] pub fn unset_alias_account(origin: OriginFor) -> DispatchResult { let alias = Self::ensure_personal_alias(origin)?; let account = AliasToAccount::::take(&alias).ok_or(Error::::InvalidAccount)?; AccountToAlias::::remove(&account); pezframe_system::Pezpallet::::dec_sufficients(&account); Ok(()) } /// Recognize a set of people without any additional checks. /// /// The people are identified by the provided list of keys and will each be assigned, in /// order, the next available personal ID. /// /// The origin for this call must have root privileges. #[pezpallet::call_index(3)] pub fn force_recognize_personhood( origin: OriginFor, people: Vec>, ) -> DispatchResultWithPostInfo { ensure_root(origin)?; for key in people { let personal_id = Self::reserve_new_id(); Self::recognize_personhood(personal_id, Some(key))?; } Ok(().into()) } /// Set a personal id account. /// /// The account can then be used to sign transactions on behalf of the personal id, and /// provide replay protection with the nonce. /// /// This transaction is refunded if successful and no account was previously set for the /// personal id. /// /// The call is valid from `call_valid_at` until /// `call_valid_at + account_setup_time_tolerance`. /// `account_setup_time_tolerance` is a constant available in the metadata. /// /// Parameters: /// - `account`: The account to set the alias for. /// - `call_valid_at`: The block number when the call becomes valid. #[pezpallet::call_index(4)] pub fn set_personal_id_account( origin: OriginFor, account: T::AccountId, call_valid_at: BlockNumberFor, ) -> DispatchResultWithPostInfo { let id = Self::ensure_personal_identity(origin)?; let now = pezframe_system::Pezpallet::::block_number(); let time_tolerance = Self::account_setup_time_tolerance(); ensure!( call_valid_at <= now && now <= call_valid_at.saturating_add(time_tolerance), Error::::TimeOutOfRange ); ensure!(!AccountToPersonalId::::contains_key(&account), Error::::AccountInUse); ensure!(!AccountToAlias::::contains_key(&account), Error::::AccountInUse); let mut record = People::::get(id).ok_or(Error::::NotPerson)?; let pays = if let Some(old_account) = record.account { pezframe_system::Pezpallet::::dec_sufficients(&old_account); AccountToPersonalId::::remove(&old_account); Pays::Yes } else { Pays::No }; record.account = Some(account.clone()); pezframe_system::Pezpallet::::inc_sufficients(&account); AccountToPersonalId::::insert(&account, id); People::::insert(id, &record); Ok(pays.into()) } /// Unset the personal id account. #[pezpallet::call_index(5)] pub fn unset_personal_id_account(origin: OriginFor) -> DispatchResultWithPostInfo { let id = Self::ensure_personal_identity(origin)?; let mut record = People::::get(id).ok_or(Error::::NotPerson)?; let account = record.account.take().ok_or(Error::::InvalidAccount)?; AccountToPersonalId::::take(&account).ok_or(Error::::InvalidAccount)?; pezframe_system::Pezpallet::::dec_sufficients(&account); People::::insert(id, &record); Ok(Pays::Yes.into()) } /// Migrate the key for a person who was onboarded and is currently included in a ring. The /// migration is not instant as the key replacement and subsequent inclusion in a new ring /// root will happen only after the next mutation session. #[pezpallet::call_index(6)] pub fn migrate_included_key( origin: OriginFor, new_key: MemberOf, ) -> DispatchResultWithPostInfo { let id = Self::ensure_personal_identity(origin)?; ensure!(!Keys::::contains_key(&new_key), Error::::KeyAlreadyInUse); let mut record = People::::get(id).ok_or(Error::::NotPerson)?; ensure!(record.key != new_key, Error::::SameKey); match &record.position { // If the key is already included in a ring, enqueue it for migration during the // next mutation session. RingPosition::Included { ring_index, ring_position, .. } => { // If the person scheduled another migration before, remove the key we are // replacing from the key registry. if let Some(old_migrated_key) = KeyMigrationQueue::::get(id) { Keys::::remove(old_migrated_key); } // Add this new key to the migration queue. KeyMigrationQueue::::insert(id, &new_key); // Mark this record as stale. record.position = RingPosition::Included { ring_index: *ring_index, ring_position: *ring_position, scheduled_for_removal: true, }; // Update the record. People::::insert(id, record); }, // This call accepts migrations only for included keys. RingPosition::Onboarding { .. } => return Err(Error::::InvalidKeyMigration.into()), // Suspended people shouldn't be able to call this, but protect against this case // anyway. RingPosition::Suspended => return Err(Error::::Suspended.into()), } Keys::::insert(new_key, id); Ok(().into()) } /// Migrate the key for a person who is currently onboarding. The operation is instant, /// replacing the old key in the onboarding queue. #[pezpallet::call_index(7)] pub fn migrate_onboarding_key( origin: OriginFor, new_key: MemberOf, ) -> DispatchResultWithPostInfo { let id = Self::ensure_personal_identity(origin)?; ensure!(!Keys::::contains_key(&new_key), Error::::KeyAlreadyInUse); let mut record = People::::get(id).ok_or(Error::::NotPerson)?; ensure!(record.key != new_key, Error::::SameKey); match &record.position { // If it's still onboarding, just replace the old key in the queue. RingPosition::Onboarding { queue_page } => { let mut keys = OnboardingQueue::::get(queue_page); if let Some(idx) = keys.iter().position(|k| *k == record.key) { // Remove the key that never made it into a ring. Keys::::remove(&keys[idx]); // Update the key in the queue. keys[idx] = new_key.clone(); OnboardingQueue::::insert(queue_page, keys); // Replace the key in the record. record.key = new_key.clone(); // Update the record. People::::insert(id, record); } else { defensive!("No key found at the position in the person record of {}", id); } }, // This call accepts migrations only for included keys. RingPosition::Included { .. } => return Err(Error::::InvalidKeyMigration.into()), // Suspended people shouldn't be able to call this, but protect against this case // anyway. RingPosition::Suspended => return Err(Error::::Suspended.into()), } Keys::::insert(new_key, id); Ok(().into()) } /// Force set the onboarding size for new people. This call requires root privileges. #[pezpallet::call_index(8)] pub fn set_onboarding_size( origin: OriginFor, onboarding_size: u32, ) -> DispatchResultWithPostInfo { ensure_root(origin)?; ensure!( onboarding_size <= ::MaxRingSize::get(), Error::::InvalidOnboardingSize ); OnboardingSize::::put(onboarding_size); Ok(Pays::No.into()) } } impl Pezpallet { /// If the conditions to build a ring are met, this function returns the number of people to /// be included in a `build_ring` call. Otherwise, this function returns `None`. pub(crate) fn should_build_ring(ring_index: RingIndex, limit: u32) -> Option { // Ring root cannot be built while there are people to remove. if !RingsState::::get().append_only() { return None; } // Suspended people should be removed from the ring before building it. if PendingSuspensions::::contains_key(ring_index) { return None; } let ring_status = RingKeysStatus::::get(ring_index); let not_included_count = ring_status.total.saturating_sub(ring_status.included); let to_include = not_included_count.min(limit); // There must be at least one person waiting to be included to build the ring. if to_include == 0 { return None; } Some(to_include) } /// If the conditions to onboard new people into rings are met, this function returns the /// number of people to be onboarded from the queue in a `onboard_people` call along with a /// flag which states whether the call will completely populate the ring. Otherwise, this /// function returns `None`. fn should_onboard_people( ring_index: RingIndex, ring_status: &RingStatus, open_slots: u32, available_for_inclusion: u32, onboarding_size: u32, ) -> Option<(u32, bool)> { // People cannot be onboarded while suspensions are ongoing. if !RingsState::::get().append_only() { return None; } // Suspended people should be removed from the ring before building it. if PendingSuspensions::::contains_key(ring_index) { return None; } let to_include = available_for_inclusion.min(open_slots); // If everything is already included, nothing to do. if to_include == 0 { return None; } // Here we check we have enough items in the queue so that the onboarding group size is // respected, but also that we can support another queue of at least onboarding size // in a future call. let can_onboard_with_cohort = to_include >= onboarding_size && ring_status.total.saturating_add(to_include.saturated_into()) <= T::MaxRingSize::get().saturating_sub(onboarding_size); // If this call completely fills the ring, no onboarding rule enforcement will be // necessary. let ring_filled = open_slots == to_include; let should_onboard = ring_filled || can_onboard_with_cohort; if !should_onboard { return None; } Some((to_include, ring_filled)) } /// Returns whether suspensions are allowed and necessary for a given ring index. pub(crate) fn should_remove_suspended_keys( ring_index: RingIndex, check_rings_state: bool, ) -> bool { if check_rings_state && !RingsState::::get().append_only() { return false; } let suspended_count = PendingSuspensions::::decode_len(ring_index).unwrap_or(0); // There must be keys to suspend. if suspended_count == 0 { return false; } true } /// Function that checks if the top two onboarding queue pages can be merged into a single /// page to defragment the list. This function returns an action to take following the /// check. In case a merge is needed, the following information is provided, in order: /// * The initial `head` of the queue - will need to remove the page at this index in case /// the merge is performed. /// * The new `head` of the queue. /// * The keys on the first page of the queue. /// * The keys on the second page of the queue. pub(crate) fn should_merge_queue_pages() -> QueueMergeAction { let (initial_head, tail) = QueuePageIndices::::get(); let first_key_page = OnboardingQueue::::get(initial_head); // A `head != tail` condition should mean that there is at least one more page // following this one. if initial_head == tail { return QueueMergeAction::NoAction; } let new_head = initial_head.checked_add(1).unwrap_or(0); let second_key_page = OnboardingQueue::::get(new_head); let page_size = T::OnboardingQueuePageSize::get(); // Make sure the pages can be merged. if first_key_page.len().saturating_add(second_key_page.len()) > page_size as usize { return QueueMergeAction::NoAction; } QueueMergeAction::Merge { initial_head, new_head, first_key_page, second_key_page } } /// Build a ring root by adding all people who were assigned to this ring but not yet /// included into the root. pub(crate) fn build_ring(ring_index: RingIndex, to_include: u32) -> DispatchResult { let (keys, mut ring_status) = Self::ring_keys_and_info(ring_index); // Get the current ring, and check it should be rebuilt. // Return the next revision. let (next_revision, mut intermediate) = if let Some(existing_root) = Root::::get(ring_index) { // We should build a new ring. Return the new revision number we should use. ( existing_root.revision.checked_add(1).ok_or(ArithmeticError::Overflow)?, existing_root.intermediate, ) } else { // No ring has been built at this index, so we start at revision 0. (0, T::Crypto::start_members()) }; // Push the members. T::Crypto::push_members( &mut intermediate, keys.iter() .skip(ring_status.included as usize) .take(to_include as usize) .cloned(), Self::fetch_chunks, ) .defensive() .map_err(|_| Error::::CouldNotPush)?; // By the end of the loop, we have included the maximum number of keys in the vector. ring_status.included = ring_status.included.saturating_add(to_include); RingKeysStatus::::insert(ring_index, ring_status); // We create the root after pushing all members. let root = T::Crypto::finish_members(intermediate.clone()); let ring_root = RingRoot { root, revision: next_revision, intermediate }; Root::::insert(ring_index, ring_root); Ok(()) } /// Onboard as many people as possible into the available ring. /// /// This function returns an error if there aren't enough people in the onboarding queue to /// complete the operation, or if the number of remaining open slots in the ring would be /// below the minimum onboarding size allowed. #[transactional] pub(crate) fn onboard_people() -> DispatchResult { // Get the keys for this ring, and make sure that the ring is full before we build it. let (top_ring_index, mut keys) = Self::available_ring(); let mut ring_status = RingKeysStatus::::get(top_ring_index); defensive_assert!( keys.len() == ring_status.total as usize, "Stored key count doesn't match the actual length" ); let keys_len = keys.len() as u32; let open_slots = T::MaxRingSize::get().saturating_sub(keys_len); let (mut head, tail) = QueuePageIndices::::get(); let old_head = head; let mut keys_to_include: Vec> = OnboardingQueue::::take(head).into_inner(); // A `head != tail` condition should mean that there is at least one key in the page // following this one. if keys_to_include.len() < open_slots as usize && head != tail { head = head.checked_add(1).unwrap_or(0); let second_key_page = OnboardingQueue::::take(head); defensive_assert!(!second_key_page.is_empty()); keys_to_include.extend(second_key_page.into_iter()); } let onboarding_size = OnboardingSize::::get(); let (to_include, ring_filled) = Self::should_onboard_people( top_ring_index, &ring_status, open_slots, keys_to_include.len().saturated_into(), onboarding_size, ) .ok_or(Error::::Incomplete)?; let mut remaining_keys = keys_to_include.split_off(to_include as usize); for key in keys_to_include.into_iter() { let personal_id = Keys::::get(&key).defensive().ok_or(Error::::NotPerson)?; let mut record = People::::get(personal_id).defensive().ok_or(Error::::KeyNotFound)?; record.position = RingPosition::Included { ring_index: top_ring_index, ring_position: keys.len().saturated_into(), scheduled_for_removal: false, }; People::::insert(personal_id, record); keys.try_push(key).defensive().map_err(|_| Error::::TooManyMembers)?; } RingKeys::::insert(top_ring_index, keys); ActiveMembers::::mutate(|active| *active = active.saturating_add(to_include)); ring_status.total = ring_status.total.saturating_add(to_include); RingKeysStatus::::insert(top_ring_index, ring_status); // Update the top ring index if this onboarding round filled the current ring. if ring_filled { CurrentRingIndex::::mutate(|i| i.saturating_inc()); } if remaining_keys.len() > T::OnboardingQueuePageSize::get() as usize { let split_idx = remaining_keys.len().saturating_sub(T::OnboardingQueuePageSize::get() as usize); let second_page_keys: BoundedVec, T::OnboardingQueuePageSize> = remaining_keys .split_off(split_idx) .try_into() .expect("the list shrunk so it must fit; qed"); let remaining_keys: BoundedVec, T::OnboardingQueuePageSize> = remaining_keys.try_into().expect("the list shrunk so it must fit; qed"); OnboardingQueue::::insert(old_head, remaining_keys); OnboardingQueue::::insert(head, second_page_keys); QueuePageIndices::::put((old_head, tail)); } else if !remaining_keys.is_empty() { let remaining_keys: BoundedVec, T::OnboardingQueuePageSize> = remaining_keys.try_into().expect("the list shrunk so it must fit; qed"); OnboardingQueue::::insert(head, remaining_keys); QueuePageIndices::::put((head, tail)); } else { // We have nothing to put back into the queue, so if this isn't the last page, move // the head to the next page of the queue. if head != tail { head = head.checked_add(1).unwrap_or(0); } QueuePageIndices::::put((head, tail)); } Ok(()) } fn derivative_call( mut origin: OriginFor, local_origin: Origin, call: ::RuntimeCall, derivation_weight: Weight, ) -> DispatchResultWithPostInfo { origin.set_caller_from(::PalletsOrigin::from( local_origin, )); let info = call.get_dispatch_info(); let result = call.dispatch(origin); let weight = derivation_weight.saturating_add(extract_actual_weight(&result, &info)); result .map(|p| PostDispatchInfo { actual_weight: Some(weight), pays_fee: p.pays_fee }) .map_err(|mut err| { err.post_info = Some(weight).into(); err }) } /// Ensure that the origin `o` represents a person. /// Returns `Ok` with the base identity of the person on success. pub fn ensure_personal_identity( origin: T::RuntimeOrigin, ) -> Result { Ok(ensure_personal_identity(origin.into_caller())?) } /// Ensure that the origin `o` represents a person. /// Returns `Ok` with the alias of the person together with the context in which it can /// be used on success. pub fn ensure_personal_alias( origin: T::RuntimeOrigin, ) -> Result { Ok(ensure_personal_alias(origin.into_caller())?) } /// Ensure that the origin `o` represents a person. /// On success returns `Ok` with the revised alias of the person together with the context /// in which it can be used and the revision of the ring the person is in. pub fn ensure_revised_personal_alias( origin: T::RuntimeOrigin, ) -> Result { Ok(ensure_revised_personal_alias(origin.into_caller())?) } // This function always returns the ring index and the keys for the ring which is currently // accepting new members. pub fn available_ring() -> (RingIndex, BoundedVec, T::MaxRingSize>) { let mut current_ring_index = CurrentRingIndex::::get(); let mut current_keys = RingKeys::::get(current_ring_index); defensive_assert!( !current_keys.is_full(), "Something bad happened inside the STF, where the current keys are full, but we should have incremented in that case." ); // This condition shouldn't be reached, but we handle the error just in case. if current_keys.is_full() { current_ring_index.saturating_inc(); CurrentRingIndex::::put(current_ring_index); current_keys = RingKeys::::get(current_ring_index); } defensive_assert!( !current_keys.is_full(), "Something bad happened inside the STF, where the current key and next key are both full. Nothing we can do here." ); (current_ring_index, current_keys) } // This allows us to associate a key with a person. pub fn do_insert_key(who: PersonalId, key: MemberOf) -> DispatchResult { // If the key is already in use by another person then error. ensure!(!Keys::::contains_key(&key), Error::::KeyAlreadyInUse); // This is a first time key, so it must be reserved. ensure!( ReservedPersonalId::::take(who).is_some(), Error::::PersonalIdNotReservedOrNotRecognized ); Self::push_to_onboarding_queue(who, key, None) } // Enqueue personhood suspensions. This function can be called multiple times until all // people are marked as suspended, but it can only happen while there is a mutation session // in progress. pub fn queue_personhood_suspensions(suspensions: &[PersonalId]) -> DispatchResult { ensure!(RingsState::::get().mutating(), Error::::NoMutationSession); for who in suspensions { let mut record = People::::get(who).ok_or(Error::::InvalidSuspensions)?; match record.position { RingPosition::Included { ring_index, ring_position, .. } => { let mut suspended_indices = PendingSuspensions::::get(ring_index); let Err(insert_idx) = suspended_indices.binary_search(&ring_position) else { return Err(Error::::KeyAlreadySuspended.into()); }; suspended_indices .try_insert(insert_idx, ring_position) .defensive() .map_err(|_| Error::::TooManyMembers)?; PendingSuspensions::::insert(ring_index, suspended_indices); }, RingPosition::Onboarding { queue_page } => { let mut keys = OnboardingQueue::::get(queue_page); let queue_idx = keys.iter().position(|k| *k == record.key); if let Some(idx) = queue_idx { // It is expensive to shift the whole vec in the worst case to remove a // suspended person from onboarding, but the pages will be small and // suspension of people who are not yet onboarded is supposed to be // extremely rare if not impossible as the pezpallet hooks should have // plenty of time to include someone recognized before the beginning of // the next suspension round. The only legitimate case when this could // happen is if someone is sitting in the onboarding queue for a long // time and cannot be included because not enough people are joining, // but it should be a rare case. keys.remove(idx); OnboardingQueue::::insert(queue_page, keys); } else { defensive!( "No key found at the position in the person record of {}", who ); } }, RingPosition::Suspended => { defensive!("Suspension queued for person {} while already suspended", who); }, } record.position = RingPosition::Suspended; if let Some(account) = record.account { AccountToPersonalId::::remove(account); record.account = None; } People::::insert(who, record); } Ok(()) } // Resume someone's personhood. This assumes that their personhood is currently suspended, // so the person was previously recognized. pub fn resume_personhood(who: PersonalId) -> DispatchResult { let record = People::::get(who).ok_or(Error::::NotPerson)?; ensure!(record.position.suspended(), Error::::NotSuspended); ensure!(Keys::::get(&record.key) == Some(who), Error::::NoKey); Self::push_to_onboarding_queue(who, record.key, record.account) } fn push_to_onboarding_queue( who: PersonalId, key: MemberOf, account: Option, ) -> DispatchResult { let (head, mut tail) = QueuePageIndices::::get(); let mut keys = OnboardingQueue::::get(tail); if let Err(k) = keys.try_push(key.clone()) { tail = tail.checked_add(1).unwrap_or(0); ensure!(tail != head, Error::::TooManyMembers); keys = alloc::vec![k].try_into().expect("must be able to hold one key; qed"); }; let record = PersonRecord { key, position: RingPosition::Onboarding { queue_page: tail }, account, }; Keys::::insert(&record.key, who); People::::insert(who, &record); Self::deposit_event(Event::::PersonOnboarding { who, key: record.key }); QueuePageIndices::::put((head, tail)); OnboardingQueue::::insert(tail, keys); Ok(()) } /// Fetch the keys in a ring along with stored inclusion information. pub fn ring_keys_and_info( ring_index: RingIndex, ) -> (BoundedVec, T::MaxRingSize>, RingStatus) { let keys = RingKeys::::get(ring_index); let ring_status = RingKeysStatus::::get(ring_index); defensive_assert!( keys.len() == ring_status.total as usize, "Stored key count doesn't match the actual length" ); (keys, ring_status) } // Given a range, returns the list of chunks that maps to the keys at those indices. pub(crate) fn fetch_chunks( range: Range, ) -> Result::StaticChunk>, ()> { let chunk_page_size = T::ChunkPageSize::get(); let expected_len = range.end.saturating_sub(range.start); let mut page_idx = range.start.checked_div(chunk_page_size as usize).ok_or(())?; let mut chunks: Vec<_> = Chunks::::get(page_idx.saturated_into::()) .defensive() .ok_or(())? .into_iter() .skip(range.start % chunk_page_size as usize) .take(expected_len) .collect(); while chunks.len() < expected_len { // Condition to eventually break out of a possible infinite loop in case // storage is full of empty chunk pages. page_idx = page_idx.checked_add(1).ok_or(())?; let page = Chunks::::get(page_idx.saturated_into::()).defensive().ok_or(())?; chunks.extend( page.into_inner().into_iter().take(expected_len.saturating_sub(chunks.len())), ); } Ok(chunks) } /// Migrates keys that people intend to replace with other keys, if possible. As this /// function mutates a fair amount of storage, it comes with a weight meter to limit on the /// number of keys to migrate in one call. pub(crate) fn migrate_keys(meter: &mut WeightMeter) { let mut drain = KeyMigrationQueue::::drain(); loop { // Ensure we have enough weight to look into `KeyMigrationQueue` and perform a // removal. let weight = T::WeightInfo::migrate_keys_single_included_key() .saturating_add(T::DbWeight::get().reads_writes(1, 1)); if !meter.can_consume(weight) { return; } let op_res = with_storage_layer::(|| match drain.next() { Some((id, new_key)) => Self::migrate_keys_single_included_key(id, new_key).map(|_| false), None => { let rings_state = RingsState::::get() .end_key_migration() .map_err(|_| Error::::NoMutationSession)?; RingsState::::put(rings_state); meter.consume(T::DbWeight::get().reads_writes(1, 1)); Ok(true) }, }); match op_res { Ok(false) => meter.consume(weight), Ok(true) => { // Read on `KeyMigrationQueue`. meter.consume(T::DbWeight::get().reads(1)); break; }, Err(e) => { meter.consume(weight); log::error!(target: LOG_TARGET, "failed to migrate keys: {:?}", e); break; }, } } } /// A single iteration of the key migration process where an included key marked for /// suspension is being removed from a ring. pub(crate) fn migrate_keys_single_included_key( id: PersonalId, new_key: MemberOf, ) -> DispatchResult { if let Some(record) = People::::get(id) { let RingPosition::Included { ring_index, ring_position, scheduled_for_removal: true, } = record.position else { Keys::::remove(new_key); return Ok(()); }; let mut suspended_indices = PendingSuspensions::::get(ring_index); let Err(insert_idx) = suspended_indices.binary_search(&ring_position) else { log::info!(target: LOG_TARGET, "key migration for person {} skipped as the person's key was already suspended", id); return Ok(()); }; suspended_indices .try_insert(insert_idx, ring_position) .map_err(|_| Error::::TooManyMembers)?; PendingSuspensions::::insert(ring_index, suspended_indices); Keys::::remove(&record.key); Self::push_to_onboarding_queue(id, new_key, record.account)?; } else { log::info!(target: LOG_TARGET, "key migration for person {} skipped as no record was found", id); } Ok(()) } /// Removes people's keys marked as suspended or inactive from a ring with a given index. pub(crate) fn remove_suspended_keys(ring_index: RingIndex) -> Weight { let keys = RingKeys::::get(ring_index); let keys_len = keys.len(); let suspended_indices = PendingSuspensions::::get(ring_index); // Construct the new keys map by skipping the suspended keys. This should prevent // reallocations in the `Vec` which happens with `remove`. let mut new_keys: BoundedVec, T::MaxRingSize> = Default::default(); let mut j = 0; for (i, key) in keys.into_iter().enumerate() { if j < suspended_indices.len() && i == suspended_indices[j] as usize { j += 1; } else if new_keys .try_push(key) .defensive_proof("cannot move more ring members than the max ring size; qed") .is_err() { return T::WeightInfo::remove_suspended_people( keys_len.try_into().unwrap_or(u32::MAX), ); } } let suspended_count = RingKeysStatus::::mutate(ring_index, |ring_status| { let new_total = new_keys.len().saturated_into(); let suspended_count = ring_status.total.saturating_sub(new_total); ring_status.total = new_total; ring_status.included = 0; suspended_count }); ActiveMembers::::mutate(|active| *active = active.saturating_sub(suspended_count)); RingKeys::::insert(ring_index, new_keys); Root::::mutate(ring_index, |maybe_root| { if let Some(root) = maybe_root { // The revision will be incremented on the next call of `build_ring`. The // current root is preserved. root.intermediate = T::Crypto::start_members(); } }); // Make sure to remove the entry from the map so that the pezpallet hooks don't iterate // over it. PendingSuspensions::::remove(ring_index); T::WeightInfo::remove_suspended_people(keys_len.try_into().unwrap_or(u32::MAX)) } /// Merges the two pages at the front of the onboarding queue. After a round of suspensions, /// it is possible for the second page of the onboarding queue to be left with few members /// such that, if the first page also has few members, the total count is below the required /// onboarding size, thus stalling the queue. This function fixes this by moving the people /// from the first page to the front of the second page, defragmenting the queue. /// /// If the operation fails, the storage is rolled back. pub(crate) fn merge_queue_pages( initial_head: u32, new_head: u32, mut first_key_page: BoundedVec, T::OnboardingQueuePageSize>, second_key_page: BoundedVec, T::OnboardingQueuePageSize>, ) { let op_res = with_storage_layer::<(), DispatchError, _>(|| { // Update the records of the people in the first page. for key in first_key_page.iter() { let personal_id = Keys::::get(key).defensive().ok_or(Error::::NotPerson)?; let mut record = People::::get(personal_id).defensive().ok_or(Error::::KeyNotFound)?; record.position = RingPosition::Onboarding { queue_page: new_head }; People::::insert(personal_id, record); } first_key_page .try_extend(second_key_page.into_iter()) .defensive() .map_err(|_| Error::::TooManyMembers)?; OnboardingQueue::::remove(initial_head); OnboardingQueue::::insert(new_head, first_key_page); QueuePageIndices::::mutate(|(h, _)| *h = new_head); Ok(()) }); if let Err(e) = op_res { log::error!(target: LOG_TARGET, "failed to merge queue pages: {:?}", e); } } } impl AddOnlyPeopleTrait for Pezpallet { type Member = MemberOf; fn reserve_new_id() -> PersonalId { let new_id = NextPersonalId::::mutate(|id| { let new_id = *id; id.saturating_inc(); new_id }); ReservedPersonalId::::insert(new_id, ()); new_id } fn cancel_id_reservation(personal_id: PersonalId) -> Result<(), DispatchError> { ReservedPersonalId::::take(personal_id).ok_or(Error::::PersonalIdNotReserved)?; Ok(()) } fn renew_id_reservation(personal_id: PersonalId) -> Result<(), DispatchError> { if NextPersonalId::::get() <= personal_id || People::::contains_key(personal_id) || ReservedPersonalId::::contains_key(personal_id) { return Err(Error::::PersonalIdReservationCannotRenew.into()); } ReservedPersonalId::::insert(personal_id, ()); Ok(()) } fn recognize_personhood( who: PersonalId, maybe_key: Option>, ) -> Result<(), DispatchError> { match maybe_key { Some(key) => Self::do_insert_key(who, key), None => Self::resume_personhood(who), } } #[cfg(feature = "runtime-benchmarks")] type Secret = <::Crypto as GenerateVerifiable>::Secret; #[cfg(feature = "runtime-benchmarks")] fn mock_key(who: PersonalId) -> (Self::Member, Self::Secret) { let mut buf = [0u8; 32]; buf[..core::mem::size_of::()].copy_from_slice(&who.to_le_bytes()[..]); let secret = T::Crypto::new_secret(buf); (T::Crypto::member_from_secret(&secret), secret) } } impl PeopleTrait for Pezpallet { fn suspend_personhood(suspensions: &[PersonalId]) -> DispatchResult { Self::queue_personhood_suspensions(suspensions) } fn start_people_set_mutation_session() -> DispatchResult { let current_state = RingsState::::get(); RingsState::::put( current_state .start_mutation_session() .map_err(|_| Error::::CouldNotStartMutationSession)?, ); Ok(()) } fn end_people_set_mutation_session() -> DispatchResult { let current_state = RingsState::::get(); RingsState::::put( current_state .end_mutation_session() .map_err(|_| Error::::NoMutationSession)?, ); Ok(()) } } /// Ensure that the origin `o` represents an extrinsic (i.e. transaction) from a personal /// identity. Returns `Ok` with the personal identity that signed the extrinsic or an `Err` /// otherwise. pub fn ensure_personal_identity(o: OuterOrigin) -> Result where OuterOrigin: TryInto, { match o.try_into() { Ok(Origin::PersonalIdentity(m)) => Ok(m), _ => Err(BadOrigin), } } /// Ensure that the origin `o` represents an extrinsic (i.e. transaction) from a personal alias. /// Returns `Ok` with the personal alias that signed the extrinsic or an `Err` otherwise. pub fn ensure_personal_alias(o: OuterOrigin) -> Result where OuterOrigin: TryInto, { match o.try_into() { Ok(Origin::PersonalAlias(rev_ca)) => Ok(rev_ca.ca), _ => Err(BadOrigin), } } /// Guard to ensure that the given origin is a person. The underlying identity of the person is /// provided on success. pub struct EnsurePersonalIdentity(PhantomData); impl EnsureOrigin> for EnsurePersonalIdentity { type Success = PersonalId; fn try_origin(o: OriginFor) -> Result> { ensure_personal_identity(o.clone().into_caller()).map_err(|_| o) } #[cfg(feature = "runtime-benchmarks")] fn try_successful_origin() -> Result, ()> { Ok(Origin::PersonalIdentity(0).into()) } } pezframe_support::impl_ensure_origin_with_arg_ignoring_arg! { impl<{ T: Config, A }> EnsureOriginWithArg< OriginFor, A> for EnsurePersonalIdentity {} } impl CountedMembers for EnsurePersonalIdentity { fn active_count(&self) -> u32 { Keys::::count() } } /// Guard to ensure that the given origin is a person. The contextual alias of the person is /// provided on success. pub struct EnsurePersonalAlias(PhantomData); impl EnsureOrigin> for EnsurePersonalAlias { type Success = ContextualAlias; fn try_origin(o: OriginFor) -> Result> { ensure_personal_alias(o.clone().into_caller()).map_err(|_| o) } #[cfg(feature = "runtime-benchmarks")] fn try_successful_origin() -> Result, ()> { Ok(Origin::PersonalAlias(RevisedContextualAlias { revision: 0, ring: 0, ca: ContextualAlias { alias: [1; 32], context: [0; 32] }, }) .into()) } } pezframe_support::impl_ensure_origin_with_arg_ignoring_arg! { impl<{ T: Config, A }> EnsureOriginWithArg< OriginFor, A> for EnsurePersonalAlias {} } impl CountedMembers for EnsurePersonalAlias { fn active_count(&self) -> u32 { ActiveMembers::::get() } } /// Guard to ensure that the given origin is a person. The alias of the person within the /// context provided as an argument is returned on success. pub struct EnsurePersonalAliasInContext(PhantomData); impl EnsureOriginWithArg, Context> for EnsurePersonalAliasInContext { type Success = Alias; fn try_origin(o: OriginFor, arg: &Context) -> Result> { match ensure_personal_alias(o.clone().into_caller()) { Ok(ca) if &ca.context == arg => Ok(ca.alias), _ => Err(o), } } #[cfg(feature = "runtime-benchmarks")] fn try_successful_origin(context: &Context) -> Result, ()> { Ok(Origin::PersonalAlias(RevisedContextualAlias { revision: 0, ring: 0, ca: ContextualAlias { alias: [1; 32], context: *context }, }) .into()) } } impl CountedMembers for EnsurePersonalAliasInContext { fn active_count(&self) -> u32 { ActiveMembers::::get() } } /// Ensure that the origin `o` represents an extrinsic (i.e. transaction) from a personal alias /// with revision information. /// /// Returns `Ok` with the revised personal alias that signed the extrinsic or an `Err` /// otherwise. pub fn ensure_revised_personal_alias( o: OuterOrigin, ) -> Result where OuterOrigin: TryInto, { match o.try_into() { Ok(Origin::PersonalAlias(rev_ca)) => Ok(rev_ca), _ => Err(BadOrigin), } } /// Guard to ensure that the given origin is a person. /// /// The revised contextual alias of the person is provided on success. The revision can be used /// to tell in the future if an alias may have been suspended. See [`RevisedContextualAlias`]. pub struct EnsureRevisedPersonalAlias(PhantomData); impl EnsureOrigin> for EnsureRevisedPersonalAlias { type Success = RevisedContextualAlias; fn try_origin(o: OriginFor) -> Result> { ensure_revised_personal_alias(o.clone().into_caller()).map_err(|_| o) } #[cfg(feature = "runtime-benchmarks")] fn try_successful_origin() -> Result, ()> { Ok(Origin::PersonalAlias(RevisedContextualAlias { revision: 0, ring: 0, ca: ContextualAlias { alias: [1; 32], context: [0; 32] }, }) .into()) } } pezframe_support::impl_ensure_origin_with_arg_ignoring_arg! { impl<{ T: Config, A }> EnsureOriginWithArg< OriginFor, A> for EnsureRevisedPersonalAlias {} } impl CountedMembers for EnsureRevisedPersonalAlias { fn active_count(&self) -> u32 { ActiveMembers::::get() } } /// Guard to ensure that the given origin is a person. /// /// The revised alias of the person within the context provided as an argument is returned on /// success. The revision can be used to tell in the future if an alias may have been suspended. /// See [`RevisedAlias`]. pub struct EnsureRevisedPersonalAliasInContext(PhantomData); impl EnsureOriginWithArg, Context> for EnsureRevisedPersonalAliasInContext { type Success = RevisedAlias; fn try_origin(o: OriginFor, arg: &Context) -> Result> { match ensure_revised_personal_alias(o.clone().into_caller()) { Ok(ca) if &ca.ca.context == arg => Ok(RevisedAlias { revision: ca.revision, ring: ca.ring, alias: ca.ca.alias }), _ => Err(o), } } #[cfg(feature = "runtime-benchmarks")] fn try_successful_origin(context: &Context) -> Result, ()> { Ok(Origin::PersonalAlias(RevisedContextualAlias { revision: 0, ring: 0, ca: ContextualAlias { alias: [1; 32], context: *context }, }) .into()) } } impl CountedMembers for EnsureRevisedPersonalAliasInContext { fn active_count(&self) -> u32 { ActiveMembers::::get() } } }