Skip to content

Commit

Permalink
Identity Deposits Relay to Parachain Migration (paritytech#1814)
Browse files Browse the repository at this point in the history
The goal of this PR is to migrate Identity deposits from the Relay Chain
to a system parachain.

The problem I want to solve is that `IdentityOf` and `SubsOf` both store
an amount that's held in reserve as a storage deposit. When migrating to
a parachain, we can take a snapshot of the actual `IdentityInfo` and
sub-account mappings, but should migrate (off chain) the `deposit`s to
zero, since the chain (and by extension, accounts) won't have any funds
at genesis.

The good news is that we expect parachain deposits to be significantly
lower (possibly 100x) on the parachain. That is, a deposit of 21 DOT on
the Relay Chain would need 0.21 DOT on a parachain. This PR proposes to
migrate the deposits in the following way:

1. Introduces a new pallet with two extrinsics: 
- `reap_identity`: Has a configurable `ReapOrigin`, which would be set
to `EnsureSigned` on the Relay Chain (i.e. callable by anyone) and
`EnsureRoot` on the parachain (we don't want identities reaped from
there).
- `poke_deposit`: Checks what deposit the pallet holds (at genesis,
zero) and attempts to update the amount based on the calculated deposit
for storage data.
2. `reap_identity` clears all storage data for a `target` account and
unreserves their deposit.
3. A `ReapIdentityHandler` teleports the necessary DOT to the parachain
and calls `poke_deposit`. Since the parachain deposit is much lower, and
was just unreserved, we know we have enough.

One awkwardness I ran into was that the XCMv3 instruction set does not
provide a way for the system to teleport assets without a fee being
deducted on reception. Users shouldn't have to pay a fee for the system
to migrate their info to a more efficient location. So I wrote my own
program and did the `InitiateTeleport` accounting on my own to send a
program with `UnpaidExecution`. Have discussed an
`InitiateUnpaidTeleport` instruction with @franciscoaguirre . Obviously
any chain executing this would have to pass a `Barrier` for free
execution.

TODO:

- [x] Confirm People Chain ParaId
- [x] Confirm People Chain deposit rates (determined in
paritytech#2281)
- [x] Add pallet to Westend

---------

Co-authored-by: Bastian Köcher <[email protected]>
  • Loading branch information
joepetrowski and bkchr authored Nov 15, 2023
1 parent 0f3db9c commit 34988ad
Show file tree
Hide file tree
Showing 2 changed files with 197 additions and 3 deletions.
133 changes: 130 additions & 3 deletions substrate/frame/identity/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,10 @@ mod tests;
mod types;
pub mod weights;

use codec::Encode;
use frame_support::{
pallet_prelude::DispatchResult,
ensure,
pallet_prelude::{DispatchError, DispatchResult},
traits::{BalanceStatus, Currency, Get, OnUnbalanced, ReservableCurrency},
};
use sp_runtime::traits::{AppendZerosInput, Hash, Saturating, StaticLookup, Zero};
Expand Down Expand Up @@ -395,8 +397,7 @@ pub mod pallet {
);

let (old_deposit, old_ids) = <SubsOf<T>>::get(&sender);
let new_deposit =
T::SubAccountDeposit::get().saturating_mul(<BalanceOf<T>>::from(subs.len() as u32));
let new_deposit = Self::subs_deposit(subs.len() as u32);

let not_other_sub =
subs.iter().filter_map(|i| SuperOf::<T>::get(&i.0)).all(|i| i.0 == sender);
Expand Down Expand Up @@ -898,6 +899,26 @@ impl<T: Config> Pallet<T> {
.collect()
}

/// Calculate the deposit required for a number of `sub` accounts.
fn subs_deposit(subs: u32) -> BalanceOf<T> {
T::SubAccountDeposit::get().saturating_mul(<BalanceOf<T>>::from(subs))
}

/// Take the `current` deposit that `who` is holding, and update it to a `new` one.
fn rejig_deposit(
who: &T::AccountId,
current: BalanceOf<T>,
new: BalanceOf<T>,
) -> DispatchResult {
if new > current {
T::Currency::reserve(who, new - current)?;
} else if new < current {
let err_amount = T::Currency::unreserve(who, current - new);
debug_assert!(err_amount.is_zero());
}
Ok(())
}

/// Check if the account has corresponding identity information by the identity field.
pub fn has_identity(
who: &T::AccountId,
Expand All @@ -906,4 +927,110 @@ impl<T: Config> Pallet<T> {
IdentityOf::<T>::get(who)
.map_or(false, |registration| (registration.info.has_identity(fields)))
}

/// Reap an identity, clearing associated storage items and refunding any deposits. This
/// function is very similar to (a) `clear_identity`, but called on a `target` account instead
/// of self; and (b) `kill_identity`, but without imposing a slash.
///
/// Parameters:
/// - `target`: The account for which to reap identity state.
///
/// Return type is a tuple of the number of registrars, `IdentityInfo` bytes, and sub accounts,
/// respectively.
///
/// NOTE: This function is here temporarily for migration of Identity info from the Polkadot
/// Relay Chain into a system parachain. It will be removed after the migration.
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 registrars = id.judgements.len() as u32;
let encoded_byte_size = id.info.encoded_size() as u32;

// subs
let (subs_deposit, sub_ids) = <SubsOf<T>>::take(&who);
let actual_subs = sub_ids.len() as u32;
for sub in sub_ids.iter() {
<SuperOf<T>>::remove(sub);
}

// unreserve any deposits
let deposit = id.total_deposit().saturating_add(subs_deposit);
let err_amount = T::Currency::unreserve(&who, deposit);
debug_assert!(err_amount.is_zero());
Ok((registrars, encoded_byte_size, actual_subs))
}

/// Update the deposits held by `target` for its identity info.
///
/// Parameters:
/// - `target`: The account for which to update deposits.
///
/// Return type is a tuple of the new Identity and Subs deposits, respectively.
///
/// NOTE: This function is here temporarily for migration of Identity info from the Polkadot
/// Relay Chain into a system parachain. It will be removed after the migration.
pub fn poke_deposit(
target: &T::AccountId,
) -> Result<(BalanceOf<T>, BalanceOf<T>), DispatchError> {
// 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)?;
// Calculate what deposit should be
let encoded_byte_size = reg.info.encoded_size() as u32;
let byte_deposit =
T::ByteDeposit::get().saturating_mul(<BalanceOf<T>>::from(encoded_byte_size));
let new_id_deposit = T::BasicDeposit::get().saturating_add(byte_deposit);

// Update account
Self::rejig_deposit(&target, reg.deposit, new_id_deposit)?;

reg.deposit = new_id_deposit;
Ok(new_id_deposit)
},
)?;

// Subs Deposit
let new_subs_deposit = SubsOf::<T>::try_mutate(
&target,
|(current_subs_deposit, subs_of)| -> Result<BalanceOf<T>, DispatchError> {
let new_subs_deposit = Self::subs_deposit(subs_of.len() as u32);
Self::rejig_deposit(&target, *current_subs_deposit, new_subs_deposit)?;
*current_subs_deposit = new_subs_deposit;
Ok(new_subs_deposit)
},
)?;
Ok((new_id_deposit, new_subs_deposit))
}

/// Set an identity with zero deposit. Only used for benchmarking that involves `rejig_deposit`.
#[cfg(feature = "runtime-benchmarks")]
pub fn set_identity_no_deposit(
who: &T::AccountId,
info: T::IdentityInformation,
) -> DispatchResult {
IdentityOf::<T>::insert(
&who,
Registration {
judgements: Default::default(),
deposit: Zero::zero(),
info: info.clone(),
},
);
Ok(())
}

/// Set subs with zero deposit. Only used for benchmarking that involves `rejig_deposit`.
#[cfg(feature = "runtime-benchmarks")]
pub fn set_sub_no_deposit(who: &T::AccountId, sub: T::AccountId) -> DispatchResult {
use frame_support::BoundedVec;
let subs = BoundedVec::<_, T::MaxSubAccounts>::try_from(vec![sub]).unwrap();
SubsOf::<T>::insert::<
&T::AccountId,
(BalanceOf<T>, BoundedVec<T::AccountId, T::MaxSubAccounts>),
>(&who, (Zero::zero(), subs));
Ok(())
}
}
67 changes: 67 additions & 0 deletions substrate/frame/identity/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -712,3 +712,70 @@ fn test_has_identity() {
));
});
}

#[test]
fn reap_identity_works() {
new_test_ext().execute_with(|| {
let ten_info = ten();
assert_ok!(Identity::set_identity(RuntimeOrigin::signed(10), Box::new(ten_info.clone())));
assert_ok!(Identity::set_subs(
RuntimeOrigin::signed(10),
vec![(20, Data::Raw(vec![40; 1].try_into().unwrap()))]
));
// deposit is correct
let id_deposit = id_deposit(&ten_info);
let subs_deposit: u64 = <<Test as Config>::SubAccountDeposit as Get<u64>>::get();
assert_eq!(Balances::free_balance(10), 1000 - id_deposit - subs_deposit);
// reap
assert_ok!(Identity::reap_identity(&10));
// no identity or subs
assert!(Identity::identity(10).is_none());
assert!(Identity::super_of(20).is_none());
// balance is unreserved
assert_eq!(Balances::free_balance(10), 1000);
});
}

#[test]
fn poke_deposit_works() {
new_test_ext().execute_with(|| {
let ten_info = ten();
// Set a custom registration with 0 deposit
IdentityOf::<Test>::insert(
&10,
Registration {
judgements: BoundedVec::default(),
deposit: Zero::zero(),
info: ten_info.clone(),
},
);
assert!(Identity::identity(10).is_some());
// Set a sub with zero deposit
SubsOf::<Test>::insert::<&u64, (u64, BoundedVec<u64, ConstU32<2>>)>(
&10,
(0, vec![20].try_into().unwrap()),
);
SuperOf::<Test>::insert(&20, (&10, Data::Raw(vec![1; 1].try_into().unwrap())));
// Balance is free
assert_eq!(Balances::free_balance(10), 1000);

// poke
assert_ok!(Identity::poke_deposit(&10));

// free balance reduced correctly
let id_deposit = id_deposit(&ten_info);
let subs_deposit: u64 = <<Test as Config>::SubAccountDeposit as Get<u64>>::get();
assert_eq!(Balances::free_balance(10), 1000 - id_deposit - subs_deposit);
// new registration deposit is 10
assert_eq!(
Identity::identity(&10),
Some(Registration {
judgements: BoundedVec::default(),
deposit: id_deposit,
info: ten()
})
);
// new subs deposit is 10 vvvvvvvvvvvv
assert_eq!(Identity::subs_of(10), (subs_deposit, vec![20].try_into().unwrap()));
});
}

0 comments on commit 34988ad

Please sign in to comment.