2072 lines
77 KiB
Rust
2072 lines
77 KiB
Rust
// 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<T>(_);
|
|
|
|
#[pezpallet::config]
|
|
pub trait Config:
|
|
pezframe_system::Config<
|
|
RuntimeOrigin: From<Origin>
|
|
+ From<<Self::RuntimeOrigin as OriginTrait>::PalletsOrigin>
|
|
+ OriginTrait<
|
|
PalletsOrigin: From<Origin>
|
|
+ TryInto<
|
|
Origin,
|
|
Error = <Self::RuntimeOrigin as OriginTrait>::PalletsOrigin,
|
|
>,
|
|
>,
|
|
RuntimeCall: Parameter
|
|
+ GetDispatchInfo
|
|
+ IsSubType<Call<Self>>
|
|
+ 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<Event<Self>>
|
|
+ IsType<<Self as pezframe_system::Config>::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<Context>;
|
|
|
|
/// Number of chunks per page.
|
|
#[pezpallet::constant]
|
|
type ChunkPageSize: Get<u32>;
|
|
|
|
/// Maximum number of people included in a ring before a new one is created.
|
|
#[pezpallet::constant]
|
|
type MaxRingSize: Get<u32>;
|
|
|
|
/// Maximum number of people included in an onboarding queue page before a new one is
|
|
/// created.
|
|
#[pezpallet::constant]
|
|
type OnboardingQueuePageSize: Get<u32>;
|
|
|
|
/// Helper for benchmarks.
|
|
#[cfg(feature = "runtime-benchmarks")]
|
|
type BenchmarkHelper: BenchmarkHelper<<Self::Crypto as GenerateVerifiable>::StaticChunk>;
|
|
}
|
|
|
|
/// The current individuals we recognise.
|
|
#[pezpallet::storage]
|
|
pub type Root<T> = StorageMap<_, Blake2_128Concat, RingIndex, RingRoot<T>>;
|
|
|
|
/// Keeps track of the ring index currently being populated.
|
|
#[pezpallet::storage]
|
|
pub type CurrentRingIndex<T: Config> = StorageValue<_, u32, ValueQuery>;
|
|
|
|
/// Maximum number of people queued before onboarding to a ring.
|
|
#[pezpallet::storage]
|
|
pub type OnboardingSize<T: Config> = 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<T: Config> = 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<T: Config> = StorageMap<
|
|
_,
|
|
Blake2_128Concat,
|
|
RingIndex,
|
|
BoundedVec<MemberOf<T>, 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<T: Config> =
|
|
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<T: Config> =
|
|
StorageMap<_, Twox64Concat, RingIndex, BoundedVec<u32, T::MaxRingSize>, ValueQuery>;
|
|
|
|
/// The number of people currently included in a ring.
|
|
#[pezpallet::storage]
|
|
pub type ActiveMembers<T: Config> = 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<T> = CountedStorageMap<_, Blake2_128Concat, MemberOf<T>, 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<T: Config> =
|
|
StorageMap<_, Blake2_128Concat, PersonalId, MemberOf<T>>;
|
|
|
|
/// 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<T: Config> =
|
|
StorageMap<_, Blake2_128Concat, PersonalId, PersonRecord<MemberOf<T>, T::AccountId>>;
|
|
|
|
/// Conversion of a contextual alias to an account ID.
|
|
#[pezpallet::storage]
|
|
pub type AliasToAccount<T> = StorageMap<
|
|
_,
|
|
Blake2_128Concat,
|
|
ContextualAlias,
|
|
<T as pezframe_system::Config>::AccountId,
|
|
OptionQuery,
|
|
>;
|
|
|
|
/// Conversion of an account ID to a contextual alias.
|
|
#[pezpallet::storage]
|
|
pub type AccountToAlias<T> = StorageMap<
|
|
_,
|
|
Blake2_128Concat,
|
|
<T as pezframe_system::Config>::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<T> = StorageMap<
|
|
_,
|
|
Blake2_128Concat,
|
|
<T as pezframe_system::Config>::AccountId,
|
|
PersonalId,
|
|
OptionQuery,
|
|
>;
|
|
|
|
/// Paginated collection of static chunks used by the verifiable crypto.
|
|
#[pezpallet::storage]
|
|
pub type Chunks<T> = StorageMap<_, Twox64Concat, PageIndex, ChunksOf<T>, OptionQuery>;
|
|
|
|
/// The next free and never reserved personal ID.
|
|
#[pezpallet::storage]
|
|
pub type NextPersonalId<T> = 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<T> = StorageValue<_, RingMembersState, ValueQuery>;
|
|
|
|
/// Candidates' reserved identities which we track.
|
|
#[pezpallet::storage]
|
|
pub type ReservedPersonalId<T: Config> =
|
|
StorageMap<_, Twox64Concat, PersonalId, (), OptionQuery>;
|
|
|
|
/// Keeps track of the page indices of the head and tail of the onboarding queue.
|
|
#[pezpallet::storage]
|
|
pub type QueuePageIndices<T: Config> = StorageValue<_, (PageIndex, PageIndex), ValueQuery>;
|
|
|
|
/// Paginated collection of people public keys ready to be included in a ring.
|
|
#[pezpallet::storage]
|
|
pub type OnboardingQueue<T> = StorageMap<
|
|
_,
|
|
Twox64Concat,
|
|
PageIndex,
|
|
BoundedVec<MemberOf<T>, <T as Config>::OnboardingQueuePageSize>,
|
|
ValueQuery,
|
|
>;
|
|
|
|
#[pezpallet::event]
|
|
#[pezpallet::generate_deposit(pub(super) fn deposit_event)]
|
|
pub enum Event<T: Config> {
|
|
/// An individual has had their personhood recognised and indexed.
|
|
PersonhoodRecognized { who: PersonalId, key: MemberOf<T> },
|
|
/// An individual has had their personhood recognised again and indexed.
|
|
PersonOnboarding { who: PersonalId, key: MemberOf<T> },
|
|
}
|
|
|
|
#[pezpallet::extra_constants]
|
|
impl<T: Config> Pezpallet<T> {
|
|
/// 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<T> {
|
|
600u32.into()
|
|
}
|
|
}
|
|
|
|
#[pezpallet::error]
|
|
pub enum Error<T> {
|
|
/// 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<T: Config> Hooks<BlockNumberFor<T>> for Pezpallet<T> {
|
|
fn integrity_test() {
|
|
assert!(
|
|
<T as Config>::ChunkPageSize::get() > 0,
|
|
"chunk page size must hold at least one element"
|
|
);
|
|
assert!(<T as Config>::MaxRingSize::get() > 0, "rings must hold at least one person");
|
|
assert!(
|
|
<T as Config>::MaxRingSize::get() <= <T as Config>::OnboardingQueuePageSize::get(),
|
|
"onboarding queue page size must greater than or equal to max ring size"
|
|
);
|
|
}
|
|
|
|
fn on_poll(_: BlockNumberFor<T>, 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::<T>::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::<T>::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<T>, 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::<T>::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::<T>::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::<T>::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<T: Config> {
|
|
pub encoded_chunks: Vec<u8>,
|
|
#[serde(skip)]
|
|
pub _phantom_data: core::marker::PhantomData<T>,
|
|
pub onboarding_size: u32,
|
|
}
|
|
|
|
impl<T: Config> Default for GenesisConfig<T> {
|
|
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<StaticChunk> = 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<T: Config> BuildGenesisConfig for GenesisConfig<T> {
|
|
fn build(&self) {
|
|
let chunks: Vec<<<T as Config>::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 = <T as Config>::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<T> = chunks[chunk_idx..chunk_idx_end]
|
|
.to_vec()
|
|
.try_into()
|
|
.expect("page size was checked against the array length; qed");
|
|
Chunks::<T>::insert(page_idx, chunk_page);
|
|
page_idx += 1;
|
|
chunk_idx = chunk_idx_end;
|
|
}
|
|
|
|
OnboardingSize::<T>::set(self.onboarding_size);
|
|
}
|
|
}
|
|
|
|
#[pezpallet::call(weight = <T as Config>::WeightInfo)]
|
|
impl<T: Config> Pezpallet<T> {
|
|
/// 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<T>,
|
|
ring_index: RingIndex,
|
|
limit: Option<u32>,
|
|
) -> 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::<T>::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::<T>::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::<T>::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::<T>::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::<T>::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<T>) -> 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::<T>::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::<T>::get();
|
|
let old_head = head;
|
|
let mut keys_to_include: Vec<MemberOf<T>> =
|
|
OnboardingQueue::<T>::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::<T>::take(head);
|
|
defensive_assert!(!second_key_page.is_empty());
|
|
keys_to_include.extend(second_key_page.into_iter());
|
|
}
|
|
|
|
let onboarding_size = OnboardingSize::<T>::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::<T>::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::<T>::get(&key).defensive().ok_or(Error::<T>::NotPerson)?;
|
|
let mut record =
|
|
People::<T>::get(personal_id).defensive().ok_or(Error::<T>::KeyNotFound)?;
|
|
record.position = RingPosition::Included {
|
|
ring_index: top_ring_index,
|
|
ring_position: keys.len().saturated_into(),
|
|
scheduled_for_removal: false,
|
|
};
|
|
People::<T>::insert(personal_id, record);
|
|
keys.try_push(key).map_err(|_| Error::<T>::TooManyMembers)?;
|
|
}
|
|
RingKeys::<T>::insert(top_ring_index, keys);
|
|
ActiveMembers::<T>::mutate(|active| *active = active.saturating_add(to_include));
|
|
ring_status.total = ring_status.total.saturating_add(to_include);
|
|
RingKeysStatus::<T>::insert(top_ring_index, ring_status);
|
|
|
|
// Update the top ring index if this onboarding round filled the current ring.
|
|
if ring_filled {
|
|
CurrentRingIndex::<T>::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<MemberOf<T>, T::OnboardingQueuePageSize> =
|
|
remaining_keys
|
|
.split_off(split_idx)
|
|
.try_into()
|
|
.expect("the list shrunk so it must fit; qed");
|
|
let remaining_keys: BoundedVec<MemberOf<T>, T::OnboardingQueuePageSize> =
|
|
remaining_keys.try_into().expect("the list shrunk so it must fit; qed");
|
|
OnboardingQueue::<T>::insert(old_head, remaining_keys);
|
|
OnboardingQueue::<T>::insert(head, second_page_keys);
|
|
QueuePageIndices::<T>::put((old_head, tail));
|
|
} else if !remaining_keys.is_empty() {
|
|
let remaining_keys: BoundedVec<MemberOf<T>, T::OnboardingQueuePageSize> =
|
|
remaining_keys.try_into().expect("the list shrunk so it must fit; qed");
|
|
OnboardingQueue::<T>::insert(head, remaining_keys);
|
|
QueuePageIndices::<T>::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::<T>::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<T>,
|
|
base_ring_index: RingIndex,
|
|
target_ring_index: RingIndex,
|
|
) -> DispatchResultWithPostInfo {
|
|
let _ = ensure_signed(origin)?;
|
|
|
|
ensure!(RingsState::<T>::get().append_only(), Error::<T>::SuspensionSessionInProgress);
|
|
// Top ring that onboards new candidates cannot be merged. Identical rings cannot be
|
|
// merged.
|
|
let current_ring_index = CurrentRingIndex::<T>::get();
|
|
ensure!(
|
|
base_ring_index != target_ring_index &&
|
|
base_ring_index != current_ring_index &&
|
|
target_ring_index != current_ring_index,
|
|
Error::<T>::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::<T>::RingAboveMergeThreshold
|
|
);
|
|
ensure!(
|
|
PendingSuspensions::<T>::decode_len(base_ring_index).unwrap_or(0) == 0,
|
|
Error::<T>::SuspensionsPending
|
|
);
|
|
let target_keys = RingKeys::<T>::get(target_ring_index);
|
|
RingKeysStatus::<T>::remove(target_ring_index);
|
|
ensure!(
|
|
target_keys.len() < T::MaxRingSize::get() as usize / 2,
|
|
Error::<T>::RingAboveMergeThreshold
|
|
);
|
|
ensure!(
|
|
PendingSuspensions::<T>::decode_len(target_ring_index).unwrap_or(0) == 0,
|
|
Error::<T>::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::<T>::get(&key).defensive().ok_or(Error::<T>::KeyNotFound)?;
|
|
let mut record =
|
|
People::<T>::get(personal_id).defensive().ok_or(Error::<T>::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::<T>::TooManyMembers)?;
|
|
People::<T>::insert(personal_id, record)
|
|
}
|
|
|
|
// Newly added keys are not yet included.
|
|
RingKeys::<T>::insert(base_ring_index, base_keys);
|
|
RingKeysStatus::<T>::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::<T>::remove(target_ring_index);
|
|
RingKeys::<T>::remove(target_ring_index);
|
|
RingKeysStatus::<T>::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<T>,
|
|
call: Box<<T as pezframe_system::Config>::RuntimeCall>,
|
|
) -> DispatchResultWithPostInfo {
|
|
let account = ensure_signed(origin.clone())?;
|
|
let rev_ca = AccountToAlias::<T>::get(&account).ok_or(Error::<T>::InvalidAccount)?;
|
|
ensure!(
|
|
Root::<T>::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<T>,
|
|
account: T::AccountId,
|
|
call_valid_at: BlockNumberFor<T>,
|
|
) -> DispatchResultWithPostInfo {
|
|
let rev_ca = Self::ensure_revised_personal_alias(origin)?;
|
|
let now = pezframe_system::Pezpallet::<T>::block_number();
|
|
let time_tolerance = Self::account_setup_time_tolerance();
|
|
ensure!(
|
|
call_valid_at <= now && now <= call_valid_at.saturating_add(time_tolerance),
|
|
Error::<T>::TimeOutOfRange
|
|
);
|
|
ensure!(T::AccountContexts::contains(&rev_ca.ca.context), Error::<T>::InvalidContext);
|
|
ensure!(!AccountToPersonalId::<T>::contains_key(&account), Error::<T>::AccountInUse);
|
|
|
|
let old_account = AliasToAccount::<T>::get(&rev_ca.ca);
|
|
let old_rev_ca = old_account.as_ref().and_then(AccountToAlias::<T>::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::<T>::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::<T>::contains_key(&account), Error::<T>::AccountInUse);
|
|
if let Some(old_account) = &old_account {
|
|
pezframe_system::Pezpallet::<T>::dec_sufficients(old_account);
|
|
AccountToAlias::<T>::remove(old_account);
|
|
}
|
|
pezframe_system::Pezpallet::<T>::inc_sufficients(&account);
|
|
}
|
|
|
|
AccountToAlias::<T>::insert(&account, &rev_ca);
|
|
AliasToAccount::<T>::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<T>) -> DispatchResult {
|
|
let alias = Self::ensure_personal_alias(origin)?;
|
|
let account = AliasToAccount::<T>::take(&alias).ok_or(Error::<T>::InvalidAccount)?;
|
|
AccountToAlias::<T>::remove(&account);
|
|
pezframe_system::Pezpallet::<T>::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<T>,
|
|
people: Vec<MemberOf<T>>,
|
|
) -> 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<T>,
|
|
account: T::AccountId,
|
|
call_valid_at: BlockNumberFor<T>,
|
|
) -> DispatchResultWithPostInfo {
|
|
let id = Self::ensure_personal_identity(origin)?;
|
|
let now = pezframe_system::Pezpallet::<T>::block_number();
|
|
let time_tolerance = Self::account_setup_time_tolerance();
|
|
ensure!(
|
|
call_valid_at <= now && now <= call_valid_at.saturating_add(time_tolerance),
|
|
Error::<T>::TimeOutOfRange
|
|
);
|
|
ensure!(!AccountToPersonalId::<T>::contains_key(&account), Error::<T>::AccountInUse);
|
|
ensure!(!AccountToAlias::<T>::contains_key(&account), Error::<T>::AccountInUse);
|
|
let mut record = People::<T>::get(id).ok_or(Error::<T>::NotPerson)?;
|
|
let pays = if let Some(old_account) = record.account {
|
|
pezframe_system::Pezpallet::<T>::dec_sufficients(&old_account);
|
|
AccountToPersonalId::<T>::remove(&old_account);
|
|
Pays::Yes
|
|
} else {
|
|
Pays::No
|
|
};
|
|
record.account = Some(account.clone());
|
|
pezframe_system::Pezpallet::<T>::inc_sufficients(&account);
|
|
AccountToPersonalId::<T>::insert(&account, id);
|
|
People::<T>::insert(id, &record);
|
|
|
|
Ok(pays.into())
|
|
}
|
|
|
|
/// Unset the personal id account.
|
|
#[pezpallet::call_index(5)]
|
|
pub fn unset_personal_id_account(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
|
|
let id = Self::ensure_personal_identity(origin)?;
|
|
let mut record = People::<T>::get(id).ok_or(Error::<T>::NotPerson)?;
|
|
let account = record.account.take().ok_or(Error::<T>::InvalidAccount)?;
|
|
AccountToPersonalId::<T>::take(&account).ok_or(Error::<T>::InvalidAccount)?;
|
|
pezframe_system::Pezpallet::<T>::dec_sufficients(&account);
|
|
People::<T>::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<T>,
|
|
new_key: MemberOf<T>,
|
|
) -> DispatchResultWithPostInfo {
|
|
let id = Self::ensure_personal_identity(origin)?;
|
|
ensure!(!Keys::<T>::contains_key(&new_key), Error::<T>::KeyAlreadyInUse);
|
|
let mut record = People::<T>::get(id).ok_or(Error::<T>::NotPerson)?;
|
|
ensure!(record.key != new_key, Error::<T>::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::<T>::get(id) {
|
|
Keys::<T>::remove(old_migrated_key);
|
|
}
|
|
// Add this new key to the migration queue.
|
|
KeyMigrationQueue::<T>::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::<T>::insert(id, record);
|
|
},
|
|
// This call accepts migrations only for included keys.
|
|
RingPosition::Onboarding { .. } =>
|
|
return Err(Error::<T>::InvalidKeyMigration.into()),
|
|
// Suspended people shouldn't be able to call this, but protect against this case
|
|
// anyway.
|
|
RingPosition::Suspended => return Err(Error::<T>::Suspended.into()),
|
|
}
|
|
Keys::<T>::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<T>,
|
|
new_key: MemberOf<T>,
|
|
) -> DispatchResultWithPostInfo {
|
|
let id = Self::ensure_personal_identity(origin)?;
|
|
ensure!(!Keys::<T>::contains_key(&new_key), Error::<T>::KeyAlreadyInUse);
|
|
let mut record = People::<T>::get(id).ok_or(Error::<T>::NotPerson)?;
|
|
ensure!(record.key != new_key, Error::<T>::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::<T>::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::<T>::remove(&keys[idx]);
|
|
// Update the key in the queue.
|
|
keys[idx] = new_key.clone();
|
|
OnboardingQueue::<T>::insert(queue_page, keys);
|
|
// Replace the key in the record.
|
|
record.key = new_key.clone();
|
|
// Update the record.
|
|
People::<T>::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::<T>::InvalidKeyMigration.into()),
|
|
// Suspended people shouldn't be able to call this, but protect against this case
|
|
// anyway.
|
|
RingPosition::Suspended => return Err(Error::<T>::Suspended.into()),
|
|
}
|
|
Keys::<T>::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<T>,
|
|
onboarding_size: u32,
|
|
) -> DispatchResultWithPostInfo {
|
|
ensure_root(origin)?;
|
|
ensure!(
|
|
onboarding_size <= <T as Config>::MaxRingSize::get(),
|
|
Error::<T>::InvalidOnboardingSize
|
|
);
|
|
OnboardingSize::<T>::put(onboarding_size);
|
|
Ok(Pays::No.into())
|
|
}
|
|
}
|
|
|
|
impl<T: Config> Pezpallet<T> {
|
|
/// 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<u32> {
|
|
// Ring root cannot be built while there are people to remove.
|
|
if !RingsState::<T>::get().append_only() {
|
|
return None;
|
|
}
|
|
// Suspended people should be removed from the ring before building it.
|
|
if PendingSuspensions::<T>::contains_key(ring_index) {
|
|
return None;
|
|
}
|
|
|
|
let ring_status = RingKeysStatus::<T>::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::<T>::get().append_only() {
|
|
return None;
|
|
}
|
|
|
|
// Suspended people should be removed from the ring before building it.
|
|
if PendingSuspensions::<T>::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::<T>::get().append_only() {
|
|
return false;
|
|
}
|
|
let suspended_count = PendingSuspensions::<T>::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<T> {
|
|
let (initial_head, tail) = QueuePageIndices::<T>::get();
|
|
let first_key_page = OnboardingQueue::<T>::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::<T>::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::<T>::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::<T>::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::<T>::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::<T>::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::<T>::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::<T>::get();
|
|
let old_head = head;
|
|
let mut keys_to_include: Vec<MemberOf<T>> =
|
|
OnboardingQueue::<T>::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::<T>::take(head);
|
|
defensive_assert!(!second_key_page.is_empty());
|
|
keys_to_include.extend(second_key_page.into_iter());
|
|
}
|
|
|
|
let onboarding_size = OnboardingSize::<T>::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::<T>::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::<T>::get(&key).defensive().ok_or(Error::<T>::NotPerson)?;
|
|
let mut record =
|
|
People::<T>::get(personal_id).defensive().ok_or(Error::<T>::KeyNotFound)?;
|
|
record.position = RingPosition::Included {
|
|
ring_index: top_ring_index,
|
|
ring_position: keys.len().saturated_into(),
|
|
scheduled_for_removal: false,
|
|
};
|
|
People::<T>::insert(personal_id, record);
|
|
keys.try_push(key).defensive().map_err(|_| Error::<T>::TooManyMembers)?;
|
|
}
|
|
RingKeys::<T>::insert(top_ring_index, keys);
|
|
ActiveMembers::<T>::mutate(|active| *active = active.saturating_add(to_include));
|
|
ring_status.total = ring_status.total.saturating_add(to_include);
|
|
RingKeysStatus::<T>::insert(top_ring_index, ring_status);
|
|
|
|
// Update the top ring index if this onboarding round filled the current ring.
|
|
if ring_filled {
|
|
CurrentRingIndex::<T>::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<MemberOf<T>, T::OnboardingQueuePageSize> =
|
|
remaining_keys
|
|
.split_off(split_idx)
|
|
.try_into()
|
|
.expect("the list shrunk so it must fit; qed");
|
|
let remaining_keys: BoundedVec<MemberOf<T>, T::OnboardingQueuePageSize> =
|
|
remaining_keys.try_into().expect("the list shrunk so it must fit; qed");
|
|
OnboardingQueue::<T>::insert(old_head, remaining_keys);
|
|
OnboardingQueue::<T>::insert(head, second_page_keys);
|
|
QueuePageIndices::<T>::put((old_head, tail));
|
|
} else if !remaining_keys.is_empty() {
|
|
let remaining_keys: BoundedVec<MemberOf<T>, T::OnboardingQueuePageSize> =
|
|
remaining_keys.try_into().expect("the list shrunk so it must fit; qed");
|
|
OnboardingQueue::<T>::insert(head, remaining_keys);
|
|
QueuePageIndices::<T>::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::<T>::put((head, tail));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn derivative_call(
|
|
mut origin: OriginFor<T>,
|
|
local_origin: Origin,
|
|
call: <T as pezframe_system::Config>::RuntimeCall,
|
|
derivation_weight: Weight,
|
|
) -> DispatchResultWithPostInfo {
|
|
origin.set_caller_from(<T::RuntimeOrigin as OriginTrait>::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<PersonalId, DispatchError> {
|
|
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<ContextualAlias, DispatchError> {
|
|
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<RevisedContextualAlias, DispatchError> {
|
|
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<MemberOf<T>, T::MaxRingSize>) {
|
|
let mut current_ring_index = CurrentRingIndex::<T>::get();
|
|
let mut current_keys = RingKeys::<T>::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::<T>::put(current_ring_index);
|
|
current_keys = RingKeys::<T>::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<T>) -> DispatchResult {
|
|
// If the key is already in use by another person then error.
|
|
ensure!(!Keys::<T>::contains_key(&key), Error::<T>::KeyAlreadyInUse);
|
|
// This is a first time key, so it must be reserved.
|
|
ensure!(
|
|
ReservedPersonalId::<T>::take(who).is_some(),
|
|
Error::<T>::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::<T>::get().mutating(), Error::<T>::NoMutationSession);
|
|
for who in suspensions {
|
|
let mut record = People::<T>::get(who).ok_or(Error::<T>::InvalidSuspensions)?;
|
|
match record.position {
|
|
RingPosition::Included { ring_index, ring_position, .. } => {
|
|
let mut suspended_indices = PendingSuspensions::<T>::get(ring_index);
|
|
let Err(insert_idx) = suspended_indices.binary_search(&ring_position)
|
|
else {
|
|
return Err(Error::<T>::KeyAlreadySuspended.into());
|
|
};
|
|
suspended_indices
|
|
.try_insert(insert_idx, ring_position)
|
|
.defensive()
|
|
.map_err(|_| Error::<T>::TooManyMembers)?;
|
|
PendingSuspensions::<T>::insert(ring_index, suspended_indices);
|
|
},
|
|
RingPosition::Onboarding { queue_page } => {
|
|
let mut keys = OnboardingQueue::<T>::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::<T>::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::<T>::remove(account);
|
|
record.account = None;
|
|
}
|
|
|
|
People::<T>::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::<T>::get(who).ok_or(Error::<T>::NotPerson)?;
|
|
ensure!(record.position.suspended(), Error::<T>::NotSuspended);
|
|
ensure!(Keys::<T>::get(&record.key) == Some(who), Error::<T>::NoKey);
|
|
|
|
Self::push_to_onboarding_queue(who, record.key, record.account)
|
|
}
|
|
|
|
fn push_to_onboarding_queue(
|
|
who: PersonalId,
|
|
key: MemberOf<T>,
|
|
account: Option<T::AccountId>,
|
|
) -> DispatchResult {
|
|
let (head, mut tail) = QueuePageIndices::<T>::get();
|
|
let mut keys = OnboardingQueue::<T>::get(tail);
|
|
if let Err(k) = keys.try_push(key.clone()) {
|
|
tail = tail.checked_add(1).unwrap_or(0);
|
|
ensure!(tail != head, Error::<T>::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::<T>::insert(&record.key, who);
|
|
People::<T>::insert(who, &record);
|
|
Self::deposit_event(Event::<T>::PersonOnboarding { who, key: record.key });
|
|
|
|
QueuePageIndices::<T>::put((head, tail));
|
|
OnboardingQueue::<T>::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<MemberOf<T>, T::MaxRingSize>, RingStatus) {
|
|
let keys = RingKeys::<T>::get(ring_index);
|
|
let ring_status = RingKeysStatus::<T>::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<usize>,
|
|
) -> Result<Vec<<T::Crypto as GenerateVerifiable>::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::<T>::get(page_idx.saturated_into::<u32>())
|
|
.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::<T>::get(page_idx.saturated_into::<u32>()).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::<T>::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::<bool, DispatchError, _>(|| match drain.next() {
|
|
Some((id, new_key)) =>
|
|
Self::migrate_keys_single_included_key(id, new_key).map(|_| false),
|
|
None => {
|
|
let rings_state = RingsState::<T>::get()
|
|
.end_key_migration()
|
|
.map_err(|_| Error::<T>::NoMutationSession)?;
|
|
RingsState::<T>::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<T>,
|
|
) -> DispatchResult {
|
|
if let Some(record) = People::<T>::get(id) {
|
|
let RingPosition::Included {
|
|
ring_index,
|
|
ring_position,
|
|
scheduled_for_removal: true,
|
|
} = record.position
|
|
else {
|
|
Keys::<T>::remove(new_key);
|
|
return Ok(());
|
|
};
|
|
let mut suspended_indices = PendingSuspensions::<T>::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::<T>::TooManyMembers)?;
|
|
PendingSuspensions::<T>::insert(ring_index, suspended_indices);
|
|
Keys::<T>::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::<T>::get(ring_index);
|
|
let keys_len = keys.len();
|
|
let suspended_indices = PendingSuspensions::<T>::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<MemberOf<T>, 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::<T>::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::<T>::mutate(|active| *active = active.saturating_sub(suspended_count));
|
|
RingKeys::<T>::insert(ring_index, new_keys);
|
|
Root::<T>::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::<T>::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<MemberOf<T>, T::OnboardingQueuePageSize>,
|
|
second_key_page: BoundedVec<MemberOf<T>, 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::<T>::get(key).defensive().ok_or(Error::<T>::NotPerson)?;
|
|
let mut record =
|
|
People::<T>::get(personal_id).defensive().ok_or(Error::<T>::KeyNotFound)?;
|
|
record.position = RingPosition::Onboarding { queue_page: new_head };
|
|
People::<T>::insert(personal_id, record);
|
|
}
|
|
|
|
first_key_page
|
|
.try_extend(second_key_page.into_iter())
|
|
.defensive()
|
|
.map_err(|_| Error::<T>::TooManyMembers)?;
|
|
OnboardingQueue::<T>::remove(initial_head);
|
|
OnboardingQueue::<T>::insert(new_head, first_key_page);
|
|
QueuePageIndices::<T>::mutate(|(h, _)| *h = new_head);
|
|
Ok(())
|
|
});
|
|
if let Err(e) = op_res {
|
|
log::error!(target: LOG_TARGET, "failed to merge queue pages: {:?}", e);
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<T: Config> AddOnlyPeopleTrait for Pezpallet<T> {
|
|
type Member = MemberOf<T>;
|
|
|
|
fn reserve_new_id() -> PersonalId {
|
|
let new_id = NextPersonalId::<T>::mutate(|id| {
|
|
let new_id = *id;
|
|
id.saturating_inc();
|
|
new_id
|
|
});
|
|
ReservedPersonalId::<T>::insert(new_id, ());
|
|
new_id
|
|
}
|
|
|
|
fn cancel_id_reservation(personal_id: PersonalId) -> Result<(), DispatchError> {
|
|
ReservedPersonalId::<T>::take(personal_id).ok_or(Error::<T>::PersonalIdNotReserved)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn renew_id_reservation(personal_id: PersonalId) -> Result<(), DispatchError> {
|
|
if NextPersonalId::<T>::get() <= personal_id ||
|
|
People::<T>::contains_key(personal_id) ||
|
|
ReservedPersonalId::<T>::contains_key(personal_id)
|
|
{
|
|
return Err(Error::<T>::PersonalIdReservationCannotRenew.into());
|
|
}
|
|
ReservedPersonalId::<T>::insert(personal_id, ());
|
|
Ok(())
|
|
}
|
|
|
|
fn recognize_personhood(
|
|
who: PersonalId,
|
|
maybe_key: Option<MemberOf<T>>,
|
|
) -> Result<(), DispatchError> {
|
|
match maybe_key {
|
|
Some(key) => Self::do_insert_key(who, key),
|
|
None => Self::resume_personhood(who),
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "runtime-benchmarks")]
|
|
type Secret = <<T as Config>::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::<PersonalId>()].copy_from_slice(&who.to_le_bytes()[..]);
|
|
let secret = T::Crypto::new_secret(buf);
|
|
(T::Crypto::member_from_secret(&secret), secret)
|
|
}
|
|
}
|
|
|
|
impl<T: Config> PeopleTrait for Pezpallet<T> {
|
|
fn suspend_personhood(suspensions: &[PersonalId]) -> DispatchResult {
|
|
Self::queue_personhood_suspensions(suspensions)
|
|
}
|
|
fn start_people_set_mutation_session() -> DispatchResult {
|
|
let current_state = RingsState::<T>::get();
|
|
RingsState::<T>::put(
|
|
current_state
|
|
.start_mutation_session()
|
|
.map_err(|_| Error::<T>::CouldNotStartMutationSession)?,
|
|
);
|
|
Ok(())
|
|
}
|
|
fn end_people_set_mutation_session() -> DispatchResult {
|
|
let current_state = RingsState::<T>::get();
|
|
RingsState::<T>::put(
|
|
current_state
|
|
.end_mutation_session()
|
|
.map_err(|_| Error::<T>::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<OuterOrigin>(o: OuterOrigin) -> Result<PersonalId, BadOrigin>
|
|
where
|
|
OuterOrigin: TryInto<Origin, Error = OuterOrigin>,
|
|
{
|
|
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<OuterOrigin>(o: OuterOrigin) -> Result<ContextualAlias, BadOrigin>
|
|
where
|
|
OuterOrigin: TryInto<Origin, Error = OuterOrigin>,
|
|
{
|
|
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<T>(PhantomData<T>);
|
|
impl<T: Config> EnsureOrigin<OriginFor<T>> for EnsurePersonalIdentity<T> {
|
|
type Success = PersonalId;
|
|
|
|
fn try_origin(o: OriginFor<T>) -> Result<Self::Success, OriginFor<T>> {
|
|
ensure_personal_identity(o.clone().into_caller()).map_err(|_| o)
|
|
}
|
|
|
|
#[cfg(feature = "runtime-benchmarks")]
|
|
fn try_successful_origin() -> Result<OriginFor<T>, ()> {
|
|
Ok(Origin::PersonalIdentity(0).into())
|
|
}
|
|
}
|
|
|
|
pezframe_support::impl_ensure_origin_with_arg_ignoring_arg! {
|
|
impl<{ T: Config, A }>
|
|
EnsureOriginWithArg< OriginFor<T>, A> for EnsurePersonalIdentity<T>
|
|
{}
|
|
}
|
|
|
|
impl<T: Config> CountedMembers for EnsurePersonalIdentity<T> {
|
|
fn active_count(&self) -> u32 {
|
|
Keys::<T>::count()
|
|
}
|
|
}
|
|
|
|
/// Guard to ensure that the given origin is a person. The contextual alias of the person is
|
|
/// provided on success.
|
|
pub struct EnsurePersonalAlias<T>(PhantomData<T>);
|
|
impl<T: Config> EnsureOrigin<OriginFor<T>> for EnsurePersonalAlias<T> {
|
|
type Success = ContextualAlias;
|
|
|
|
fn try_origin(o: OriginFor<T>) -> Result<Self::Success, OriginFor<T>> {
|
|
ensure_personal_alias(o.clone().into_caller()).map_err(|_| o)
|
|
}
|
|
|
|
#[cfg(feature = "runtime-benchmarks")]
|
|
fn try_successful_origin() -> Result<OriginFor<T>, ()> {
|
|
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<T>, A> for EnsurePersonalAlias<T>
|
|
{}
|
|
}
|
|
|
|
impl<T: Config> CountedMembers for EnsurePersonalAlias<T> {
|
|
fn active_count(&self) -> u32 {
|
|
ActiveMembers::<T>::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<T>(PhantomData<T>);
|
|
impl<T: Config> EnsureOriginWithArg<OriginFor<T>, Context> for EnsurePersonalAliasInContext<T> {
|
|
type Success = Alias;
|
|
|
|
fn try_origin(o: OriginFor<T>, arg: &Context) -> Result<Self::Success, OriginFor<T>> {
|
|
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<OriginFor<T>, ()> {
|
|
Ok(Origin::PersonalAlias(RevisedContextualAlias {
|
|
revision: 0,
|
|
ring: 0,
|
|
ca: ContextualAlias { alias: [1; 32], context: *context },
|
|
})
|
|
.into())
|
|
}
|
|
}
|
|
|
|
impl<T: Config> CountedMembers for EnsurePersonalAliasInContext<T> {
|
|
fn active_count(&self) -> u32 {
|
|
ActiveMembers::<T>::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<OuterOrigin>(
|
|
o: OuterOrigin,
|
|
) -> Result<RevisedContextualAlias, BadOrigin>
|
|
where
|
|
OuterOrigin: TryInto<Origin, Error = OuterOrigin>,
|
|
{
|
|
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<T>(PhantomData<T>);
|
|
impl<T: Config> EnsureOrigin<OriginFor<T>> for EnsureRevisedPersonalAlias<T> {
|
|
type Success = RevisedContextualAlias;
|
|
|
|
fn try_origin(o: OriginFor<T>) -> Result<Self::Success, OriginFor<T>> {
|
|
ensure_revised_personal_alias(o.clone().into_caller()).map_err(|_| o)
|
|
}
|
|
|
|
#[cfg(feature = "runtime-benchmarks")]
|
|
fn try_successful_origin() -> Result<OriginFor<T>, ()> {
|
|
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<T>, A> for EnsureRevisedPersonalAlias<T>
|
|
{}
|
|
}
|
|
|
|
impl<T: Config> CountedMembers for EnsureRevisedPersonalAlias<T> {
|
|
fn active_count(&self) -> u32 {
|
|
ActiveMembers::<T>::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<T>(PhantomData<T>);
|
|
impl<T: Config> EnsureOriginWithArg<OriginFor<T>, Context>
|
|
for EnsureRevisedPersonalAliasInContext<T>
|
|
{
|
|
type Success = RevisedAlias;
|
|
|
|
fn try_origin(o: OriginFor<T>, arg: &Context) -> Result<Self::Success, OriginFor<T>> {
|
|
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<OriginFor<T>, ()> {
|
|
Ok(Origin::PersonalAlias(RevisedContextualAlias {
|
|
revision: 0,
|
|
ring: 0,
|
|
ca: ContextualAlias { alias: [1; 32], context: *context },
|
|
})
|
|
.into())
|
|
}
|
|
}
|
|
|
|
impl<T: Config> CountedMembers for EnsureRevisedPersonalAliasInContext<T> {
|
|
fn active_count(&self) -> u32 {
|
|
ActiveMembers::<T>::get()
|
|
}
|
|
}
|
|
}
|