Skip to content

Commit

Permalink
Unique Usernames in Identity Pallet (paritytech#2651)
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
5 people authored Jan 10, 2024
1 parent b231c28 commit 653a4d8
Show file tree
Hide file tree
Showing 11 changed files with 2,174 additions and 320 deletions.
2 changes: 1 addition & 1 deletion substrate/bin/node/runtime/src/impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ impl IdentityVerifier<AccountId> for AllianceIdentityVerifier {
fn has_good_judgement(who: &AccountId) -> bool {
use pallet_identity::Judgement;
crate::Identity::identity(who)
.map(|registration| registration.judgements)
.map(|(registration, _)| registration.judgements)
.map_or(false, |judgements| {
judgements
.iter()
Expand Down
10 changes: 10 additions & 0 deletions substrate/bin/node/runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1500,6 +1500,12 @@ impl pallet_identity::Config for Runtime {
type Slashed = Treasury;
type ForceOrigin = EnsureRootOrHalfCouncil;
type RegistrarOrigin = EnsureRootOrHalfCouncil;
type OffchainSignature = Signature;
type SigningPublicKey = <Signature as traits::Verify>::Signer;
type UsernameAuthorityOrigin = EnsureRoot<Self::AccountId>;
type PendingUsernameExpiration = ConstU32<{ 7 * DAYS }>;
type MaxSuffixLength = ConstU32<7>;
type MaxUsernameLength = ConstU32<32>;
type WeightInfo = pallet_identity::weights::SubstrateWeight<Runtime>;
}

Expand Down Expand Up @@ -2208,13 +2214,17 @@ pub type Executive = frame_executive::Executive<
Migrations,
>;

// We don't have a limit in the Relay Chain.
const IDENTITY_MIGRATION_KEY_LIMIT: u64 = u64::MAX;

// All migrations executed on runtime upgrade as a nested tuple of types implementing
// `OnRuntimeUpgrade`. Note: These are examples and do not need to be run directly
// after the genesis block.
type Migrations = (
pallet_nomination_pools::migration::versioned::V6ToV7<Runtime>,
pallet_alliance::migration::Migration<Runtime>,
pallet_contracts::Migration<Runtime>,
pallet_identity::migration::versioned::V0ToV1<Runtime, IDENTITY_MIGRATION_KEY_LIMIT>,
);

type EventRecord = frame_system::EventRecord<
Expand Down
33 changes: 31 additions & 2 deletions substrate/frame/alliance/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@
pub use sp_core::H256;
use sp_runtime::traits::Hash;
pub use sp_runtime::{traits::BlakeTwo256, BuildStorage};
pub use sp_runtime::{
traits::{BlakeTwo256, IdentifyAccount, IdentityLookup, Lazy, Verify},
BuildStorage, MultiSignature,
};
use sp_std::convert::{TryFrom, TryInto};

pub use frame_support::{
Expand Down Expand Up @@ -101,6 +104,7 @@ parameter_types! {
pub const MaxSubAccounts: u32 = 2;
pub const MaxAdditionalFields: u32 = 2;
pub const MaxRegistrars: u32 = 20;
pub const PendingUsernameExpiration: u64 = 100;
}
ord_parameter_types! {
pub const One: u64 = 1;
Expand All @@ -124,9 +128,34 @@ impl pallet_identity::Config for Test {
type Slashed = ();
type RegistrarOrigin = EnsureOneOrRoot;
type ForceOrigin = EnsureTwoOrRoot;
type OffchainSignature = AccountU64;
type SigningPublicKey = AccountU64;
type UsernameAuthorityOrigin = EnsureOneOrRoot;
type PendingUsernameExpiration = PendingUsernameExpiration;
type MaxSuffixLength = ConstU32<7>;
type MaxUsernameLength = ConstU32<32>;
type WeightInfo = ();
}

#[derive(Clone, Debug, Encode, Decode, PartialEq, Eq, TypeInfo)]
pub struct AccountU64(u64);
impl IdentifyAccount for AccountU64 {
type AccountId = u64;
fn into_account(self) -> u64 {
0u64
}
}
impl Verify for AccountU64 {
type Signer = AccountU64;
fn verify<L: Lazy<[u8]>>(
&self,
_msg: L,
_signer: &<Self::Signer as IdentifyAccount>::AccountId,
) -> bool {
false
}
}

pub struct AllianceIdentityVerifier;
impl IdentityVerifier<AccountId> for AllianceIdentityVerifier {
fn has_required_identities(who: &AccountId) -> bool {
Expand All @@ -135,7 +164,7 @@ impl IdentityVerifier<AccountId> for AllianceIdentityVerifier {

fn has_good_judgement(who: &AccountId) -> bool {
if let Some(judgements) =
Identity::identity(who).map(|registration| registration.judgements)
Identity::identity(who).map(|(registration, _)| registration.judgements)
{
judgements
.iter()
Expand Down
4 changes: 4 additions & 0 deletions substrate/frame/identity/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ targets = ["x86_64-unknown-linux-gnu"]
[dependencies]
codec = { package = "parity-scale-codec", version = "3.6.1", default-features = false, features = ["derive", "max-encoded-len"] }
enumflags2 = { version = "0.7.7" }
log = { version = "0.4.17", default-features = false }
scale-info = { version = "2.10.0", default-features = false, features = ["derive"] }
frame-benchmarking = { path = "../benchmarking", default-features = false, optional = true }
frame-support = { path = "../support", default-features = false }
Expand All @@ -29,6 +30,7 @@ sp-std = { path = "../../primitives/std", default-features = false }
[dev-dependencies]
pallet-balances = { path = "../balances" }
sp-core = { path = "../../primitives/core" }
sp-keystore = { path = "../../primitives/keystore" }

[features]
default = ["std"]
Expand All @@ -39,10 +41,12 @@ std = [
"frame-benchmarking?/std",
"frame-support/std",
"frame-system/std",
"log/std",
"pallet-balances/std",
"scale-info/std",
"sp-core/std",
"sp-io/std",
"sp-keystore/std",
"sp-runtime/std",
"sp-std/std",
]
Expand Down
211 changes: 207 additions & 4 deletions substrate/frame/identity/src/benchmarking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,43 @@
use super::*;

use crate::Pallet as Identity;
use codec::Encode;
use frame_benchmarking::{
account, impl_benchmark_test_suite, v2::*, whitelisted_caller, BenchmarkError,
};
use frame_support::{
ensure,
traits::{EnsureOrigin, Get},
assert_ok, ensure,
traits::{EnsureOrigin, Get, OnFinalize, OnInitialize},
};
use frame_system::RawOrigin;
use sp_runtime::traits::Bounded;
use sp_io::crypto::{sr25519_generate, sr25519_sign};
use sp_runtime::{
traits::{Bounded, IdentifyAccount, One},
MultiSignature, MultiSigner,
};

const SEED: u32 = 0;

fn assert_has_event<T: Config>(generic_event: <T as Config>::RuntimeEvent) {
frame_system::Pallet::<T>::assert_has_event(generic_event.into());
}

fn assert_last_event<T: Config>(generic_event: <T as Config>::RuntimeEvent) {
frame_system::Pallet::<T>::assert_last_event(generic_event.into());
}

fn run_to_block<T: Config>(n: frame_system::pallet_prelude::BlockNumberFor<T>) {
while frame_system::Pallet::<T>::block_number() < n {
crate::Pallet::<T>::on_finalize(frame_system::Pallet::<T>::block_number());
frame_system::Pallet::<T>::on_finalize(frame_system::Pallet::<T>::block_number());
frame_system::Pallet::<T>::set_block_number(
frame_system::Pallet::<T>::block_number() + One::one(),
);
frame_system::Pallet::<T>::on_initialize(frame_system::Pallet::<T>::block_number());
crate::Pallet::<T>::on_initialize(frame_system::Pallet::<T>::block_number());
}
}

// Adds `r` registrars to the Identity Pallet. These registrars will have set fees and fields.
fn add_registrars<T: Config>(r: u32) -> Result<(), &'static str> {
for i in 0..r {
Expand Down Expand Up @@ -95,7 +116,28 @@ fn add_sub_accounts<T: Config>(
Ok(subs)
}

#[benchmarks]
fn bench_suffix() -> Vec<u8> {
b"bench".to_vec()
}

fn bench_username() -> Vec<u8> {
// len = 24
b"veryfastbenchmarkmachine".to_vec()
}

fn bounded_username<T: Config>(username: Vec<u8>, suffix: Vec<u8>) -> Username<T> {
let mut full_username = Vec::with_capacity(username.len() + suffix.len() + 1);
full_username.extend(username);
full_username.extend(b".");
full_username.extend(suffix);
Username::<T>::try_from(full_username).expect("test usernames should fit within bounds")
}

#[benchmarks(
where
<T as frame_system::Config>::AccountId: From<sp_runtime::AccountId32>,
T::OffchainSignature: From<MultiSignature>,
)]
mod benchmarks {
use super::*;

Expand Down Expand Up @@ -523,5 +565,166 @@ mod benchmarks {
Ok(())
}

#[benchmark]
fn add_username_authority() -> Result<(), BenchmarkError> {
let origin =
T::UsernameAuthorityOrigin::try_successful_origin().expect("can generate origin");

let authority: T::AccountId = account("authority", 0, SEED);
let authority_lookup = T::Lookup::unlookup(authority.clone());
let suffix = bench_suffix();
let allocation = 10;

#[extrinsic_call]
_(origin as T::RuntimeOrigin, authority_lookup, suffix, allocation);

assert_last_event::<T>(Event::<T>::AuthorityAdded { authority }.into());
Ok(())
}

#[benchmark]
fn remove_username_authority() -> Result<(), BenchmarkError> {
let origin =
T::UsernameAuthorityOrigin::try_successful_origin().expect("can generate origin");

let authority: T::AccountId = account("authority", 0, SEED);
let authority_lookup = T::Lookup::unlookup(authority.clone());
let suffix = bench_suffix();
let allocation = 10;

assert_ok!(Identity::<T>::add_username_authority(
origin.clone(),
authority_lookup.clone(),
suffix,
allocation
));

#[extrinsic_call]
_(origin as T::RuntimeOrigin, authority_lookup);

assert_last_event::<T>(Event::<T>::AuthorityRemoved { authority }.into());
Ok(())
}

#[benchmark]
fn set_username_for() -> Result<(), BenchmarkError> {
// Set up a username authority.
let auth_origin =
T::UsernameAuthorityOrigin::try_successful_origin().expect("can generate origin");
let authority: T::AccountId = account("authority", 0, SEED);
let authority_lookup = T::Lookup::unlookup(authority.clone());
let suffix = bench_suffix();
let allocation = 10;

Identity::<T>::add_username_authority(
auth_origin,
authority_lookup,
suffix.clone(),
allocation,
)?;

let username = bench_username();
let bounded_username = bounded_username::<T>(username.clone(), suffix.clone());
let encoded_username = Encode::encode(&bounded_username.to_vec());

let public = sr25519_generate(0.into(), None);
let who_account: T::AccountId = MultiSigner::Sr25519(public).into_account().into();
let who_lookup = T::Lookup::unlookup(who_account.clone());

let signature =
MultiSignature::Sr25519(sr25519_sign(0.into(), &public, &encoded_username).unwrap());

// Verify signature here to avoid surprise errors at runtime
assert!(signature.verify(&encoded_username[..], &public.into()));

#[extrinsic_call]
_(RawOrigin::Signed(authority.clone()), who_lookup, username, Some(signature.into()));

assert_has_event::<T>(
Event::<T>::UsernameSet {
who: who_account.clone(),
username: bounded_username.clone(),
}
.into(),
);
assert_has_event::<T>(
Event::<T>::PrimaryUsernameSet { who: who_account, username: bounded_username }.into(),
);
Ok(())
}

#[benchmark]
fn accept_username() -> Result<(), BenchmarkError> {
let caller: T::AccountId = whitelisted_caller();
let username = bounded_username::<T>(bench_username(), bench_suffix());

Identity::<T>::queue_acceptance(&caller, username.clone());

#[extrinsic_call]
_(RawOrigin::Signed(caller.clone()), username.clone());

assert_last_event::<T>(Event::<T>::UsernameSet { who: caller, username }.into());
Ok(())
}

#[benchmark]
fn remove_expired_approval() -> Result<(), BenchmarkError> {
let caller: T::AccountId = whitelisted_caller();
let username = bounded_username::<T>(bench_username(), bench_suffix());
Identity::<T>::queue_acceptance(&caller, username.clone());

let expected_exiration =
frame_system::Pallet::<T>::block_number() + T::PendingUsernameExpiration::get();

run_to_block::<T>(expected_exiration + One::one());

#[extrinsic_call]
_(RawOrigin::Signed(caller.clone()), username);

assert_last_event::<T>(Event::<T>::PreapprovalExpired { whose: caller }.into());
Ok(())
}

#[benchmark]
fn set_primary_username() -> Result<(), BenchmarkError> {
let caller: T::AccountId = whitelisted_caller();
let first_username = bounded_username::<T>(bench_username(), bench_suffix());
let second_username = bounded_username::<T>(b"slowbenchmark".to_vec(), bench_suffix());

// First one will be set as primary. Second will not be.
Identity::<T>::insert_username(&caller, first_username);
Identity::<T>::insert_username(&caller, second_username.clone());

#[extrinsic_call]
_(RawOrigin::Signed(caller.clone()), second_username.clone());

assert_last_event::<T>(
Event::<T>::PrimaryUsernameSet { who: caller, username: second_username }.into(),
);
Ok(())
}

#[benchmark]
fn remove_dangling_username() -> Result<(), BenchmarkError> {
let caller: T::AccountId = whitelisted_caller();
let first_username = bounded_username::<T>(bench_username(), bench_suffix());
let second_username = bounded_username::<T>(b"slowbenchmark".to_vec(), bench_suffix());

// First one will be set as primary. Second will not be.
Identity::<T>::insert_username(&caller, first_username);
Identity::<T>::insert_username(&caller, second_username.clone());

// User calls `clear_identity`, leaving their second username as "dangling"
Identity::<T>::clear_identity(RawOrigin::Signed(caller.clone()).into())?;

#[extrinsic_call]
_(RawOrigin::Signed(caller.clone()), second_username.clone());

assert_last_event::<T>(
Event::<T>::DanglingUsernameRemoved { who: caller, username: second_username }.into(),
);
Ok(())
}

impl_benchmark_test_suite!(Identity, crate::tests::new_test_ext(), crate::tests::Test);
}
Loading

0 comments on commit 653a4d8

Please sign in to comment.