Skip to content

Commit

Permalink
Migrate multisig (#272)
Browse files Browse the repository at this point in the history
* Migrate multisig

* Unit tests

* Doc

* Fix

* More checks

* Doc

* Reject duplicative submission
  • Loading branch information
AurevoirXavier authored Feb 9, 2023
1 parent 93aff17 commit 9057d49
Show file tree
Hide file tree
Showing 12 changed files with 610 additions and 103 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions pallet/account-migration/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,13 @@ pallet-balances = { default-features = false, git = "https://github.com/parityte
pallet-identity = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.36" }
pallet-vesting = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.36" }
sp-core = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.36" }
sp-io = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.36" }
sp-runtime = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.36" }
sp-std = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.36" }

[dev-dependencies]
sp-keyring = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.36" }
sp-version = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.36" }

[features]
default = ["std"]
Expand All @@ -54,6 +56,7 @@ std = [
"pallet-identity/std",
"pallet-vesting/std",
"sp-core/std",
"sp-io/std",
"sp-runtime/std",
"sp-std/std",
]
Expand Down
275 changes: 229 additions & 46 deletions pallet/account-migration/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,6 @@
#![cfg_attr(not(feature = "std"), no_std)]
#![deny(missing_docs)]

#[cfg(test)]
mod tests;

// darwinia
use darwinia_deposit::Deposit;
use darwinia_staking::Ledger;
Expand All @@ -62,8 +59,9 @@ use pallet_balances::AccountData;
use pallet_identity::Registration;
use pallet_vesting::VestingInfo;
use sp_core::sr25519::{Public, Signature};
use sp_io::hashing;
use sp_runtime::{
traits::{IdentityLookup, Verify},
traits::{IdentityLookup, TrailingZeroInput, Verify},
AccountId32, RuntimeDebug,
};
use sp_std::prelude::*;
Expand All @@ -74,6 +72,17 @@ pub mod pallet {

const KTON_ID: u64 = 1026;

/// The migration destination was already taken by someone.
pub const E_ACCOUNT_ALREADY_EXISTED: u8 = 0;
/// The migration source was not exist.
pub const E_ACCOUNT_NOT_FOUND: u8 = 1;
/// Invalid signature.
pub const E_INVALID_SIGNATURE: u8 = 2;
/// Duplicative submission.
pub const E_DUPLICATIVE_SUBMISSION: u8 = 3;
/// The account is not a member of the multisig.
pub const E_NOT_MULTISIG_MEMBER: u8 = 4;

#[pallet::pallet]
pub struct Pallet<T>(PhantomData<T>);

Expand Down Expand Up @@ -110,6 +119,8 @@ pub mod pallet {
ExceedMaxVestings,
/// Exceed maximum deposit count.
ExceedMaxDeposits,
/// The migration destination was already taken by someone.
AccountAlreadyExisted,
}

/// [`frame_system::Account`] data.
Expand All @@ -123,7 +134,6 @@ pub mod pallet {
/// [`pallet_asset::AssetAccount`] data.
///
/// https://github.dev/paritytech/substrate/blob/polkadot-v0.9.30/frame/assets/src/types.rs#L115
// The size of encoded `pallet_asset::AssetAccount` is 18 bytes.
#[pallet::storage]
#[pallet::getter(fn kton_account_of)]
pub type KtonAccounts<T: Config> = StorageMap<_, Blake2_128Concat, AccountId32, AssetAccount>;
Expand Down Expand Up @@ -160,6 +170,12 @@ pub mod pallet {
#[pallet::getter(fn ledger_of)]
pub type Ledgers<T: Config> = StorageMap<_, Blake2_128Concat, AccountId32, Ledger<T>>;

/// Multisig migration caches.
#[pallet::storage]
#[pallet::unbounded]
#[pallet::getter(fn multisig_of)]
pub type Multisigs<T: Config> = StorageMap<_, Identity, AccountId32, Multisig>;

#[pallet::call]
impl<T: Config> Pallet<T> {
/// Migrate all the account data under the `from` to `to`.
Expand All @@ -173,8 +189,180 @@ pub mod pallet {
) -> DispatchResult {
ensure_none(origin)?;

Self::migrate_inner(from, to)?;

Ok(())
}

/// Similar to `migrate` but for multisig accounts.
///
/// The `_signature` should be provided by `who`.
#[pallet::call_index(1)]
#[pallet::weight(0)]
pub fn migrate_multisig(
origin: OriginFor<T>,
submitter: AccountId32,
others: Vec<AccountId32>,
threshold: u16,
to: AccountId20,
_signature: Signature,
) -> DispatchResult {
ensure_none(origin)?;

let (members, multisig) = multisig_of(submitter.clone(), others, threshold);

if threshold < 2 {
Self::migrate_inner(multisig, to)?;
} else {
let mut members = members.into_iter().map(|m| (m, false)).collect::<Vec<_>>();

// Set the status to `true`.
//
// Because the `_signature` was already been verified in `pre_dispatch`.
members
.iter_mut()
.find(|(who, _)| who == &submitter)
.expect("[pallet::account-migration] `who` must be existed; qed")
.1 = true;

<Multisigs<T>>::insert(multisig, Multisig { migrate_to: to, members, threshold });
}

Ok(())
}

/// To complete the pending multisig migration.
///
/// The `_signature` should be provided by `submitter`.
#[pallet::call_index(2)]
#[pallet::weight(0)]
pub fn complete_multisig_migration(
origin: OriginFor<T>,
multisig: AccountId32,
submitter: AccountId32,
_signature: Signature,
) -> DispatchResult {
ensure_none(origin)?;

let mut multisig_info = <Multisigs<T>>::take(&multisig)
.expect("[pallet::account-migration] already checked in `pre_dispatch`; qed");

// Kill the storage, if the `migrate_to` was created during the migration.
//
// Require to redo the `migrate_multisig` operation.
if <frame_system::Account<T>>::contains_key(multisig_info.migrate_to) {
Err(<Error<T>>::AccountAlreadyExisted)?;
}

// Set the status to `true`.
//
// Because the `_signature` was already been verified in `pre_dispatch`.
multisig_info
.members
.iter_mut()
.find(|(who, _)| who == &submitter)
.expect("[pallet::account-migration] already checked in `pre_dispatch`; qed")
.1 = true;

if multisig_info.members.iter().fold(0, |acc, (_, ok)| if *ok { acc + 1 } else { acc })
>= multisig_info.threshold
{
Self::migrate_inner(multisig, multisig_info.migrate_to)?;
} else {
<Multisigs<T>>::insert(multisig, multisig_info);
}

Ok(())
}
}
#[pallet::validate_unsigned]
impl<T: Config> ValidateUnsigned for Pallet<T> {
type Call = Call<T>;

fn validate_unsigned(_source: TransactionSource, call: &Self::Call) -> TransactionValidity {
match call {
Call::migrate { from, to, signature } => {
Self::pre_check_existing(from, to)?;

Self::pre_check_signature(from, to, signature)
},
Call::migrate_multisig { submitter, others, threshold, to, signature } => {
let (_, multisig) =
multisig_of(submitter.to_owned(), others.to_owned(), *threshold);

Self::pre_check_existing(&multisig, to)?;

Self::pre_check_signature(submitter, to, signature)
},
Call::complete_multisig_migration { multisig, submitter, signature } => {
let Some(multisig_info) = <Multisigs<T>>::get(multisig) else {
return InvalidTransaction::Custom(E_ACCOUNT_NOT_FOUND).into();
};
let mut is_member = false;

for (who, ok) in multisig_info.members.iter() {
if who == submitter {
// Reject duplicative submission.
if *ok {
return InvalidTransaction::Custom(E_DUPLICATIVE_SUBMISSION).into();
}

is_member = true;

break;
}
}

if !is_member {
return InvalidTransaction::Custom(E_NOT_MULTISIG_MEMBER).into();
}

Self::pre_check_signature(submitter, &multisig_info.migrate_to, signature)
},
_ => InvalidTransaction::Call.into(),
}
}
}
impl<T> Pallet<T>
where
T: Config,
{
fn pre_check_existing(
from: &AccountId32,
to: &AccountId20,
) -> Result<(), TransactionValidityError> {
if !<Accounts<T>>::contains_key(from) {
Err(InvalidTransaction::Custom(E_ACCOUNT_NOT_FOUND))?;
}
if <frame_system::Account<T>>::contains_key(to) {
Err(InvalidTransaction::Custom(E_ACCOUNT_ALREADY_EXISTED))?;
}

Ok(())
}

fn pre_check_signature(
from: &AccountId32,
to: &AccountId20,
signature: &Signature,
) -> TransactionValidity {
let message = sr25519_signable_message(T::Version::get().spec_name.as_ref(), to);

if verify_sr25519_signature(from, &message, signature) {
ValidTransaction::with_tag_prefix("account-migration")
.and_provides(from)
.priority(100)
.longevity(TransactionLongevity::max_value())
.propagate(true)
.build()
} else {
InvalidTransaction::Custom(E_INVALID_SIGNATURE).into()
}
}

fn migrate_inner(from: AccountId32, to: AccountId20) -> DispatchResult {
let account = <Accounts<T>>::take(&from)
.ok_or("[pallet::account-migration] already checked in `pre_dispatch`; qed")?;
.expect("[pallet::account-migration] already checked in `pre_dispatch`; qed");

<frame_system::Account<T>>::insert(to, account);

Expand Down Expand Up @@ -266,45 +454,6 @@ pub mod pallet {

Ok(())
}

// TODO: migrate multi-sig
}
#[pallet::validate_unsigned]
impl<T: Config> ValidateUnsigned for Pallet<T> {
type Call = Call<T>;

fn validate_unsigned(_source: TransactionSource, call: &Self::Call) -> TransactionValidity {
// The migration destination was already taken by someone.
const E_ACCOUNT_ALREADY_EXISTED: u8 = 0;
// The migration source was not exist.
const E_ACCOUNT_NOT_FOUND: u8 = 1;
// Invalid signature.
const E_INVALID_SIGNATURE: u8 = 2;

let Call::migrate { from, to, signature } = call else {
return InvalidTransaction::Call.into();
};

if !<Accounts<T>>::contains_key(from) {
return InvalidTransaction::Custom(E_ACCOUNT_NOT_FOUND).into();
}
if <frame_system::Account<T>>::contains_key(to) {
return InvalidTransaction::Custom(E_ACCOUNT_ALREADY_EXISTED).into();
}

let message = sr25519_signable_message(T::Version::get().spec_name.as_ref(), to);

if verify_sr25519_signature(from, &message, signature) {
ValidTransaction::with_tag_prefix("account-migration")
.and_provides(from)
.priority(100)
.longevity(TransactionLongevity::max_value())
.propagate(true)
.build()
} else {
InvalidTransaction::Custom(E_INVALID_SIGNATURE).into()
}
}
}
}
pub use pallet::*;
Expand Down Expand Up @@ -332,6 +481,14 @@ pub enum ExistenceReason {
DepositRefunded,
}

#[allow(missing_docs)]
#[derive(Encode, Decode, TypeInfo, RuntimeDebug)]
pub struct Multisig {
pub migrate_to: AccountId20,
pub members: Vec<(AccountId32, bool)>,
pub threshold: u16,
}

/// Build a Darwinia account migration message.
pub fn sr25519_signable_message(spec_name: &[u8], account_id_20: &AccountId20) -> Vec<u8> {
[
Expand All @@ -350,7 +507,8 @@ pub fn sr25519_signable_message(spec_name: &[u8], account_id_20: &AccountId20) -
.concat()
}

fn verify_sr25519_signature(
/// Verify the Sr25519 signature.
pub fn verify_sr25519_signature(
public_key: &AccountId32,
message: &[u8],
signature: &Signature,
Expand All @@ -365,3 +523,28 @@ fn verify_sr25519_signature(

signature.verify(message, public_key)
}

/// Calculate the multisig account.
pub fn multisig_of(
who: AccountId32,
others: Vec<AccountId32>,
threshold: u16,
) -> (Vec<AccountId32>, AccountId32) {
// https://github.com/paritytech/substrate/blob/3bc3742d5c0c5269353d7809d9f8f91104a93273/frame/multisig/src/lib.rs#L525
fn multisig_of_inner(members: &[AccountId32], threshold: u16) -> AccountId32 {
let entropy = (b"modlpy/utilisuba", members, threshold).using_encoded(hashing::blake2_256);

Decode::decode(&mut TrailingZeroInput::new(entropy.as_ref())).expect(
"[pallet::account-migration] infinite length input; no invalid inputs for type; qed",
)
}

let mut members = others;

members.push(who);
members.sort();

let multisig = multisig_of_inner(&members, threshold);

(members, multisig)
}
Loading

0 comments on commit 9057d49

Please sign in to comment.