Unique Usernames in Identity Pallet (#2651)

This PR allows _username authorities_ to issue unique usernames that
correspond with an account. It also provides two-way lookup, that is
from `AccountId` to a single, "primary" `Username` (alongside
`Registration`) and multiple unique `Username`s to an `AccountId`.

Key features:

- Username Authorities added (and removed) via privileged origin.
- Authorities have a `suffix` and an `allocation`. They can grant up to
`allocation` usernames. Their `suffix` will be appended to the usernames
that they issue. A suffix may be up to 7 characters long.
- Users can ask an authority to grant them a username. This will take
the form `myusername.suffix`. The entire name (including suffix) must be
less than or equal to 32 alphanumeric characters.
- Users can approve a username for themselves in one of two ways (that
is, authorities cannot grant them arbitrarily):
- Pre-sign the entire username (including suffix) with a secret key that
corresponds to their `AccountId` (for keyed accounts, obviously); or
- Accept the username after it has been granted by an authority (it will
be queued until accepted) (for non-keyed accounts like pure proxies or
multisigs).
- The system does not require any funds or deposits. Users without an
identity will be given a default one (presumably all fields set to
`None`). If they update this info, they will need to place the normal
storage deposit.
- If a user does not have any username, their first one will be set as
`Primary`, and their `AccountId` will map to that one. If they get
subsequent usernames, they can choose which one to be their primary via
`set_primary_username`.
- There are some state cleanup functions to remove expired usernames
that have not been accepted and dangling usernames whose owners have
called `clear_identity`.

TODO:

- [x] Add migration to runtimes
- [x] Probably do off-chain migration into People Chain genesis
- [x] Address a few TODO questions in code (please review)

---------

Co-authored-by: Liam Aharon <liam.aharon@hotmail.com>
Co-authored-by: Gonçalo Pestana <g6pestana@gmail.com>
Co-authored-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>
Co-authored-by: Dónal Murray <donal.murray@parity.io>
This commit is contained in:
joe petrowski
2024-01-10 11:30:00 +01:00
committed by GitHub
parent a4195326b9
commit d1f678c0ec
22 changed files with 2656 additions and 331 deletions
+459 -54
View File
@@ -40,32 +40,53 @@
//! The number of registrars should be limited, and the deposit made sufficiently large, to ensure
//! no state-bloat attack is viable.
//!
//! ### Usernames
//!
//! The pallet provides functionality for username authorities to issue usernames. When an account
//! receives a username, they get a default instance of `IdentityInfo`. Usernames also serve as a
//! reverse lookup from username to account.
//!
//! Username authorities are given an allocation by governance to prevent state bloat. Usernames
//! impose no cost or deposit on the user.
//!
//! Users can have multiple usernames that map to the same `AccountId`, however one `AccountId` can
//! only map to a single username, known as the _primary_.
//!
//! ## Interface
//!
//! ### Dispatchable Functions
//!
//! #### For general users
//! #### For General Users
//! * `set_identity` - Set the associated identity of an account; a small deposit is reserved if not
//! already taken.
//! * `clear_identity` - Remove an account's associated identity; the deposit is returned.
//! * `request_judgement` - Request a judgement from a registrar, paying a fee.
//! * `cancel_request` - Cancel the previous request for a judgement.
//! * `accept_username` - Accept a username issued by a username authority.
//! * `remove_expired_approval` - Remove a username that was issued but never accepted.
//! * `set_primary_username` - Set a given username as an account's primary.
//! * `remove_dangling_username` - Remove a username that maps to an account without an identity.
//!
//! #### For general users with sub-identities
//! #### For General Users with Sub-Identities
//! * `set_subs` - Set the sub-accounts of an identity.
//! * `add_sub` - Add a sub-identity to an identity.
//! * `remove_sub` - Remove a sub-identity of an identity.
//! * `rename_sub` - Rename a sub-identity of an identity.
//! * `quit_sub` - Remove a sub-identity of an identity (called by the sub-identity).
//!
//! #### For registrars
//! #### For Registrars
//! * `set_fee` - Set the fee required to be paid for a judgement to be given by the registrar.
//! * `set_fields` - Set the fields that a registrar cares about in their judgements.
//! * `provide_judgement` - Provide a judgement to an identity.
//!
//! #### For super-users
//! #### For Username Authorities
//! * `set_username_for` - Set a username for a given account. The account must approve it.
//!
//! #### For Superusers
//! * `add_registrar` - Add a new registrar to the system.
//! * `kill_identity` - Forcibly remove the associated identity; the deposit is lost.
//! * `add_username_authority` - Add an account with the ability to issue usernames.
//! * `remove_username_authority` - Remove an account with the ability to issue usernames.
//!
//! [`Call`]: ./enum.Call.html
//! [`Config`]: ./trait.Config.html
@@ -74,25 +95,29 @@
mod benchmarking;
pub mod legacy;
pub mod migration;
#[cfg(test)]
mod tests;
mod types;
pub mod weights;
use crate::types::{AuthorityPropertiesOf, Suffix, Username};
use codec::Encode;
use frame_support::{
ensure,
pallet_prelude::{DispatchError, DispatchResult},
traits::{BalanceStatus, Currency, Get, OnUnbalanced, ReservableCurrency},
traits::{BalanceStatus, Currency, Get, OnUnbalanced, ReservableCurrency, StorageVersion},
BoundedVec,
};
use sp_runtime::traits::{AppendZerosInput, Hash, Saturating, StaticLookup, Zero};
use sp_std::prelude::*;
pub use weights::WeightInfo;
pub use pallet::*;
use sp_runtime::traits::{
AppendZerosInput, Hash, IdentifyAccount, Saturating, StaticLookup, Verify, Zero,
};
use sp_std::prelude::*;
pub use types::{
Data, IdentityInformationProvider, Judgement, RegistrarIndex, RegistrarInfo, Registration,
};
pub use weights::WeightInfo;
type BalanceOf<T> =
<<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
@@ -115,7 +140,7 @@ pub mod pallet {
/// The currency trait.
type Currency: ReservableCurrency<Self::AccountId>;
/// The amount held on deposit for a registered identity
/// The amount held on deposit for a registered identity.
#[pallet::constant]
type BasicDeposit: Get<BalanceOf<Self>>;
@@ -150,14 +175,41 @@ pub mod pallet {
/// The origin which may add or remove registrars. Root can always do this.
type RegistrarOrigin: EnsureOrigin<Self::RuntimeOrigin>;
/// Signature type for pre-authorizing usernames off-chain.
///
/// Can verify whether an `Self::SigningPublicKey` created a signature.
type OffchainSignature: Verify<Signer = Self::SigningPublicKey> + Parameter;
/// Public key that corresponds to an on-chain `Self::AccountId`.
type SigningPublicKey: IdentifyAccount<AccountId = Self::AccountId>;
/// The origin which may add or remove username authorities. Root can always do this.
type UsernameAuthorityOrigin: EnsureOrigin<Self::RuntimeOrigin>;
/// The number of blocks within which a username grant must be accepted.
#[pallet::constant]
type PendingUsernameExpiration: Get<BlockNumberFor<Self>>;
/// The maximum length of a suffix.
#[pallet::constant]
type MaxSuffixLength: Get<u32>;
/// The maximum length of a username, including its suffix and any system-added delimiters.
#[pallet::constant]
type MaxUsernameLength: Get<u32>;
/// Weight information for extrinsics in this pallet.
type WeightInfo: WeightInfo;
}
const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
#[pallet::pallet]
#[pallet::storage_version(STORAGE_VERSION)]
pub struct Pallet<T>(_);
/// Information that is pertinent to identify the entity behind an account.
/// Information that is pertinent to identify the entity behind an account. First item is the
/// registration, second is the account's primary username.
///
/// TWOX-NOTE: OK ― `AccountId` is a secure hash.
#[pallet::storage]
@@ -166,7 +218,7 @@ pub mod pallet {
_,
Twox64Concat,
T::AccountId,
Registration<BalanceOf<T>, T::MaxRegistrars, T::IdentityInformation>,
(Registration<BalanceOf<T>, T::MaxRegistrars, T::IdentityInformation>, Option<Username<T>>),
OptionQuery,
>;
@@ -213,6 +265,38 @@ pub mod pallet {
ValueQuery,
>;
/// A map of the accounts who are authorized to grant usernames.
#[pallet::storage]
#[pallet::getter(fn authority)]
pub(super) type UsernameAuthorities<T: Config> =
StorageMap<_, Twox64Concat, T::AccountId, AuthorityPropertiesOf<T>, OptionQuery>;
/// Reverse lookup from `username` to the `AccountId` that has registered it. The value should
/// be a key in the `IdentityOf` map, but it may not if the user has cleared their identity.
///
/// Multiple usernames may map to the same `AccountId`, but `IdentityOf` will only map to one
/// primary username.
#[pallet::storage]
#[pallet::getter(fn username)]
pub(super) type AccountOfUsername<T: Config> =
StorageMap<_, Blake2_128Concat, Username<T>, T::AccountId, OptionQuery>;
/// Usernames that an authority has granted, but that the account controller has not confirmed
/// that they want it. Used primarily in cases where the `AccountId` cannot provide a signature
/// because they are a pure proxy, multisig, etc. In order to confirm it, they should call
/// [`Call::accept_username`].
///
/// First tuple item is the account and second is the acceptance deadline.
#[pallet::storage]
#[pallet::getter(fn preapproved_usernames)]
pub type PendingUsernames<T: Config> = StorageMap<
_,
Blake2_128Concat,
Username<T>,
(T::AccountId, BlockNumberFor<T>),
OptionQuery,
>;
#[pallet::error]
pub enum Error<T> {
/// Too many subs-accounts.
@@ -249,6 +333,24 @@ pub mod pallet {
JudgementForDifferentIdentity,
/// Error that occurs when there is an issue paying for judgement.
JudgementPaymentFailed,
/// The provided suffix is too long.
InvalidSuffix,
/// The sender does not have permission to issue a username.
NotUsernameAuthority,
/// The authority cannot allocate any more usernames.
NoAllocation,
/// The signature on a username was not valid.
InvalidSignature,
/// Setting this username requires a signature, but none was provided.
RequiresSignature,
/// The username does not meet the requirements.
InvalidUsername,
/// The username is already taken.
UsernameTaken,
/// The requested username does not exist.
NoUsername,
/// The username cannot be forcefully removed because it can still be accepted.
NotExpired,
}
#[pallet::event]
@@ -275,6 +377,21 @@ pub mod pallet {
/// A sub-identity was cleared, and the given deposit repatriated from the
/// main identity account to the sub-identity account.
SubIdentityRevoked { sub: T::AccountId, main: T::AccountId, deposit: BalanceOf<T> },
/// A username authority was added.
AuthorityAdded { authority: T::AccountId },
/// A username authority was removed.
AuthorityRemoved { authority: T::AccountId },
/// A username was set for `who`.
UsernameSet { who: T::AccountId, username: Username<T> },
/// A username was queued, but `who` must accept it prior to `expiration`.
UsernameQueued { who: T::AccountId, username: Username<T>, expiration: BlockNumberFor<T> },
/// A queued username passed its expiration without being claimed and was removed.
PreapprovalExpired { whose: T::AccountId },
/// A username was set as a primary and can be looked up from `who`.
PrimaryUsernameSet { who: T::AccountId, username: Username<T> },
/// A dangling username (as in, a username corresponding to an account that has removed its
/// identity) has been removed.
DanglingUsernameRemoved { who: T::AccountId, username: Username<T> },
}
#[pallet::call]
@@ -331,36 +448,34 @@ pub mod pallet {
info: Box<T::IdentityInformation>,
) -> DispatchResultWithPostInfo {
let sender = ensure_signed(origin)?;
let encoded_byte_size = info.encoded_size() as u32;
let byte_deposit =
T::ByteDeposit::get().saturating_mul(<BalanceOf<T>>::from(encoded_byte_size));
let mut id = match <IdentityOf<T>>::get(&sender) {
Some(mut id) => {
// Only keep non-positive judgements.
id.judgements.retain(|j| j.1.is_sticky());
id.info = *info;
id
},
None => Registration {
info: *info,
judgements: BoundedVec::default(),
deposit: Zero::zero(),
},
let (mut id, username) = match <IdentityOf<T>>::get(&sender) {
Some((mut id, maybe_username)) => (
{
// Only keep non-positive judgements.
id.judgements.retain(|j| j.1.is_sticky());
id.info = *info;
id
},
maybe_username,
),
None => (
Registration {
info: *info,
judgements: BoundedVec::default(),
deposit: Zero::zero(),
},
None,
),
};
let new_deposit = Self::calculate_identity_deposit(&id.info);
let old_deposit = id.deposit;
id.deposit = T::BasicDeposit::get().saturating_add(byte_deposit);
if id.deposit > old_deposit {
T::Currency::reserve(&sender, id.deposit - old_deposit)?;
}
if old_deposit > id.deposit {
let err_amount = T::Currency::unreserve(&sender, old_deposit - id.deposit);
debug_assert!(err_amount.is_zero());
}
Self::rejig_deposit(&sender, old_deposit, new_deposit)?;
id.deposit = new_deposit;
let judgements = id.judgements.len();
<IdentityOf<T>>::insert(&sender, id);
<IdentityOf<T>>::insert(&sender, (id, username));
Self::deposit_event(Event::IdentitySet { who: sender });
Ok(Some(T::WeightInfo::set_identity(judgements as u32)).into())
@@ -452,11 +567,15 @@ pub mod pallet {
let sender = ensure_signed(origin)?;
let (subs_deposit, sub_ids) = <SubsOf<T>>::take(&sender);
let id = <IdentityOf<T>>::take(&sender).ok_or(Error::<T>::NotNamed)?;
let (id, maybe_username) =
<IdentityOf<T>>::take(&sender).ok_or(Error::<T>::NoIdentity)?;
let deposit = id.total_deposit().saturating_add(subs_deposit);
for sub in sub_ids.iter() {
<SuperOf<T>>::remove(sub);
}
if let Some(username) = maybe_username {
AccountOfUsername::<T>::remove(username);
}
let err_amount = T::Currency::unreserve(&sender, deposit);
debug_assert!(err_amount.is_zero());
@@ -501,7 +620,7 @@ pub mod pallet {
.and_then(Option::as_ref)
.ok_or(Error::<T>::EmptyIndex)?;
ensure!(max_fee >= registrar.fee, Error::<T>::FeeChanged);
let mut id = <IdentityOf<T>>::get(&sender).ok_or(Error::<T>::NoIdentity)?;
let (mut id, username) = <IdentityOf<T>>::get(&sender).ok_or(Error::<T>::NoIdentity)?;
let item = (reg_index, Judgement::FeePaid(registrar.fee));
match id.judgements.binary_search_by_key(&reg_index, |x| x.0) {
@@ -518,7 +637,7 @@ pub mod pallet {
T::Currency::reserve(&sender, registrar.fee)?;
let judgements = id.judgements.len();
<IdentityOf<T>>::insert(&sender, id);
<IdentityOf<T>>::insert(&sender, (id, username));
Self::deposit_event(Event::JudgementRequested {
who: sender,
@@ -545,7 +664,7 @@ pub mod pallet {
reg_index: RegistrarIndex,
) -> DispatchResultWithPostInfo {
let sender = ensure_signed(origin)?;
let mut id = <IdentityOf<T>>::get(&sender).ok_or(Error::<T>::NoIdentity)?;
let (mut id, username) = <IdentityOf<T>>::get(&sender).ok_or(Error::<T>::NoIdentity)?;
let pos = id
.judgements
@@ -560,7 +679,7 @@ pub mod pallet {
let err_amount = T::Currency::unreserve(&sender, fee);
debug_assert!(err_amount.is_zero());
let judgements = id.judgements.len();
<IdentityOf<T>>::insert(&sender, id);
<IdentityOf<T>>::insert(&sender, (id, username));
Self::deposit_event(Event::JudgementUnrequested {
who: sender,
@@ -679,6 +798,8 @@ pub mod pallet {
/// - `identity`: The hash of the [`IdentityInformationProvider`] for that the judgement is
/// provided.
///
/// Note: Judgements do not apply to a username.
///
/// Emits `JudgementGiven` if successful.
#[pallet::call_index(9)]
#[pallet::weight(T::WeightInfo::provide_judgement(T::MaxRegistrars::get()))]
@@ -697,7 +818,8 @@ pub mod pallet {
.and_then(Option::as_ref)
.filter(|r| r.account == sender)
.ok_or(Error::<T>::InvalidIndex)?;
let mut id = <IdentityOf<T>>::get(&target).ok_or(Error::<T>::InvalidTarget)?;
let (mut id, username) =
<IdentityOf<T>>::get(&target).ok_or(Error::<T>::InvalidTarget)?;
if T::Hashing::hash_of(&id.info) != identity {
return Err(Error::<T>::JudgementForDifferentIdentity.into())
@@ -724,7 +846,7 @@ pub mod pallet {
}
let judgements = id.judgements.len();
<IdentityOf<T>>::insert(&target, id);
<IdentityOf<T>>::insert(&target, (id, username));
Self::deposit_event(Event::JudgementGiven { target, registrar_index: reg_index });
Ok(Some(T::WeightInfo::provide_judgement(judgements as u32)).into())
@@ -757,11 +879,15 @@ pub mod pallet {
let target = T::Lookup::lookup(target)?;
// Grab their deposit (and check that they have one).
let (subs_deposit, sub_ids) = <SubsOf<T>>::take(&target);
let id = <IdentityOf<T>>::take(&target).ok_or(Error::<T>::NotNamed)?;
let (id, maybe_username) =
<IdentityOf<T>>::take(&target).ok_or(Error::<T>::NoIdentity)?;
let deposit = id.total_deposit().saturating_add(subs_deposit);
for sub in sub_ids.iter() {
<SuperOf<T>>::remove(sub);
}
if let Some(username) = maybe_username {
AccountOfUsername::<T>::remove(username);
}
// Slash their deposit from them.
T::Slashed::on_unbalanced(T::Currency::slash_reserved(&target, deposit).0);
@@ -886,6 +1012,186 @@ pub mod pallet {
});
Ok(())
}
/// Add an `AccountId` with permission to grant usernames with a given `suffix` appended.
///
/// The authority can grant up to `allocation` usernames. To top up their allocation, they
/// should just issue (or request via governance) a new `add_username_authority` call.
#[pallet::call_index(15)]
#[pallet::weight(T::WeightInfo::add_username_authority())]
pub fn add_username_authority(
origin: OriginFor<T>,
authority: AccountIdLookupOf<T>,
suffix: Vec<u8>,
allocation: u32,
) -> DispatchResult {
T::UsernameAuthorityOrigin::ensure_origin(origin)?;
let authority = T::Lookup::lookup(authority)?;
// We don't need to check the length because it gets checked when casting into a
// `BoundedVec`.
Self::validate_username(&suffix, None).map_err(|_| Error::<T>::InvalidSuffix)?;
let suffix = Suffix::<T>::try_from(suffix).map_err(|_| Error::<T>::InvalidSuffix)?;
// The authority may already exist, but we don't need to check. They might be changing
// their suffix or adding allocation, so we just want to overwrite whatever was there.
UsernameAuthorities::<T>::insert(
&authority,
AuthorityPropertiesOf::<T> { suffix, allocation },
);
Self::deposit_event(Event::AuthorityAdded { authority });
Ok(())
}
/// Remove `authority` from the username authorities.
#[pallet::call_index(16)]
#[pallet::weight(T::WeightInfo::remove_username_authority())]
pub fn remove_username_authority(
origin: OriginFor<T>,
authority: AccountIdLookupOf<T>,
) -> DispatchResult {
T::UsernameAuthorityOrigin::ensure_origin(origin)?;
let authority = T::Lookup::lookup(authority)?;
UsernameAuthorities::<T>::take(&authority).ok_or(Error::<T>::NotUsernameAuthority)?;
Self::deposit_event(Event::AuthorityRemoved { authority });
Ok(())
}
/// Set the username for `who`. Must be called by a username authority.
///
/// The authority must have an `allocation`. Users can either pre-sign their usernames or
/// accept them later.
///
/// Usernames must:
/// - Only contain lowercase ASCII characters or digits.
/// - When combined with the suffix of the issuing authority be _less than_ the
/// `MaxUsernameLength`.
#[pallet::call_index(17)]
#[pallet::weight(T::WeightInfo::set_username_for())]
pub fn set_username_for(
origin: OriginFor<T>,
who: AccountIdLookupOf<T>,
username: Vec<u8>,
signature: Option<T::OffchainSignature>,
) -> DispatchResult {
// Ensure origin is a Username Authority and has an allocation. Decrement their
// allocation by one.
let sender = ensure_signed(origin)?;
let suffix = UsernameAuthorities::<T>::try_mutate(
&sender,
|maybe_authority| -> Result<Suffix<T>, DispatchError> {
let properties =
maybe_authority.as_mut().ok_or(Error::<T>::NotUsernameAuthority)?;
ensure!(properties.allocation > 0, Error::<T>::NoAllocation);
properties.allocation.saturating_dec();
Ok(properties.suffix.clone())
},
)?;
// Ensure that the username only contains allowed characters. We already know the suffix
// does.
let username_length = username.len().saturating_add(suffix.len()) as u32;
Self::validate_username(&username, Some(username_length))?;
// Concatenate the username with suffix and cast into a BoundedVec. Should be infallible
// since we already ensured it is below the max length.
let mut full_username =
Vec::with_capacity(username.len().saturating_add(suffix.len()).saturating_add(1));
full_username.extend(username);
full_username.extend(b".");
full_username.extend(suffix);
let bounded_username =
Username::<T>::try_from(full_username).map_err(|_| Error::<T>::InvalidUsername)?;
// Usernames must be unique. Ensure it's not taken.
ensure!(
!AccountOfUsername::<T>::contains_key(&bounded_username),
Error::<T>::UsernameTaken
);
ensure!(
!PendingUsernames::<T>::contains_key(&bounded_username),
Error::<T>::UsernameTaken
);
// Insert or queue.
let who = T::Lookup::lookup(who)?;
if let Some(s) = signature {
// Account has pre-signed an authorization. Verify the signature provided and grant
// the username directly.
let encoded = Encode::encode(&bounded_username.to_vec());
Self::validate_signature(&encoded, &s, &who)?;
Self::insert_username(&who, bounded_username);
} else {
// The user must accept the username, therefore, queue it.
Self::queue_acceptance(&who, bounded_username);
}
Ok(())
}
/// Accept a given username that an `authority` granted. The call must include the full
/// username, as in `username.suffix`.
#[pallet::call_index(18)]
#[pallet::weight(T::WeightInfo::accept_username())]
pub fn accept_username(
origin: OriginFor<T>,
username: Username<T>,
) -> DispatchResultWithPostInfo {
let who = ensure_signed(origin)?;
let (approved_for, _) =
PendingUsernames::<T>::take(&username).ok_or(Error::<T>::NoUsername)?;
ensure!(approved_for == who.clone(), Error::<T>::InvalidUsername);
Self::insert_username(&who, username.clone());
Self::deposit_event(Event::UsernameSet { who: who.clone(), username });
Ok(Pays::No.into())
}
/// Remove an expired username approval. The username was approved by an authority but never
/// accepted by the user and must now be beyond its expiration. The call must include the
/// full username, as in `username.suffix`.
#[pallet::call_index(19)]
#[pallet::weight(T::WeightInfo::remove_expired_approval())]
pub fn remove_expired_approval(
origin: OriginFor<T>,
username: Username<T>,
) -> DispatchResultWithPostInfo {
let _ = ensure_signed(origin)?;
if let Some((who, expiration)) = PendingUsernames::<T>::take(&username) {
let now = frame_system::Pallet::<T>::block_number();
ensure!(now > expiration, Error::<T>::NotExpired);
Self::deposit_event(Event::PreapprovalExpired { whose: who.clone() });
Ok(Pays::No.into())
} else {
Err(Error::<T>::NoUsername.into())
}
}
/// Set a given username as the primary. The username should include the suffix.
#[pallet::call_index(20)]
#[pallet::weight(T::WeightInfo::set_primary_username())]
pub fn set_primary_username(origin: OriginFor<T>, username: Username<T>) -> DispatchResult {
// ensure `username` maps to `origin` (i.e. has already been set by an authority).
let who = ensure_signed(origin)?;
ensure!(AccountOfUsername::<T>::contains_key(&username), Error::<T>::NoUsername);
let (registration, _maybe_username) =
IdentityOf::<T>::get(&who).ok_or(Error::<T>::NoIdentity)?;
IdentityOf::<T>::insert(&who, (registration, Some(username.clone())));
Self::deposit_event(Event::PrimaryUsernameSet { who: who.clone(), username });
Ok(())
}
/// Remove a username that corresponds to an account with no identity. Exists when a user
/// gets a username but then calls `clear_identity`.
#[pallet::call_index(21)]
#[pallet::weight(T::WeightInfo::remove_dangling_username())]
pub fn remove_dangling_username(
origin: OriginFor<T>,
username: Username<T>,
) -> DispatchResultWithPostInfo {
// ensure `username` maps to `origin` (i.e. has already been set by an authority).
let _ = ensure_signed(origin)?;
let who = AccountOfUsername::<T>::take(&username).ok_or(Error::<T>::NoUsername)?;
ensure!(!IdentityOf::<T>::contains_key(&who), Error::<T>::InvalidUsername);
Self::deposit_event(Event::DanglingUsernameRemoved { who: who.clone(), username });
Ok(Pays::No.into())
}
}
}
@@ -925,7 +1231,104 @@ impl<T: Config> Pallet<T> {
fields: <T::IdentityInformation as IdentityInformationProvider>::FieldsIdentifier,
) -> bool {
IdentityOf::<T>::get(who)
.map_or(false, |registration| (registration.info.has_identity(fields)))
.map_or(false, |(registration, _username)| (registration.info.has_identity(fields)))
}
/// Calculate the deposit required for an identity.
fn calculate_identity_deposit(info: &T::IdentityInformation) -> BalanceOf<T> {
let bytes = info.encoded_size() as u32;
let byte_deposit = T::ByteDeposit::get().saturating_mul(<BalanceOf<T>>::from(bytes));
T::BasicDeposit::get().saturating_add(byte_deposit)
}
/// Validate that a username conforms to allowed characters/format.
///
/// The function will validate the characters in `username` and that `length` (if `Some`)
/// conforms to the limit. It is not expected to pass a fully formatted username here (i.e. one
/// with any protocol-added characters included, such as a `.`). The suffix is also separately
/// validated by this function to ensure the full username conforms.
fn validate_username(username: &Vec<u8>, length: Option<u32>) -> DispatchResult {
// Verify input length before allocating a Vec with the user's input. `<` instead of `<=`
// because it needs one element for the point (`username` + `.` + `suffix`).
if let Some(l) = length {
ensure!(l < T::MaxUsernameLength::get(), Error::<T>::InvalidUsername);
}
// Usernames cannot be empty.
ensure!(!username.is_empty(), Error::<T>::InvalidUsername);
// Username must be lowercase and alphanumeric.
ensure!(
username.iter().all(|byte| byte.is_ascii_digit() || byte.is_ascii_lowercase()),
Error::<T>::InvalidUsername
);
Ok(())
}
/// Validate a signature. Supports signatures on raw `data` or `data` wrapped in HTML `<Bytes>`.
pub fn validate_signature(
data: &Vec<u8>,
signature: &T::OffchainSignature,
signer: &T::AccountId,
) -> DispatchResult {
// Happy path, user has signed the raw data.
if signature.verify(&data[..], &signer) {
return Ok(())
}
// NOTE: for security reasons modern UIs implicitly wrap the data requested to sign into
// `<Bytes> + data + </Bytes>`, so why we support both wrapped and raw versions.
let prefix = b"<Bytes>";
let suffix = b"</Bytes>";
let mut wrapped: Vec<u8> = Vec::with_capacity(data.len() + prefix.len() + suffix.len());
wrapped.extend(prefix);
wrapped.extend(data);
wrapped.extend(suffix);
ensure!(signature.verify(&wrapped[..], &signer), Error::<T>::InvalidSignature);
Ok(())
}
/// A username has met all conditions. Insert the relevant storage items.
pub fn insert_username(who: &T::AccountId, username: Username<T>) {
// Check if they already have a primary. If so, leave it. If not, set it.
// Likewise, check if they have an identity. If not, give them a minimal one.
let (reg, primary_username, new_is_primary) = match <IdentityOf<T>>::get(&who) {
// User has an existing Identity and a primary username. Leave it.
Some((reg, Some(primary))) => (reg, primary, false),
// User has an Identity but no primary. Set the new one as primary.
Some((reg, None)) => (reg, username.clone(), true),
// User does not have an existing Identity. Give them a fresh default one and set
// their username as primary.
None => (
Registration {
info: Default::default(),
judgements: Default::default(),
deposit: Zero::zero(),
},
username.clone(),
true,
),
};
// Enter in identity map. Note: In the case that the user did not have a pre-existing
// Identity, we have given them the storage item for free. If they ever call
// `set_identity` with identity info, then they will need to place the normal identity
// deposit.
IdentityOf::<T>::insert(&who, (reg, Some(primary_username)));
// Enter in username map.
AccountOfUsername::<T>::insert(username.clone(), &who);
Self::deposit_event(Event::UsernameSet { who: who.clone(), username: username.clone() });
if new_is_primary {
Self::deposit_event(Event::PrimaryUsernameSet { who: who.clone(), username });
}
}
/// A username was granted by an authority, but must be accepted by `who`. Put the username
/// into a queue for acceptance.
pub fn queue_acceptance(who: &T::AccountId, username: Username<T>) {
let now = frame_system::Pallet::<T>::block_number();
let expiration = now.saturating_add(T::PendingUsernameExpiration::get());
PendingUsernames::<T>::insert(&username, (who.clone(), expiration));
Self::deposit_event(Event::UsernameQueued { who: who.clone(), username, expiration });
}
/// Reap an identity, clearing associated storage items and refunding any deposits. This
@@ -943,7 +1346,7 @@ impl<T: Config> Pallet<T> {
pub fn reap_identity(who: &T::AccountId) -> Result<(u32, u32, u32), DispatchError> {
// `take` any storage items keyed by `target`
// identity
let id = <IdentityOf<T>>::take(&who).ok_or(Error::<T>::NotNamed)?;
let (id, _maybe_username) = <IdentityOf<T>>::take(&who).ok_or(Error::<T>::NoIdentity)?;
let registrars = id.judgements.len() as u32;
let encoded_byte_size = id.info.encoded_size() as u32;
@@ -976,8 +1379,8 @@ impl<T: Config> Pallet<T> {
// Identity Deposit
let new_id_deposit = IdentityOf::<T>::try_mutate(
&target,
|registration| -> Result<BalanceOf<T>, DispatchError> {
let reg = registration.as_mut().ok_or(Error::<T>::NoIdentity)?;
|identity_of| -> Result<BalanceOf<T>, DispatchError> {
let (reg, _) = identity_of.as_mut().ok_or(Error::<T>::NoIdentity)?;
// Calculate what deposit should be
let encoded_byte_size = reg.info.encoded_size() as u32;
let byte_deposit =
@@ -1014,11 +1417,14 @@ impl<T: Config> Pallet<T> {
) -> DispatchResult {
IdentityOf::<T>::insert(
&who,
Registration {
judgements: Default::default(),
deposit: Zero::zero(),
info: info.clone(),
},
(
Registration {
judgements: Default::default(),
deposit: Zero::zero(),
info: info.clone(),
},
None::<Username<T>>,
),
);
Ok(())
}
@@ -1030,7 +1436,6 @@ impl<T: Config> Pallet<T> {
who: &T::AccountId,
subs: Vec<(T::AccountId, Data)>,
) -> DispatchResult {
use frame_support::BoundedVec;
let mut sub_accounts = BoundedVec::<T::AccountId, T::MaxSubAccounts>::default();
for (sub, name) in subs {
<SuperOf<T>>::insert(&sub, (who.clone(), name));