From f1f3f62db3df8cd6a50f868aed5db2c15603c2c2 Mon Sep 17 00:00:00 2001 From: Francisco Silva Date: Fri, 4 Oct 2024 09:25:51 +0200 Subject: [PATCH] Adding omni-account pallet (#3098) --- common/primitives/core/src/identity.rs | 35 +- parachain/Cargo.lock | 24 + parachain/Cargo.toml | 2 + parachain/pallets/omni-account/Cargo.toml | 52 ++ parachain/pallets/omni-account/src/lib.rs | 279 ++++++++++ parachain/pallets/omni-account/src/mock.rs | 195 +++++++ parachain/pallets/omni-account/src/tests.rs | 538 ++++++++++++++++++++ parachain/runtime/litentry/Cargo.toml | 1 + parachain/runtime/litentry/src/lib.rs | 22 +- parachain/runtime/paseo/Cargo.toml | 1 + parachain/runtime/paseo/src/lib.rs | 21 +- parachain/runtime/rococo/Cargo.toml | 1 + parachain/runtime/rococo/src/lib.rs | 21 +- 13 files changed, 1183 insertions(+), 9 deletions(-) create mode 100644 parachain/pallets/omni-account/Cargo.toml create mode 100644 parachain/pallets/omni-account/src/lib.rs create mode 100644 parachain/pallets/omni-account/src/mock.rs create mode 100644 parachain/pallets/omni-account/src/tests.rs diff --git a/common/primitives/core/src/identity.rs b/common/primitives/core/src/identity.rs index a2b3090ab4..3e5980a826 100644 --- a/common/primitives/core/src/identity.rs +++ b/common/primitives/core/src/identity.rs @@ -30,12 +30,12 @@ use parity_scale_codec::{Decode, Encode, Error, Input, MaxEncodedLen}; use scale_info::{meta_type, Type, TypeDefSequence, TypeInfo}; use sp_core::{ crypto::{AccountId32, ByteArray}, - ecdsa, ed25519, sr25519, H160, + ecdsa, ed25519, sr25519, H160, H256, }; use sp_io::hashing::blake2_256; use sp_runtime::{ traits::{BlakeTwo256, ConstU32}, - BoundedVec, + BoundedVec, RuntimeDebug, }; use strum_macros::EnumIter; @@ -468,6 +468,11 @@ impl Identity { } )) } + + pub fn hash(&self) -> Result { + let did = self.to_did()?; + Ok(H256::from(blake2_256(&did.encode()))) + } } impl From for Identity { @@ -524,6 +529,24 @@ impl From<[u8; 33]> for Identity { } } +#[derive(Encode, Decode, TypeInfo, Clone, PartialEq, Eq, RuntimeDebug)] +pub enum MemberIdentity { + Public(Identity), + Private(Vec), +} + +impl MemberIdentity { + pub fn is_public(&self) -> bool { + matches!(self, Self::Public(..)) + } +} + +impl From for MemberIdentity { + fn from(identity: Identity) -> Self { + Self::Public(identity) + } +} + #[cfg(test)] mod tests { use super::*; @@ -773,4 +796,12 @@ mod tests { assert_eq!(identity.to_did().unwrap(), did.as_str()); assert_eq!(Identity::from_did(did.as_str()).unwrap(), identity); } + + #[test] + fn test_identity_hash() { + let identity = Identity::Substrate([0; 32].into()); + let did_str = "did:litentry:substrate:0x0000000000000000000000000000000000000000000000000000000000000000"; + let hash = identity.hash().unwrap(); + assert_eq!(hash, H256::from(blake2_256(&did_str.encode()))); + } } diff --git a/parachain/Cargo.lock b/parachain/Cargo.lock index 4b7e019a05..76618ffb6a 100644 --- a/parachain/Cargo.lock +++ b/parachain/Cargo.lock @@ -5661,6 +5661,7 @@ dependencies = [ "pallet-membership", "pallet-message-queue", "pallet-multisig", + "pallet-omni-account", "pallet-parachain-staking", "pallet-preimage", "pallet-proxy", @@ -7805,6 +7806,27 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-omni-account" +version = "0.1.0" +dependencies = [ + "core-primitives", + "frame-support", + "frame-system", + "pallet-balances", + "pallet-teebag", + "pallet-timestamp", + "pallet-utility", + "parity-scale-codec", + "scale-info", + "sp-core", + "sp-core-hashing", + "sp-io", + "sp-keyring", + "sp-runtime", + "sp-std", +] + [[package]] name = "pallet-parachain-staking" version = "0.1.0" @@ -8531,6 +8553,7 @@ dependencies = [ "pallet-membership", "pallet-message-queue", "pallet-multisig", + "pallet-omni-account", "pallet-parachain-staking", "pallet-preimage", "pallet-proxy", @@ -10830,6 +10853,7 @@ dependencies = [ "pallet-membership", "pallet-message-queue", "pallet-multisig", + "pallet-omni-account", "pallet-parachain-staking", "pallet-preimage", "pallet-proxy", diff --git a/parachain/Cargo.toml b/parachain/Cargo.toml index d229cfd2eb..d5bf8816b5 100644 --- a/parachain/Cargo.toml +++ b/parachain/Cargo.toml @@ -18,6 +18,7 @@ members = [ 'pallets/teebag', 'pallets/vc-management', 'pallets/xcm-asset-manager', + 'pallets/omni-account', 'precompiles/*', 'runtime/litentry', 'runtime/rococo', @@ -257,6 +258,7 @@ pallet-bridge-transfer = { path = "pallets/bridge/bridge-transfer", default-feat pallet-extrinsic-filter = { path = "pallets/extrinsic-filter", default-features = false } pallet-group = { path = "pallets/group", default-features = false } pallet-identity-management = { path = "pallets/identity-management", default-features = false } +pallet-omni-account = { path = "pallets/omni-account", default-features = false } pallet-parachain-staking = { path = "pallets/parachain-staking", default-features = false } pallet-score-staking = { path = "pallets/score-staking", default-features = false } pallet-teebag = { path = "pallets/teebag", default-features = false } diff --git a/parachain/pallets/omni-account/Cargo.toml b/parachain/pallets/omni-account/Cargo.toml new file mode 100644 index 0000000000..451bb0ee7e --- /dev/null +++ b/parachain/pallets/omni-account/Cargo.toml @@ -0,0 +1,52 @@ +[package] +authors = ['Trust Computing GmbH '] +version = "0.1.0" +edition = "2021" +homepage = 'https://litentry.com' +name = 'pallet-omni-account' +repository = 'https://github.com/litentry/litentry-parachain' + +[dependencies] +parity-scale-codec = { workspace = true } +scale-info = { workspace = true } + +frame-support = { workspace = true } +frame-system = { workspace = true } +sp-core = { workspace = true } +sp-core-hashing = { workspace = true } +sp-io = { workspace = true } +sp-runtime = { workspace = true } +sp-std = { workspace = true } + +core-primitives = { workspace = true } + +[dev-dependencies] +pallet-balances = { workspace = true, default-features = true } +pallet-timestamp = { workspace = true, features = ["std"] } +pallet-teebag = { workspace = true, features = ["std", "test-util"] } +pallet-utility = { workspace = true, features = ["std"] } +sp-keyring = { workspace = true } + +[features] +default = ["std"] +runtime-benchmarks = [ + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "pallet-timestamp/runtime-benchmarks", + "pallet-teebag/runtime-benchmarks", +] +std = [ + "parity-scale-codec/std", + "scale-info/std", + "sp-std/std", + "sp-io/std", + "sp-core/std", + "sp-runtime/std", + "sp-core/std", + "sp-io/std", + "frame-support/std", + "frame-system/std", + "pallet-teebag/std", + "core-primitives/std", +] +try-runtime = ["frame-support/try-runtime"] diff --git a/parachain/pallets/omni-account/src/lib.rs b/parachain/pallets/omni-account/src/lib.rs new file mode 100644 index 0000000000..228a5a1d53 --- /dev/null +++ b/parachain/pallets/omni-account/src/lib.rs @@ -0,0 +1,279 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +#[cfg(test)] +mod mock; +#[cfg(test)] +mod tests; + +pub use core_primitives::{Identity, MemberIdentity}; +pub use frame_system::pallet_prelude::BlockNumberFor; +pub use pallet::*; + +use frame_support::pallet_prelude::*; +use frame_system::pallet_prelude::*; +use sp_core::H256; +use sp_core_hashing::blake2_256; +use sp_std::vec::Vec; + +#[derive(Encode, Decode, TypeInfo, Clone, PartialEq, Eq, RuntimeDebug)] +pub struct IDGraphMember { + pub id: MemberIdentity, + pub hash: H256, +} + +pub trait AccountIdConverter { + fn convert(identity: &Identity) -> Option; +} + +pub trait IDGraphHash { + fn graph_hash(&self) -> H256; +} + +impl IDGraphHash for BoundedVec { + fn graph_hash(&self) -> H256 { + let id_graph_members_hashes: Vec = self.iter().map(|member| member.hash).collect(); + H256::from(blake2_256(&id_graph_members_hashes.encode())) + } +} + +#[frame_support::pallet] +pub mod pallet { + use super::*; + + /// The current storage version. + const STORAGE_VERSION: StorageVersion = StorageVersion::new(0); + + #[pallet::pallet] + #[pallet::storage_version(STORAGE_VERSION)] + #[pallet::without_storage_info] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: frame_system::Config { + /// The event type of this pallet. + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + /// The origin which can manage the pallet. + type TEECallOrigin: EnsureOrigin; + /// The maximum number of identities an id graph can have. + #[pallet::constant] + type MaxIDGraphLength: Get; + /// AccountId converter + type AccountIdConverter: AccountIdConverter; + } + pub type IDGraph = BoundedVec::MaxIDGraphLength>; + + #[pallet::storage] + pub type LinkedIdentityHashes = + StorageMap; + + #[pallet::storage] + #[pallet::getter(fn id_graphs)] + pub type IDGraphs = + StorageMap>; + + #[pallet::storage] + #[pallet::getter(fn id_graph_hashes)] + pub type IDGraphHashes = + StorageMap; + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// Identity linked + IdentityLinked { who: T::AccountId, identity: H256 }, + /// Identity remove + IdentityRemoved { who: T::AccountId, identity_hashes: Vec }, + /// Identity made public + IdentityMadePublic { who: T::AccountId, identity_hash: H256 }, + } + + #[pallet::error] + pub enum Error { + /// Identity is already linked + IdentityAlreadyLinked, + /// IDGraph len limit reached + IDGraphLenLimitReached, + /// Identity not found + IdentityNotFound, + /// Invalid identity + InvalidIdentity, + /// IDGraph not found + UnknownIDGraph, + /// Identity is private + IdentityIsPrivate, + /// Identities empty + IdentitiesEmpty, + /// IDGraph hash does not match + IDGraphHashMismatch, + /// Missing IDGraph hash + IDGraphHashMissing, + } + + #[pallet::call] + impl Pallet { + #[pallet::call_index(0)] + #[pallet::weight((195_000_000, DispatchClass::Normal))] + pub fn link_identity( + origin: OriginFor, + who: Identity, + member_account: IDGraphMember, + maybe_id_graph_hash: Option, + ) -> DispatchResult { + let _ = T::TEECallOrigin::ensure_origin(origin)?; + ensure!( + !LinkedIdentityHashes::::contains_key(member_account.hash), + Error::::IdentityAlreadyLinked + ); + let who_account_id = match T::AccountIdConverter::convert(&who) { + Some(account_id) => account_id, + None => return Err(Error::::InvalidIdentity.into()), + }; + let identity_hash = member_account.hash; + let mut id_graph = Self::get_or_create_id_graph( + who.clone(), + who_account_id.clone(), + maybe_id_graph_hash, + )?; + id_graph + .try_push(member_account) + .map_err(|_| Error::::IDGraphLenLimitReached)?; + + LinkedIdentityHashes::::insert(identity_hash, ()); + IDGraphHashes::::insert(who_account_id.clone(), id_graph.graph_hash()); + IDGraphs::::insert(who_account_id.clone(), id_graph); + + Self::deposit_event(Event::IdentityLinked { + who: who_account_id, + identity: identity_hash, + }); + + Ok(()) + } + + #[pallet::call_index(4)] + #[pallet::weight((195_000_000, DispatchClass::Normal))] + pub fn remove_identities( + origin: OriginFor, + who: Identity, + identity_hashes: Vec, + ) -> DispatchResult { + let _ = T::TEECallOrigin::ensure_origin(origin)?; + ensure!(!identity_hashes.is_empty(), Error::::IdentitiesEmpty); + + let who_account_id = match T::AccountIdConverter::convert(&who) { + Some(account_id) => account_id, + None => return Err(Error::::InvalidIdentity.into()), + }; + + let mut id_graph_members = + IDGraphs::::get(&who_account_id).ok_or(Error::::UnknownIDGraph)?; + + id_graph_members.retain(|member| { + if identity_hashes.contains(&member.hash) { + LinkedIdentityHashes::::remove(member.hash); + false + } else { + true + } + }); + + if id_graph_members.is_empty() { + IDGraphs::::remove(&who_account_id); + } else { + IDGraphs::::insert(who_account_id.clone(), id_graph_members); + } + + Self::deposit_event(Event::IdentityRemoved { who: who_account_id, identity_hashes }); + + Ok(()) + } + + #[pallet::call_index(5)] + #[pallet::weight((195_000_000, DispatchClass::Normal))] + pub fn make_identity_public( + origin: OriginFor, + who: Identity, + identity_hash: H256, + public_identity: MemberIdentity, + ) -> DispatchResult { + let _ = T::TEECallOrigin::ensure_origin(origin)?; + ensure!(public_identity.is_public(), Error::::IdentityIsPrivate); + + let who_account_id = match T::AccountIdConverter::convert(&who) { + Some(account_id) => account_id, + None => return Err(Error::::InvalidIdentity.into()), + }; + + let mut id_graph_members = + IDGraphs::::get(&who_account_id).ok_or(Error::::UnknownIDGraph)?; + let id_graph_link = id_graph_members + .iter_mut() + .find(|member| member.hash == identity_hash) + .ok_or(Error::::IdentityNotFound)?; + id_graph_link.id = public_identity; + + IDGraphs::::insert(who_account_id.clone(), id_graph_members); + + Self::deposit_event(Event::IdentityMadePublic { who: who_account_id, identity_hash }); + + Ok(()) + } + } + + impl Pallet { + pub fn get_or_create_id_graph( + who: Identity, + who_account_id: T::AccountId, + maybe_id_graph_hash: Option, + ) -> Result, Error> { + match IDGraphs::::get(&who_account_id) { + Some(id_graph_members) => { + Self::verify_id_graph_hash(&who_account_id, maybe_id_graph_hash)?; + Ok(id_graph_members) + }, + None => Self::create_id_graph(who, who_account_id), + } + } + + fn verify_id_graph_hash( + who: &T::AccountId, + maybe_id_graph_hash: Option, + ) -> Result<(), Error> { + let current_id_graph_hash = + IDGraphHashes::::get(who).ok_or(Error::::IDGraphHashMissing)?; + match maybe_id_graph_hash { + Some(id_graph_hash) => { + ensure!( + current_id_graph_hash == id_graph_hash, + Error::::IDGraphHashMismatch + ); + }, + None => return Err(Error::::IDGraphHashMissing), + } + + Ok(()) + } + + fn create_id_graph( + owner_identity: Identity, + owner_account_id: T::AccountId, + ) -> Result, Error> { + let owner_identity_hash = + owner_identity.hash().map_err(|_| Error::::InvalidIdentity)?; + if LinkedIdentityHashes::::contains_key(owner_identity_hash) { + return Err(Error::::IdentityAlreadyLinked); + } + let mut id_graph_members: IDGraph = BoundedVec::new(); + id_graph_members + .try_push(IDGraphMember { + id: MemberIdentity::from(owner_identity.clone()), + hash: owner_identity_hash, + }) + .map_err(|_| Error::::IDGraphLenLimitReached)?; + LinkedIdentityHashes::::insert(owner_identity_hash, ()); + IDGraphs::::insert(owner_account_id.clone(), id_graph_members.clone()); + + Ok(id_graph_members) + } + } +} diff --git a/parachain/pallets/omni-account/src/mock.rs b/parachain/pallets/omni-account/src/mock.rs new file mode 100644 index 0000000000..fd2cdabf32 --- /dev/null +++ b/parachain/pallets/omni-account/src/mock.rs @@ -0,0 +1,195 @@ +use crate::{self as pallet_omni_account}; +use core_primitives::Identity; +use frame_support::{ + assert_ok, + pallet_prelude::EnsureOrigin, + traits::{ConstU16, ConstU32, ConstU64, Everything}, +}; +use frame_system::EnsureRoot; +pub use pallet_teebag::test_util::get_signer; +use pallet_teebag::test_util::{TEST8_CERT, TEST8_SIGNER_PUB, TEST8_TIMESTAMP, URL}; +use sp_core::H256; +use sp_keyring::AccountKeyring; +use sp_runtime::{ + traits::{BlakeTwo256, IdentifyAccount, IdentityLookup, Verify}, + BuildStorage, +}; +use sp_std::marker::PhantomData; + +pub type Signature = sp_runtime::MultiSignature; +pub type AccountId = <::Signer as IdentifyAccount>::AccountId; +pub type Balance = u64; +pub type SystemAccountId = ::AccountId; + +pub struct EnsureEnclaveSigner(PhantomData); +impl EnsureOrigin for EnsureEnclaveSigner +where + T: frame_system::Config + pallet_teebag::Config + pallet_timestamp::Config, + ::AccountId: From<[u8; 32]>, + ::Hash: From<[u8; 32]>, +{ + type Success = T::AccountId; + fn try_origin(o: T::RuntimeOrigin) -> Result { + o.into().and_then(|o| match o { + frame_system::RawOrigin::Signed(who) + if pallet_teebag::EnclaveRegistry::::contains_key(&who) => + { + Ok(who) + }, + r => Err(T::RuntimeOrigin::from(r)), + }) + } + + #[cfg(feature = "runtime-benchmarks")] + fn try_successful_origin() -> Result { + use pallet_teebag::test_util::{get_signer, TEST8_MRENCLAVE, TEST8_SIGNER_PUB}; + let signer: ::AccountId = get_signer(TEST8_SIGNER_PUB); + if !pallet_teebag::EnclaveRegistry::::contains_key(signer.clone()) { + assert_ok!(pallet_teebag::Pallet::::add_enclave( + &signer, + &pallet_teebag::Enclave::default().with_mrenclave(TEST8_MRENCLAVE), + )); + } + Ok(frame_system::RawOrigin::Signed(signer).into()) + } +} + +pub fn alice() -> AccountId { + AccountKeyring::Alice.to_account_id() +} + +pub fn bob() -> AccountId { + AccountKeyring::Bob.to_account_id() +} + +pub fn charlie() -> AccountId { + AccountKeyring::Charlie.to_account_id() +} + +frame_support::construct_runtime!( + pub enum TestRuntime + { + System: frame_system, + Balances: pallet_balances, + Teebag: pallet_teebag, + Timestamp: pallet_timestamp, + Utility: pallet_utility, + OmniAccount: pallet_omni_account, + } +); + +impl frame_system::Config for TestRuntime { + type BaseCallFilter = Everything; + type BlockWeights = (); + type BlockLength = (); + type Block = frame_system::mocking::MockBlock; + type DbWeight = (); + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + type Nonce = u64; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = ConstU64<250>; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = ConstU16<31>; + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; +} + +impl pallet_balances::Config for TestRuntime { + type MaxLocks = ConstU32<50>; + type MaxReserves = (); + type ReserveIdentifier = [u8; 8]; + type Balance = Balance; + type RuntimeEvent = RuntimeEvent; + type DustRemoval = (); + type ExistentialDeposit = ConstU64<1>; + type AccountStore = System; + type WeightInfo = (); + type FreezeIdentifier = (); + type MaxHolds = (); + type MaxFreezes = (); + type RuntimeHoldReason = (); +} + +impl pallet_timestamp::Config for TestRuntime { + type Moment = u64; + type OnTimestampSet = (); + type MinimumPeriod = ConstU64<10000>; + type WeightInfo = (); +} + +impl pallet_utility::Config for TestRuntime { + type RuntimeEvent = RuntimeEvent; + type RuntimeCall = RuntimeCall; + type PalletsOrigin = OriginCaller; + type WeightInfo = (); +} + +impl pallet_teebag::Config for TestRuntime { + type RuntimeEvent = RuntimeEvent; + type MomentsPerDay = ConstU64<86_400_000>; // [ms/d] + type SetAdminOrigin = EnsureRoot; + type MaxEnclaveIdentifier = ConstU32<3>; + type MaxAuthorizedEnclave = ConstU32<3>; + type WeightInfo = (); +} + +pub struct IdentityToAccountIdConverter; + +impl pallet_omni_account::AccountIdConverter for IdentityToAccountIdConverter { + fn convert(identity: &Identity) -> Option { + identity.to_account_id() + } +} + +impl pallet_omni_account::Config for TestRuntime { + type RuntimeEvent = RuntimeEvent; + type TEECallOrigin = EnsureEnclaveSigner; + type MaxIDGraphLength = ConstU32<3>; + type AccountIdConverter = IdentityToAccountIdConverter; +} + +pub fn get_tee_signer() -> SystemAccountId { + get_signer(TEST8_SIGNER_PUB) +} + +pub fn new_test_ext() -> sp_io::TestExternalities { + let system = frame_system::GenesisConfig::::default(); + let mut ext: sp_io::TestExternalities = RuntimeGenesisConfig { system, ..Default::default() } + .build_storage() + .unwrap() + .into(); + ext.execute_with(|| { + System::set_block_number(1); + let signer = get_tee_signer(); + assert_ok!(Teebag::set_admin(RuntimeOrigin::root(), signer.clone())); + assert_ok!(Teebag::set_mode( + RuntimeOrigin::signed(signer.clone()), + pallet_teebag::OperationalMode::Development + )); + + Timestamp::set_timestamp(TEST8_TIMESTAMP); + if !pallet_teebag::EnclaveRegistry::::contains_key(signer.clone()) { + assert_ok!(Teebag::register_enclave( + RuntimeOrigin::signed(signer), + pallet_teebag::WorkerType::Identity, + pallet_teebag::WorkerMode::Sidechain, + TEST8_CERT.to_vec(), + URL.to_vec(), + None, + None, + pallet_teebag::AttestationType::Ias, + )); + } + }); + ext +} diff --git a/parachain/pallets/omni-account/src/tests.rs b/parachain/pallets/omni-account/src/tests.rs new file mode 100644 index 0000000000..4b8b0c795e --- /dev/null +++ b/parachain/pallets/omni-account/src/tests.rs @@ -0,0 +1,538 @@ +use crate::{mock::*, IDGraphs, LinkedIdentityHashes, *}; +use core_primitives::Identity; +use frame_support::{assert_noop, assert_ok}; +use sp_runtime::traits::BadOrigin; +use sp_std::vec; + +#[test] +fn link_identity_works() { + new_test_ext().execute_with(|| { + let tee_signer = get_tee_signer(); + + let who = alice(); + let who_identity = Identity::from(who.clone()); + let who_identity_hash = who_identity.hash().unwrap(); + + let bob_member_account = IDGraphMember { + id: MemberIdentity::Private(bob().encode()), + hash: Identity::from(bob()).hash().unwrap(), + }; + let charlie_member_account = IDGraphMember { + id: MemberIdentity::Public(Identity::from(charlie())), + hash: Identity::from(charlie()).hash().unwrap(), + }; + + let expected_id_graph: IDGraph = BoundedVec::truncate_from(vec![ + IDGraphMember { + id: MemberIdentity::from(who_identity.clone()), + hash: who_identity_hash, + }, + bob_member_account.clone(), + ]); + let expected_id_graph_hash = H256::from(blake2_256( + &expected_id_graph + .iter() + .map(|member| member.hash) + .collect::>() + .encode(), + )); + + assert_ok!(OmniAccount::link_identity( + RuntimeOrigin::signed(tee_signer.clone()), + who_identity.clone(), + bob_member_account.clone(), + None + )); + System::assert_last_event( + Event::IdentityLinked { who: who.clone(), identity: bob_member_account.hash }.into(), + ); + + assert!(IDGraphs::::contains_key(&who)); + assert_eq!(IDGraphs::::get(&who).unwrap(), expected_id_graph); + assert_eq!(IDGraphHashes::::get(&who).unwrap(), expected_id_graph_hash); + + assert_ok!(OmniAccount::link_identity( + RuntimeOrigin::signed(tee_signer), + who_identity.clone(), + charlie_member_account.clone(), + Some(expected_id_graph_hash), + )); + System::assert_last_event( + Event::IdentityLinked { who: who.clone(), identity: charlie_member_account.hash } + .into(), + ); + + let expected_id_graph: IDGraph = BoundedVec::truncate_from(vec![ + IDGraphMember { + id: MemberIdentity::from(who_identity.clone()), + hash: who_identity_hash, + }, + bob_member_account.clone(), + charlie_member_account.clone(), + ]); + let expecte_id_graph_hash = H256::from(blake2_256( + &expected_id_graph + .iter() + .map(|member| member.hash) + .collect::>() + .encode(), + )); + + assert_eq!(IDGraphs::::get(&who).unwrap(), expected_id_graph); + assert_eq!(IDGraphHashes::::get(&who).unwrap(), expecte_id_graph_hash); + + assert!(LinkedIdentityHashes::::contains_key(bob_member_account.hash)); + assert!(LinkedIdentityHashes::::contains_key(charlie_member_account.hash)); + }); +} + +#[test] +fn link_identity_exising_id_graph_id_graph_hash_missing_works() { + new_test_ext().execute_with(|| { + let tee_signer = get_tee_signer(); + let who_identity = Identity::from(alice()); + + let bob_member_account = IDGraphMember { + id: MemberIdentity::Private(bob().encode()), + hash: Identity::from(bob()).hash().unwrap(), + }; + let charlie_member_account = IDGraphMember { + id: MemberIdentity::Public(Identity::from(charlie())), + hash: Identity::from(charlie()).hash().unwrap(), + }; + + // IDGraph gets created with the first identity + assert_ok!(OmniAccount::link_identity( + RuntimeOrigin::signed(tee_signer.clone()), + who_identity.clone(), + bob_member_account, + None + )); + + // to mutate IDGraph with a new identity, the current id_graph_hash must be provided + assert_noop!( + OmniAccount::link_identity( + RuntimeOrigin::signed(tee_signer), + who_identity, + charlie_member_account, + None + ), + Error::::IDGraphHashMissing + ); + }); +} + +#[test] +fn link_identity_origin_check_works() { + new_test_ext().execute_with(|| { + let who = Identity::from(alice()); + let member_account = IDGraphMember { + id: MemberIdentity::Private(vec![1, 2, 3]), + hash: H256::from(blake2_256(&[1, 2, 3])), + }; + + assert_noop!( + OmniAccount::link_identity(RuntimeOrigin::signed(bob()), who, member_account, None), + BadOrigin + ); + }); +} + +#[test] +fn link_identity_already_linked_works() { + new_test_ext().execute_with(|| { + let tee_signer = get_tee_signer(); + let who = alice(); + let who_identity = Identity::from(who.clone()); + + let member_account = IDGraphMember { + id: MemberIdentity::Public(Identity::from(bob())), + hash: Identity::from(bob()).hash().unwrap(), + }; + + assert_ok!(OmniAccount::link_identity( + RuntimeOrigin::signed(tee_signer.clone()), + who_identity.clone(), + member_account.clone(), + None + )); + assert_noop!( + OmniAccount::link_identity( + RuntimeOrigin::signed(tee_signer.clone()), + who_identity.clone(), + member_account, + None + ), + Error::::IdentityAlreadyLinked + ); + + // intent to create a new id_graph with an identity that is already linked + let who = Identity::from(bob()); + let alice_member_account = IDGraphMember { + id: MemberIdentity::Public(Identity::from(alice())), + hash: Identity::from(alice()).hash().unwrap(), + }; + assert_noop!( + OmniAccount::link_identity( + RuntimeOrigin::signed(tee_signer), + who.clone(), + alice_member_account, + None + ), + Error::::IdentityAlreadyLinked + ); + }); +} + +#[test] +fn link_identity_id_graph_len_limit_reached_works() { + new_test_ext().execute_with(|| { + let tee_signer = get_tee_signer(); + + let who = alice(); + let who_identity = Identity::from(who.clone()); + let who_identity_hash = who_identity.hash().unwrap(); + + let member_account_2 = IDGraphMember { + id: MemberIdentity::Private(vec![1, 2, 3]), + hash: H256::from(blake2_256(&[1, 2, 3])), + }; + let member_account_3 = IDGraphMember { + id: MemberIdentity::Private(vec![4, 5, 6]), + hash: H256::from(blake2_256(&[4, 5, 6])), + }; + + let id_graph: IDGraph = BoundedVec::truncate_from(vec![ + IDGraphMember { + id: MemberIdentity::from(who_identity.clone()), + hash: who_identity_hash, + }, + member_account_2.clone(), + member_account_3.clone(), + ]); + let id_graph_hash = H256::from(blake2_256(&id_graph.encode())); + + IDGraphs::::insert(who.clone(), id_graph.clone()); + IDGraphHashes::::insert(who.clone(), id_graph_hash); + + assert_noop!( + OmniAccount::link_identity( + RuntimeOrigin::signed(tee_signer), + who_identity, + IDGraphMember { + id: MemberIdentity::Private(vec![7, 8, 9]), + hash: H256::from(blake2_256(&[7, 8, 9])), + }, + Some(id_graph_hash), + ), + Error::::IDGraphLenLimitReached + ); + }); +} + +#[test] +fn link_identity_id_graph_hash_mismatch_works() { + new_test_ext().execute_with(|| { + let tee_signer = get_tee_signer(); + + let who = alice(); + let who_identity = Identity::from(who.clone()); + let who_identity_hash = who_identity.hash().unwrap(); + + let member_account = IDGraphMember { + id: MemberIdentity::Private(vec![1, 2, 3]), + hash: H256::from(blake2_256(&[1, 2, 3])), + }; + + let id_graph: IDGraph = BoundedVec::truncate_from(vec![ + IDGraphMember { + id: MemberIdentity::from(who_identity.clone()), + hash: who_identity_hash, + }, + member_account.clone(), + ]); + let id_graph_hash = H256::from(blake2_256( + &id_graph.iter().map(|member| member.hash).collect::>().encode(), + )); + + assert_ok!(OmniAccount::link_identity( + RuntimeOrigin::signed(tee_signer.clone()), + who_identity.clone(), + member_account.clone(), + None + )); + + assert_eq!(IDGraphs::::get(&who).unwrap(), id_graph); + assert_eq!(IDGraphHashes::::get(&who).unwrap(), id_graph_hash); + + // link another identity to the id_graph with the correct id_graph_hash + assert_ok!(OmniAccount::link_identity( + RuntimeOrigin::signed(tee_signer.clone()), + who_identity.clone(), + IDGraphMember { + id: MemberIdentity::Private(vec![4, 5, 6]), + hash: H256::from(blake2_256(&[4, 5, 6])), + }, + Some(id_graph_hash), + )); + + let id_graph: IDGraph = BoundedVec::truncate_from(vec![ + IDGraphMember { + id: MemberIdentity::from(who_identity.clone()), + hash: who_identity_hash, + }, + member_account.clone(), + IDGraphMember { + id: MemberIdentity::Private(vec![4, 5, 6]), + hash: H256::from(blake2_256(&[4, 5, 6])), + }, + ]); + assert_eq!(IDGraphs::::get(&who).unwrap(), id_graph); + + // attempt to link an identity with an old id_graph_hash + assert_noop!( + OmniAccount::link_identity( + RuntimeOrigin::signed(tee_signer), + who_identity, + IDGraphMember { + id: MemberIdentity::Private(vec![7, 8, 9]), + hash: H256::from(blake2_256(&[7, 8, 9])), + }, + Some(id_graph_hash), + ), + Error::::IDGraphHashMismatch + ); + }); +} + +#[test] +fn remove_identity_works() { + new_test_ext().execute_with(|| { + let tee_signer = get_tee_signer(); + let who = alice(); + let who_identity = Identity::from(who.clone()); + let who_identity_hash = who_identity.hash().unwrap(); + + let member_account = IDGraphMember { + id: MemberIdentity::Private(vec![1, 2, 3]), + hash: H256::from(blake2_256(&[1, 2, 3])), + }; + let identity_hash = member_account.hash; + let identities_to_remove = vec![identity_hash]; + + assert_ok!(OmniAccount::link_identity( + RuntimeOrigin::signed(tee_signer.clone()), + who_identity.clone(), + member_account.clone(), + None + )); + assert_ok!(OmniAccount::remove_identities( + RuntimeOrigin::signed(tee_signer.clone()), + who_identity.clone(), + identities_to_remove.clone() + )); + System::assert_last_event( + Event::IdentityRemoved { who: who.clone(), identity_hashes: identities_to_remove } + .into(), + ); + + let expected_id_graph: IDGraph = + BoundedVec::truncate_from(vec![IDGraphMember { + id: MemberIdentity::Public(who_identity.clone()), + hash: who_identity_hash, + }]); + + assert_eq!(IDGraphs::::get(&who).unwrap(), expected_id_graph); + assert!(!LinkedIdentityHashes::::contains_key(identity_hash)); + + assert_ok!(OmniAccount::remove_identities( + RuntimeOrigin::signed(tee_signer.clone()), + who_identity.clone(), + vec![who_identity_hash], + )); + System::assert_last_event( + Event::IdentityRemoved { who: who.clone(), identity_hashes: vec![who_identity_hash] } + .into(), + ); + + assert!(!IDGraphs::::contains_key(&who)); + }); +} + +#[test] +fn remove_identity_empty_identity_check_works() { + new_test_ext().execute_with(|| { + let tee_signer = get_tee_signer(); + let who = Identity::from(alice()); + + assert_ok!(OmniAccount::link_identity( + RuntimeOrigin::signed(tee_signer.clone()), + who.clone(), + IDGraphMember { + id: MemberIdentity::Private(vec![1, 2, 3]), + hash: H256::from(blake2_256(&[1, 2, 3])), + }, + None + )); + assert_noop!( + OmniAccount::remove_identities(RuntimeOrigin::signed(tee_signer.clone()), who, vec![],), + Error::::IdentitiesEmpty + ); + }); +} + +#[test] +fn remove_identity_origin_check_works() { + new_test_ext().execute_with(|| { + let who = Identity::from(alice()); + let identities_to_remove = vec![H256::from(blake2_256(&[1, 2, 3]))]; + + assert_noop!( + OmniAccount::remove_identities(RuntimeOrigin::signed(bob()), who, identities_to_remove), + BadOrigin + ); + }); +} + +#[test] +fn make_identity_public_works() { + new_test_ext().execute_with(|| { + let tee_signer = get_tee_signer(); + let who = alice(); + let who_identity = Identity::from(who.clone()); + + let private_identity = MemberIdentity::Private(vec![1, 2, 3]); + let public_identity = MemberIdentity::Public(Identity::from(bob())); + let identity_hash = + H256::from(blake2_256(&Identity::from(bob()).to_did().unwrap().encode())); + + assert_ok!(OmniAccount::link_identity( + RuntimeOrigin::signed(tee_signer.clone()), + who_identity.clone(), + IDGraphMember { id: private_identity.clone(), hash: identity_hash }, + None + )); + + let expected_id_graph: IDGraph = BoundedVec::truncate_from(vec![ + IDGraphMember { + id: MemberIdentity::Public(who_identity.clone()), + hash: who_identity.hash().unwrap(), + }, + IDGraphMember { id: private_identity.clone(), hash: identity_hash }, + ]); + assert_eq!(IDGraphs::::get(&who).unwrap(), expected_id_graph); + + assert_ok!(OmniAccount::make_identity_public( + RuntimeOrigin::signed(tee_signer.clone()), + who_identity.clone(), + identity_hash, + public_identity.clone() + )); + System::assert_last_event( + Event::IdentityMadePublic { who: who.clone(), identity_hash }.into(), + ); + + let expected_id_graph: IDGraph = BoundedVec::truncate_from(vec![ + IDGraphMember { + id: MemberIdentity::Public(who_identity.clone()), + hash: who_identity.hash().unwrap(), + }, + IDGraphMember { id: public_identity.clone(), hash: identity_hash }, + ]); + assert_eq!(IDGraphs::::get(&who).unwrap(), expected_id_graph); + }); +} + +#[test] +fn make_identity_public_origin_check_works() { + new_test_ext().execute_with(|| { + let who = Identity::from(alice()); + let identity = Identity::from(bob()); + let identity_hash = identity.hash().unwrap(); + let public_identity = MemberIdentity::Public(identity.clone()); + + assert_noop!( + OmniAccount::make_identity_public( + RuntimeOrigin::signed(bob()), + who, + identity_hash, + public_identity + ), + BadOrigin + ); + }); +} + +#[test] +fn make_identity_public_identity_not_found_works() { + new_test_ext().execute_with(|| { + let tee_signer = get_tee_signer(); + let who = Identity::from(alice()); + + let private_identity = MemberIdentity::Private(vec![1, 2, 3]); + let identity = Identity::from(bob()); + let public_identity = MemberIdentity::Public(identity.clone()); + let identity_hash = + H256::from(blake2_256(&Identity::from(bob()).to_did().unwrap().encode())); + + assert_noop!( + OmniAccount::make_identity_public( + RuntimeOrigin::signed(tee_signer.clone()), + who.clone(), + identity_hash, + public_identity.clone() + ), + Error::::UnknownIDGraph + ); + + assert_ok!(OmniAccount::link_identity( + RuntimeOrigin::signed(tee_signer.clone()), + who.clone(), + IDGraphMember { id: private_identity.clone(), hash: identity_hash }, + None + )); + + let charlie_identity = Identity::from(charlie()); + let other_identity = MemberIdentity::Public(charlie_identity.clone()); + let other_identity_hash = + H256::from(blake2_256(&charlie_identity.to_did().unwrap().encode())); + + assert_noop!( + OmniAccount::make_identity_public( + RuntimeOrigin::signed(tee_signer), + who, + other_identity_hash, + other_identity, + ), + Error::::IdentityNotFound + ); + }); +} + +#[test] +fn make_identity_public_identity_is_private_check_works() { + new_test_ext().execute_with(|| { + let tee_signer = get_tee_signer(); + let who = Identity::from(alice()); + + let private_identity = MemberIdentity::Private(vec![1, 2, 3]); + let identity_hash = Identity::from(bob()).hash().unwrap(); + + assert_ok!(OmniAccount::link_identity( + RuntimeOrigin::signed(tee_signer.clone()), + who.clone(), + IDGraphMember { id: private_identity.clone(), hash: identity_hash }, + None + )); + + assert_noop!( + OmniAccount::make_identity_public( + RuntimeOrigin::signed(tee_signer), + who, + identity_hash, + private_identity, + ), + Error::::IdentityIsPrivate + ); + }); +} diff --git a/parachain/runtime/litentry/Cargo.toml b/parachain/runtime/litentry/Cargo.toml index 4b3d3f3b52..3b3a3bc1ba 100644 --- a/parachain/runtime/litentry/Cargo.toml +++ b/parachain/runtime/litentry/Cargo.toml @@ -87,6 +87,7 @@ pallet-evm-assertions = { workspace = true } pallet-extrinsic-filter = { workspace = true } pallet-group = { workspace = true } pallet-identity-management = { workspace = true } +pallet-omni-account = { workspace = true } pallet-parachain-staking = { workspace = true } pallet-score-staking = { workspace = true } pallet-teebag = { workspace = true } diff --git a/parachain/runtime/litentry/src/lib.rs b/parachain/runtime/litentry/src/lib.rs index 176f7164d3..1d81366913 100644 --- a/parachain/runtime/litentry/src/lib.rs +++ b/parachain/runtime/litentry/src/lib.rs @@ -18,7 +18,7 @@ #![allow(clippy::identity_op)] #![allow(clippy::items_after_test_module)] // `construct_runtime!` does a lot of recursion and requires us to increase the limit to 256. -#![recursion_limit = "256"] +#![recursion_limit = "512"] #[cfg(feature = "runtime-benchmarks")] #[macro_use] @@ -59,8 +59,8 @@ use xcm_executor::XcmExecutor; pub use constants::currency::deposit; pub use core_primitives::{ - opaque, AccountId, Amount, AssetId, Balance, BlockNumber, Hash, Header, Nonce, Signature, DAYS, - HOURS, LITENTRY_PARA_ID, MINUTES, SLOT_DURATION, + opaque, AccountId, Amount, AssetId, Balance, BlockNumber, Hash, Header, Identity, Nonce, + Signature, DAYS, HOURS, LITENTRY_PARA_ID, MINUTES, SLOT_DURATION, }; pub use runtime_common::currency::*; use runtime_common::{ @@ -951,6 +951,21 @@ impl pallet_identity_management::Config for Runtime { type MaxOIDCClientRedirectUris = ConstU32<10>; } +pub struct IdentityToAccountIdConverter; + +impl pallet_omni_account::AccountIdConverter for IdentityToAccountIdConverter { + fn convert(identity: &Identity) -> Option { + identity.to_account_id() + } +} + +impl pallet_omni_account::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type TEECallOrigin = EnsureEnclaveSigner; + type MaxIDGraphLength = ConstU32<64>; + type AccountIdConverter = IdentityToAccountIdConverter; +} + impl pallet_bitacross::Config for Runtime { type RuntimeEvent = RuntimeEvent; type TEECallOrigin = EnsureEnclaveSigner; @@ -1209,6 +1224,7 @@ construct_runtime! { VCManagement: pallet_vc_management = 81, IMPExtrinsicWhitelist: pallet_group:: = 82, VCMPExtrinsicWhitelist: pallet_group:: = 83, + OmniAccount: pallet_omni_account = 84, // Frontier EVM: pallet_evm = 120, diff --git a/parachain/runtime/paseo/Cargo.toml b/parachain/runtime/paseo/Cargo.toml index 09c035fa95..eb194daa05 100644 --- a/parachain/runtime/paseo/Cargo.toml +++ b/parachain/runtime/paseo/Cargo.toml @@ -87,6 +87,7 @@ pallet-evm-assertions = { workspace = true } pallet-extrinsic-filter = { workspace = true } pallet-group = { workspace = true } pallet-identity-management = { workspace = true } +pallet-omni-account = { workspace = true } pallet-parachain-staking = { workspace = true } pallet-score-staking = { workspace = true } pallet-teebag = { workspace = true } diff --git a/parachain/runtime/paseo/src/lib.rs b/parachain/runtime/paseo/src/lib.rs index e18997cdff..1b77269ae2 100644 --- a/parachain/runtime/paseo/src/lib.rs +++ b/parachain/runtime/paseo/src/lib.rs @@ -69,8 +69,8 @@ use xcm_executor::XcmExecutor; pub use constants::currency::deposit; pub use core_primitives::{ - opaque, AccountId, Amount, AssetId, Balance, BlockNumber, Hash, Header, Nonce, Signature, DAYS, - HOURS, MINUTES, SLOT_DURATION, + opaque, AccountId, Amount, AssetId, Balance, BlockNumber, Hash, Header, Identity, Nonce, + Signature, DAYS, HOURS, MINUTES, SLOT_DURATION, }; pub use runtime_common::currency::*; @@ -994,6 +994,21 @@ impl pallet_identity_management::Config for Runtime { type MaxOIDCClientRedirectUris = ConstU32<10>; } +pub struct IdentityToAccountIdConverter; + +impl pallet_omni_account::AccountIdConverter for IdentityToAccountIdConverter { + fn convert(identity: &Identity) -> Option { + identity.to_account_id() + } +} + +impl pallet_omni_account::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type TEECallOrigin = EnsureEnclaveSigner; + type MaxIDGraphLength = ConstU32<64>; + type AccountIdConverter = IdentityToAccountIdConverter; +} + impl pallet_bitacross::Config for Runtime { type RuntimeEvent = RuntimeEvent; type TEECallOrigin = EnsureEnclaveSigner; @@ -1254,6 +1269,8 @@ construct_runtime! { // New Bridge Added AssetsHandler: pallet_assets_handler = 76, + OmniAccount: pallet_omni_account = 84, + // TEE Teebag: pallet_teebag = 93, diff --git a/parachain/runtime/rococo/Cargo.toml b/parachain/runtime/rococo/Cargo.toml index 0da86e3f28..231467f5bf 100644 --- a/parachain/runtime/rococo/Cargo.toml +++ b/parachain/runtime/rococo/Cargo.toml @@ -87,6 +87,7 @@ pallet-evm-assertions = { workspace = true } pallet-extrinsic-filter = { workspace = true } pallet-group = { workspace = true } pallet-identity-management = { workspace = true } +pallet-omni-account = { workspace = true } pallet-parachain-staking = { workspace = true } pallet-score-staking = { workspace = true } pallet-teebag = { workspace = true } diff --git a/parachain/runtime/rococo/src/lib.rs b/parachain/runtime/rococo/src/lib.rs index 334234027c..faf7106e51 100644 --- a/parachain/runtime/rococo/src/lib.rs +++ b/parachain/runtime/rococo/src/lib.rs @@ -68,8 +68,8 @@ use xcm_executor::XcmExecutor; pub use constants::currency::deposit; pub use core_primitives::{ - opaque, AccountId, Amount, AssetId, Balance, BlockNumber, Hash, Header, Nonce, Signature, DAYS, - HOURS, MINUTES, ROCOCO_PARA_ID, SLOT_DURATION, + opaque, AccountId, Amount, AssetId, Balance, BlockNumber, Hash, Header, Identity, Nonce, + Signature, DAYS, HOURS, MINUTES, ROCOCO_PARA_ID, SLOT_DURATION, }; pub use runtime_common::currency::*; @@ -993,6 +993,21 @@ impl pallet_identity_management::Config for Runtime { type MaxOIDCClientRedirectUris = ConstU32<10>; } +pub struct IdentityToAccountIdConverter; + +impl pallet_omni_account::AccountIdConverter for IdentityToAccountIdConverter { + fn convert(identity: &Identity) -> Option { + identity.to_account_id() + } +} + +impl pallet_omni_account::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type TEECallOrigin = EnsureEnclaveSigner; + type MaxIDGraphLength = ConstU32<64>; + type AccountIdConverter = IdentityToAccountIdConverter; +} + impl pallet_bitacross::Config for Runtime { type RuntimeEvent = RuntimeEvent; type TEECallOrigin = EnsureEnclaveSigner; @@ -1253,6 +1268,8 @@ construct_runtime! { // New Bridge Added AssetsHandler: pallet_assets_handler = 76, + OmniAccount: pallet_omni_account = 84, + // TEE Teebag: pallet_teebag = 93,