diff --git a/Cargo.lock b/Cargo.lock index 60500c064..66e6713b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9906,6 +9906,7 @@ dependencies = [ "log", "pallet-authorship", "pallet-balances", + "pallet-deposit-storage", "pallet-did-lookup", "pallet-dip-provider", "pallet-membership", diff --git a/crates/kilt-dip-primitives/src/merkle/v0.rs b/crates/kilt-dip-primitives/src/merkle/v0.rs index d6124bb76..a5719e4c8 100644 --- a/crates/kilt-dip-primitives/src/merkle/v0.rs +++ b/crates/kilt-dip-primitives/src/merkle/v0.rs @@ -168,6 +168,12 @@ pub struct TimeBoundDidSignature { pub(crate) valid_until: BlockNumber, } +impl TimeBoundDidSignature { + pub fn new(signature: DidSignature, valid_until: BlockNumber) -> Self { + Self { signature, valid_until } + } +} + #[cfg(feature = "runtime-benchmarks")] impl kilt_support::traits::GetWorstCase for TimeBoundDidSignature where @@ -770,6 +776,38 @@ pub struct DipDidProofWithVerifiedSubjectCommitment< pub(crate) signature: TimeBoundDidSignature, } +impl< + Commitment, + KiltDidKeyId, + KiltAccountId, + KiltBlockNumber, + KiltWeb3Name, + KiltLinkableAccountId, + ConsumerBlockNumber, + > + DipDidProofWithVerifiedSubjectCommitment< + Commitment, + KiltDidKeyId, + KiltAccountId, + KiltBlockNumber, + KiltWeb3Name, + KiltLinkableAccountId, + ConsumerBlockNumber, + > +{ + pub fn new( + dip_commitment: Commitment, + dip_proof: DidMerkleProof, + signature: TimeBoundDidSignature, + ) -> Self { + Self { + dip_commitment, + dip_proof, + signature, + } + } +} + impl< Commitment, KiltDidKeyId, @@ -1164,7 +1202,7 @@ impl< } /// Relationship of a key to a DID Document. -#[derive(Clone, Copy, Debug, Encode, Decode, PartialEq, Eq, TypeInfo, MaxEncodedLen)] +#[derive(Clone, Copy, Debug, Encode, Decode, PartialEq, Eq, PartialOrd, Ord, TypeInfo, MaxEncodedLen)] pub enum DidKeyRelationship { Encryption, Verification(DidVerificationKeyRelationship), @@ -1275,7 +1313,7 @@ where /// The details of a DID key after it has been successfully verified in a Merkle /// proof. -#[derive(Clone, Copy, Debug, Encode, Decode, PartialEq, Eq, TypeInfo, MaxEncodedLen)] +#[derive(Clone, Copy, Debug, Encode, Decode, PartialEq, Eq, PartialOrd, Ord, TypeInfo, MaxEncodedLen)] pub struct RevealedDidKey { /// The key ID, according to the provider's definition. pub id: KeyId, @@ -1288,7 +1326,7 @@ pub struct RevealedDidKey { /// The details of a web3name after it has been successfully verified in a /// Merkle proof. -#[derive(Clone, Copy, Debug, Encode, Decode, PartialEq, Eq, TypeInfo, MaxEncodedLen)] +#[derive(Clone, Copy, Debug, Encode, Decode, PartialEq, Eq, PartialOrd, Ord, TypeInfo, MaxEncodedLen)] pub struct RevealedWeb3Name { /// The web3name. pub web3_name: Web3Name, @@ -1299,5 +1337,5 @@ pub struct RevealedWeb3Name { /// The details of an account after it has been successfully verified in a /// Merkle proof. -#[derive(Clone, Copy, Debug, Encode, Decode, PartialEq, Eq, TypeInfo, MaxEncodedLen)] +#[derive(Clone, Copy, Debug, Encode, Decode, PartialEq, Eq, PartialOrd, Ord, TypeInfo, MaxEncodedLen)] pub struct RevealedAccountId(pub AccountId); diff --git a/dip-template/runtimes/dip-consumer/Cargo.toml b/dip-template/runtimes/dip-consumer/Cargo.toml index e2f22e4f7..5a2986fbd 100644 --- a/dip-template/runtimes/dip-consumer/Cargo.toml +++ b/dip-template/runtimes/dip-consumer/Cargo.toml @@ -112,7 +112,7 @@ std = [ "pallet-collator-selection/std", "parachain-info/std", "rococo-runtime/std", - "frame-benchmarking/std", + "frame-benchmarking?/std", "frame-system-benchmarking?/std", ] diff --git a/pallets/did/src/lib.rs b/pallets/did/src/lib.rs index c84c8f62c..94c630e91 100644 --- a/pallets/did/src/lib.rs +++ b/pallets/did/src/lib.rs @@ -1463,7 +1463,7 @@ pub mod pallet { /// Deletes DID details from storage, including its linked service /// endpoints, adds the identifier to the blacklisted DIDs and frees the /// deposit. - pub(crate) fn delete_did(did_subject: DidIdentifierOf, endpoints_to_remove: u32) -> DispatchResult { + pub fn delete_did(did_subject: DidIdentifierOf, endpoints_to_remove: u32) -> DispatchResult { let current_endpoints_count = DidEndpointsCount::::get(&did_subject); ensure!( current_endpoints_count <= endpoints_to_remove, diff --git a/pallets/pallet-deposit-storage/src/deposit/mock.rs b/pallets/pallet-deposit-storage/src/deposit/mock.rs new file mode 100644 index 000000000..ebea257cb --- /dev/null +++ b/pallets/pallet-deposit-storage/src/deposit/mock.rs @@ -0,0 +1,177 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The KILT Blockchain is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +use frame_support::{ + construct_runtime, + sp_runtime::{ + testing::H256, + traits::{BlakeTwo256, IdentityLookup}, + AccountId32, + }, + traits::{ConstU128, ConstU16, ConstU32, ConstU64, Currency, Everything, Get}, +}; +use frame_system::{mocking::MockBlock, EnsureSigned}; +use pallet_dip_provider::{DefaultIdentityCommitmentGenerator, DefaultIdentityProvider, IdentityCommitmentVersion}; +use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; +use scale_info::TypeInfo; +use sp_runtime::RuntimeDebug; + +use crate::{ + self as storage_deposit_pallet, DepositEntryOf, DepositKeyOf, FixedDepositCollectorViaDepositsPallet, Pallet, +}; + +pub(crate) type Balance = u128; + +#[derive(Encode, Decode, MaxEncodedLen, TypeInfo, Clone, PartialEq, Eq, RuntimeDebug, Default)] +pub enum DepositNamespaces { + #[default] + ExampleNamespace, +} + +impl Get for DepositNamespaces { + fn get() -> DepositNamespaces { + Self::ExampleNamespace + } +} + +construct_runtime!( + pub struct TestRuntime { + System: frame_system, + Balances: pallet_balances, + DipProvider: pallet_dip_provider, + StorageDepositPallet: storage_deposit_pallet, + } +); + +pub(crate) const SUBJECT: AccountId32 = AccountId32::new([100u8; 32]); +pub(crate) const SUBMITTER: AccountId32 = AccountId32::new([200u8; 32]); + +impl frame_system::Config for TestRuntime { + type AccountData = pallet_balances::AccountData; + type AccountId = AccountId32; + type BaseCallFilter = Everything; + type Block = MockBlock; + type BlockHashCount = ConstU64<256>; + type BlockLength = (); + type BlockWeights = (); + type DbWeight = (); + type Hash = H256; + type Hashing = BlakeTwo256; + type Lookup = IdentityLookup; + type MaxConsumers = ConstU32<16>; + type Nonce = u64; + type OnKilledAccount = (); + type OnNewAccount = (); + type OnSetCode = (); + type PalletInfo = PalletInfo; + type RuntimeCall = RuntimeCall; + type RuntimeEvent = RuntimeEvent; + type RuntimeOrigin = RuntimeOrigin; + type SS58Prefix = ConstU16<1>; + type SystemWeightInfo = (); + type Version = (); +} + +impl pallet_balances::Config for TestRuntime { + type FreezeIdentifier = RuntimeFreezeReason; + type RuntimeHoldReason = RuntimeHoldReason; + type MaxFreezes = ConstU32<50>; + type MaxHolds = ConstU32<50>; + type Balance = Balance; + type DustRemoval = (); + type RuntimeEvent = RuntimeEvent; + type ExistentialDeposit = ConstU128<500>; + type AccountStore = System; + type WeightInfo = (); + type MaxLocks = ConstU32<50>; + type MaxReserves = ConstU32<50>; + type ReserveIdentifier = [u8; 8]; +} + +pub(crate) type DepositCollectorHook = FixedDepositCollectorViaDepositsPallet< + DepositNamespaces, + ConstU128<1_000>, + ( + ::Identifier, + AccountId32, + IdentityCommitmentVersion, + ), +>; + +impl pallet_dip_provider::Config for TestRuntime { + type CommitOrigin = AccountId32; + type CommitOriginCheck = EnsureSigned; + type Identifier = AccountId32; + type IdentityCommitmentGenerator = DefaultIdentityCommitmentGenerator; + type IdentityProvider = DefaultIdentityProvider; + type ProviderHooks = DepositCollectorHook; + type RuntimeEvent = RuntimeEvent; + type WeightInfo = (); +} + +impl crate::Config for TestRuntime { + type CheckOrigin = EnsureSigned; + type Currency = Balances; + type DepositHooks = (); + type RuntimeEvent = RuntimeEvent; + type RuntimeHoldReason = RuntimeHoldReason; + type MaxKeyLength = ConstU32<256>; + type Namespace = DepositNamespaces; + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHooks = (); + type WeightInfo = (); +} + +#[derive(Default)] +pub(crate) struct ExtBuilder( + Vec<(AccountId32, Balance)>, + Vec<(DepositKeyOf, DepositEntryOf)>, +); + +impl ExtBuilder { + pub(crate) fn with_balances(mut self, balances: Vec<(AccountId32, Balance)>) -> Self { + self.0 = balances; + self + } + + pub(crate) fn with_deposits( + mut self, + deposits: Vec<(DepositKeyOf, DepositEntryOf)>, + ) -> Self { + self.1 = deposits; + self + } + + pub(crate) fn build(self) -> sp_io::TestExternalities { + let mut ext = sp_io::TestExternalities::default(); + + ext.execute_with(|| { + for (account, balance) in self.0 { + Balances::make_free_balance_be(&account, balance); + } + + for (deposit_key, deposit_entry) in self.1 { + // Add existential deposit + deposit amount. + Balances::make_free_balance_be(&deposit_entry.deposit.owner, 500 + deposit_entry.deposit.amount); + Pallet::::add_deposit(DepositNamespaces::get(), deposit_key, deposit_entry).unwrap(); + } + }); + + ext + } +} diff --git a/pallets/pallet-deposit-storage/src/deposit.rs b/pallets/pallet-deposit-storage/src/deposit/mod.rs similarity index 90% rename from pallets/pallet-deposit-storage/src/deposit.rs rename to pallets/pallet-deposit-storage/src/deposit/mod.rs index 53f1a2585..1bad8160f 100644 --- a/pallets/pallet-deposit-storage/src/deposit.rs +++ b/pallets/pallet-deposit-storage/src/deposit/mod.rs @@ -32,6 +32,11 @@ use sp_std::marker::PhantomData; use crate::{BalanceOf, Config, Error, HoldReason, Pallet}; +#[cfg(test)] +mod mock; +#[cfg(test)] +mod tests; + /// Details associated to an on-chain deposit. #[derive(Clone, Debug, Encode, Decode, Eq, PartialEq, Ord, PartialOrd, TypeInfo, MaxEncodedLen)] pub struct DepositEntry { @@ -107,11 +112,12 @@ where }, reason: HoldReason::Deposit.into(), }; - Pallet::::add_deposit(namespace, key, deposit_entry).map_err(|e| match e { - pallet_error if pallet_error == DispatchError::from(Error::::DepositExisting) => { + Pallet::::add_deposit(namespace, key, deposit_entry).map_err(|e| { + if e == DispatchError::from(Error::::DepositExisting) { FixedDepositCollectorViaDepositsPalletError::DepositAlreadyTaken - } - _ => { + } else if e == DispatchError::from(Error::::FailedToHold) { + FixedDepositCollectorViaDepositsPalletError::FailedToHold + } else { log::error!( "Error {:#?} should not be generated inside `on_identity_committed` hook.", e @@ -140,11 +146,15 @@ where ); FixedDepositCollectorViaDepositsPalletError::Internal })?; - Pallet::::remove_deposit(&namespace, &key, None).map_err(|e| match e { - pallet_error if pallet_error == DispatchError::from(Error::::DepositNotFound) => { + // We don't set any expected owner for the deposit on purpose, since this hook + // assumes the dip-provider pallet has performed all the access control logic + // necessary. + Pallet::::remove_deposit(&namespace, &key, None).map_err(|e| { + if e == DispatchError::from(Error::::DepositNotFound) { FixedDepositCollectorViaDepositsPalletError::DepositNotFound - } - _ => { + } else if e == DispatchError::from(Error::::FailedToRelease) { + FixedDepositCollectorViaDepositsPalletError::FailedToRelease + } else { log::error!( "Error {:#?} should not be generated inside `on_commitment_removed` hook.", e diff --git a/pallets/pallet-deposit-storage/src/deposit/tests/mod.rs b/pallets/pallet-deposit-storage/src/deposit/tests/mod.rs new file mode 100644 index 000000000..047b5c8cd --- /dev/null +++ b/pallets/pallet-deposit-storage/src/deposit/tests/mod.rs @@ -0,0 +1,20 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The KILT Blockchain is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +mod on_commitment_removed; +mod on_identity_committed; diff --git a/pallets/pallet-deposit-storage/src/deposit/tests/on_commitment_removed.rs b/pallets/pallet-deposit-storage/src/deposit/tests/on_commitment_removed.rs new file mode 100644 index 000000000..cecf9ed40 --- /dev/null +++ b/pallets/pallet-deposit-storage/src/deposit/tests/on_commitment_removed.rs @@ -0,0 +1,153 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The KILT Blockchain is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +use frame_support::{ + assert_noop, assert_ok, + traits::{fungible::InspectHold, Get}, +}; +use kilt_support::Deposit; +use pallet_dip_provider::{traits::ProviderHooks, IdentityCommitmentOf, IdentityCommitmentVersion}; +use parity_scale_codec::Encode; +use sp_runtime::traits::Zero; + +use crate::{ + deposit::{ + mock::{DepositCollectorHook, DepositNamespaces, ExtBuilder, TestRuntime, SUBJECT, SUBMITTER}, + DepositEntry, FixedDepositCollectorViaDepositsPalletError, + }, + DepositKeyOf, HoldReason, Pallet, +}; + +#[test] +fn on_commitment_removed_successful() { + let namespace = DepositNamespaces::get(); + let key: DepositKeyOf = (SUBJECT, SUBMITTER, 0 as IdentityCommitmentVersion) + .encode() + .try_into() + .unwrap(); + ExtBuilder::default() + .with_deposits(vec![( + key.clone(), + DepositEntry { + deposit: Deposit { + amount: 1_000, + owner: SUBMITTER, + }, + reason: HoldReason::Deposit.into(), + }, + )]) + .build() + .execute_with(|| { + assert_eq!( + Pallet::::deposits(&namespace, &key), + Some(DepositEntry { + reason: HoldReason::Deposit.into(), + deposit: Deposit { + amount: 1_000, + owner: SUBMITTER + } + }) + ); + assert_eq!( + pallet_balances::Pallet::::balance_on_hold(&HoldReason::Deposit.into(), &SUBMITTER), + 1_000 + ); + + assert_ok!( + as ProviderHooks>::on_commitment_removed( + &SUBJECT, + &SUBMITTER, + &IdentityCommitmentOf::::default(), + 0 + ), + ); + + assert!(Pallet::::deposits(&namespace, &key).is_none(),); + assert!( + pallet_balances::Pallet::::balance_on_hold(&HoldReason::Deposit.into(), &SUBMITTER) + .is_zero() + ); + }); +} + +#[test] +fn on_commitment_removed_different_owner_successful() { + let namespace = DepositNamespaces::get(); + let key: DepositKeyOf = (SUBJECT, SUBMITTER, 0 as IdentityCommitmentVersion) + .encode() + .try_into() + .unwrap(); + ExtBuilder::default() + .with_deposits(vec![( + key.clone(), + DepositEntry { + deposit: Deposit { + amount: 1_000, + owner: SUBJECT, + }, + reason: HoldReason::Deposit.into(), + }, + )]) + .build() + .execute_with(|| { + assert_eq!( + Pallet::::deposits(&namespace, &key), + Some(DepositEntry { + reason: HoldReason::Deposit.into(), + deposit: Deposit { + amount: 1_000, + owner: SUBJECT + } + }) + ); + assert_eq!( + pallet_balances::Pallet::::balance_on_hold(&HoldReason::Deposit.into(), &SUBJECT), + 1_000 + ); + + assert_ok!( + as ProviderHooks>::on_commitment_removed( + &SUBJECT, + &SUBMITTER, + &IdentityCommitmentOf::::default(), + 0 + ) + ); + + assert!(Pallet::::deposits(&namespace, &key).is_none(),); + assert!( + pallet_balances::Pallet::::balance_on_hold(&HoldReason::Deposit.into(), &SUBJECT) + .is_zero() + ); + }); +} + +#[test] +fn on_commitment_removed_deposit_not_found() { + ExtBuilder::default().build().execute_with(|| { + assert_noop!( + as ProviderHooks>::on_commitment_removed( + &SUBJECT, + &SUBMITTER, + &IdentityCommitmentOf::::default(), + 0 + ), + FixedDepositCollectorViaDepositsPalletError::DepositNotFound + ); + }); +} diff --git a/pallets/pallet-deposit-storage/src/deposit/tests/on_identity_committed.rs b/pallets/pallet-deposit-storage/src/deposit/tests/on_identity_committed.rs new file mode 100644 index 000000000..1103cda7c --- /dev/null +++ b/pallets/pallet-deposit-storage/src/deposit/tests/on_identity_committed.rs @@ -0,0 +1,124 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The KILT Blockchain is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +use frame_support::{ + assert_noop, assert_ok, + traits::{fungible::InspectHold, Get}, +}; +use kilt_support::Deposit; +use pallet_dip_provider::{traits::ProviderHooks, IdentityCommitmentOf, IdentityCommitmentVersion}; +use parity_scale_codec::Encode; +use sp_runtime::traits::Zero; + +use crate::{ + deposit::{ + mock::{DepositCollectorHook, DepositNamespaces, ExtBuilder, TestRuntime, SUBJECT, SUBMITTER}, + DepositEntry, FixedDepositCollectorViaDepositsPalletError, + }, + DepositKeyOf, HoldReason, Pallet, +}; + +#[test] +fn on_identity_committed_successful() { + ExtBuilder::default() + .with_balances(vec![(SUBMITTER, 100_000)]) + .build() + .execute_with(|| { + let namespace = DepositNamespaces::get(); + let key: DepositKeyOf = (SUBJECT, SUBMITTER, 0 as IdentityCommitmentVersion) + .encode() + .try_into() + .unwrap(); + + assert!(Pallet::::deposits(&namespace, &key).is_none(),); + assert!( + pallet_balances::Pallet::::balance_on_hold(&HoldReason::Deposit.into(), &SUBMITTER) + .is_zero() + ); + + assert_ok!( + as ProviderHooks>::on_identity_committed( + &SUBJECT, + &SUBMITTER, + &IdentityCommitmentOf::::default(), + 0 + ) + ); + + assert_eq!( + Pallet::::deposits(namespace, key), + Some(DepositEntry { + reason: HoldReason::Deposit.into(), + deposit: Deposit { + amount: 1_000, + owner: SUBMITTER + } + }) + ); + assert_eq!( + pallet_balances::Pallet::::balance_on_hold(&HoldReason::Deposit.into(), &SUBMITTER), + 1_000 + ); + }); +} + +#[test] +fn on_identity_committed_existing_deposit() { + let key: DepositKeyOf = (SUBJECT, SUBMITTER, 0 as IdentityCommitmentVersion) + .encode() + .try_into() + .unwrap(); + ExtBuilder::default() + .with_deposits(vec![( + key, + DepositEntry { + reason: HoldReason::Deposit.into(), + deposit: Deposit { + amount: 1_000, + owner: SUBMITTER, + }, + }, + )]) + .build() + .execute_with(|| { + assert_noop!( + as ProviderHooks>::on_identity_committed( + &SUBJECT, + &SUBMITTER, + &IdentityCommitmentOf::::default(), + 0 + ), + FixedDepositCollectorViaDepositsPalletError::DepositAlreadyTaken + ); + }); +} + +#[test] +fn on_identity_committed_insufficient_balance() { + ExtBuilder::default().build().execute_with(|| { + assert_noop!( + as ProviderHooks>::on_identity_committed( + &SUBJECT, + &SUBMITTER, + &IdentityCommitmentOf::::default(), + 0 + ), + FixedDepositCollectorViaDepositsPalletError::FailedToHold, + ); + }); +} diff --git a/pallets/pallet-deposit-storage/src/lib.rs b/pallets/pallet-deposit-storage/src/lib.rs index fe9dbc6d8..0e05c2bcb 100644 --- a/pallets/pallet-deposit-storage/src/lib.rs +++ b/pallets/pallet-deposit-storage/src/lib.rs @@ -32,13 +32,13 @@ pub mod traits; #[cfg(test)] mod mock; +#[cfg(test)] +mod tests; + #[cfg(feature = "runtime-benchmarks")] mod benchmarking; -pub use crate::{ - default_weights::WeightInfo, deposit::FixedDepositCollectorViaDepositsPallet, pallet::*, - traits::NoopDepositStorageHooks, -}; +pub use crate::{default_weights::WeightInfo, deposit::FixedDepositCollectorViaDepositsPallet, pallet::*}; #[frame_support::pallet] pub mod pallet { @@ -113,6 +113,10 @@ pub mod pallet { /// The origin was not authorized to perform the operation on the /// specified deposit entry. Unauthorized, + /// The origin did not have enough fund to pay for the deposit. + FailedToHold, + /// Error when trying to release a previously-reserved deposit. + FailedToRelease, /// The external hook failed. Hook(u16), } @@ -184,7 +188,8 @@ pub mod pallet { entry.deposit.owner.clone(), entry.deposit.amount, &entry.reason, - )?; + ) + .map_err(|_| Error::::FailedToHold)?; Self::deposit_event(Event::::DepositAdded { namespace: namespace.clone(), key: key.clone(), @@ -214,7 +219,8 @@ pub mod pallet { Error::::Unauthorized ); } - free_deposit::, T::Currency>(&existing_entry.deposit, &existing_entry.reason)?; + free_deposit::, T::Currency>(&existing_entry.deposit, &existing_entry.reason) + .map_err(|_| Error::::FailedToRelease)?; Self::deposit_event(Event::::DepositReclaimed { namespace: namespace.clone(), key: key.clone(), diff --git a/pallets/pallet-deposit-storage/src/mock.rs b/pallets/pallet-deposit-storage/src/mock.rs index 29b68ebd4..cac23c37b 100644 --- a/pallets/pallet-deposit-storage/src/mock.rs +++ b/pallets/pallet-deposit-storage/src/mock.rs @@ -23,21 +23,21 @@ use frame_support::{ traits::{BlakeTwo256, IdentityLookup}, AccountId32, }, - traits::{ConstU16, ConstU32, ConstU64, Everything}, + traits::{ConstU16, ConstU32, ConstU64, Currency, Everything}, }; use frame_system::{mocking::MockBlock, EnsureSigned}; use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; use scale_info::TypeInfo; use sp_runtime::RuntimeDebug; -use crate::{self as storage_deposit_pallet, NoopDepositStorageHooks}; +use crate::{self as storage_deposit_pallet, DepositEntryOf, DepositKeyOf, Pallet}; pub(crate) type Balance = u128; #[derive(Encode, Decode, MaxEncodedLen, TypeInfo, Clone, PartialEq, Eq, RuntimeDebug, Default)] -pub enum DepositNamespaces { +pub enum DepositNamespace { #[default] - ExampleNameSpaces, + ExampleNamespace, } construct_runtime!( @@ -101,27 +101,60 @@ impl pallet_balances::Config for TestRuntime { impl crate::Config for TestRuntime { type CheckOrigin = EnsureSigned; type Currency = Balances; - type DepositHooks = NoopDepositStorageHooks; + type DepositHooks = (); type RuntimeEvent = RuntimeEvent; type RuntimeHoldReason = RuntimeHoldReason; type MaxKeyLength = ConstU32<256>; - type Namespace = DepositNamespaces; + type Namespace = DepositNamespace; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHooks = (); type WeightInfo = (); } +pub(crate) const OWNER: AccountId32 = AccountId32::new([100u8; 32]); +pub(crate) const OTHER_ACCOUNT: AccountId32 = AccountId32::new([101u8; 32]); + #[derive(Default)] -pub(crate) struct ExtBuilder; +pub(crate) struct ExtBuilder( + Vec<(AccountId32, Balance)>, + Vec<(DepositNamespace, DepositKeyOf, DepositEntryOf)>, +); impl ExtBuilder { - pub fn _build(self) -> sp_io::TestExternalities { - sp_io::TestExternalities::default() + pub(crate) fn with_balances(mut self, balances: Vec<(AccountId32, Balance)>) -> Self { + self.0 = balances; + self + } + + pub(crate) fn with_deposits( + mut self, + deposits: Vec<(DepositNamespace, DepositKeyOf, DepositEntryOf)>, + ) -> Self { + self.1 = deposits; + self + } + + pub(crate) fn build(self) -> sp_io::TestExternalities { + let mut ext = sp_io::TestExternalities::default(); + + ext.execute_with(|| { + for (account_id, amount) in self.0 { + Balances::make_free_balance_be(&account_id, amount); + } + + for (namespace, key, entry) in self.1 { + // Fund each account with ED + deposit amount + Balances::make_free_balance_be(&entry.deposit.owner, 500 + entry.deposit.amount); + Pallet::::add_deposit(namespace, key, entry).unwrap(); + } + }); + + ext } #[cfg(feature = "runtime-benchmarks")] - pub fn build_with_keystore(self) -> sp_io::TestExternalities { - let mut ext = self._build(); + pub(crate) fn build_with_keystore(self) -> sp_io::TestExternalities { + let mut ext = self.build(); let keystore = sp_keystore::testing::MemoryKeystore::new(); ext.register_extension(sp_keystore::KeystoreExt(sp_std::sync::Arc::new(keystore))); ext diff --git a/pallets/pallet-deposit-storage/src/tests/add_deposit.rs b/pallets/pallet-deposit-storage/src/tests/add_deposit.rs new file mode 100644 index 000000000..149e6f5a3 --- /dev/null +++ b/pallets/pallet-deposit-storage/src/tests/add_deposit.rs @@ -0,0 +1,101 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The KILT Blockchain is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +use frame_support::{assert_noop, assert_ok, traits::fungible::InspectHold}; +use kilt_support::Deposit; +use sp_runtime::traits::Zero; + +use crate::{ + mock::{Balances, DepositNamespace, ExtBuilder, TestRuntime, OWNER}, + DepositEntryOf, DepositKeyOf, Error, HoldReason, Pallet, +}; + +#[test] +fn add_deposit_new() { + ExtBuilder::default() + // Deposit amount + existential deposit + .with_balances(vec![(OWNER, 500 + 10_000)]) + .build() + .execute_with(|| { + let deposit = DepositEntryOf:: { + reason: HoldReason::Deposit.into(), + deposit: Deposit { + amount: 10_000, + owner: OWNER, + }, + }; + let namespace = DepositNamespace::ExampleNamespace; + let key = DepositKeyOf::::default(); + + assert!(Pallet::::deposits(&namespace, &key).is_none()); + assert!(Balances::balance_on_hold(&HoldReason::Deposit.into(), &OWNER).is_zero()); + + assert_ok!(Pallet::::add_deposit( + namespace.clone(), + key.clone(), + deposit.clone() + )); + + assert_eq!(Pallet::::deposits(&namespace, &key), Some(deposit)); + assert_eq!(Balances::balance_on_hold(&HoldReason::Deposit.into(), &OWNER), 10_000); + }); +} + +#[test] +fn add_deposit_existing() { + let deposit = DepositEntryOf:: { + reason: HoldReason::Deposit.into(), + deposit: Deposit { + amount: 10_000, + owner: OWNER, + }, + }; + let namespace = DepositNamespace::ExampleNamespace; + let key = DepositKeyOf::::default(); + ExtBuilder::default() + .with_deposits(vec![(namespace.clone(), key.clone(), deposit.clone())]) + .build() + .execute_with(|| { + assert_noop!( + Pallet::::add_deposit(namespace.clone(), key.clone(), deposit), + Error::::DepositExisting + ); + }); +} + +#[test] +fn add_deposit_failed_to_hold() { + ExtBuilder::default().build().execute_with(|| { + let deposit = DepositEntryOf:: { + reason: HoldReason::Deposit.into(), + deposit: Deposit { + amount: 10_000, + owner: OWNER, + }, + }; + + assert_noop!( + Pallet::::add_deposit( + DepositNamespace::ExampleNamespace, + DepositKeyOf::::default(), + deposit + ), + Error::::FailedToHold + ); + }); +} diff --git a/pallets/pallet-deposit-storage/src/tests/mod.rs b/pallets/pallet-deposit-storage/src/tests/mod.rs new file mode 100644 index 000000000..34e831e42 --- /dev/null +++ b/pallets/pallet-deposit-storage/src/tests/mod.rs @@ -0,0 +1,20 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The KILT Blockchain is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +mod add_deposit; +mod reclaim_deposit; diff --git a/pallets/pallet-deposit-storage/src/tests/reclaim_deposit.rs b/pallets/pallet-deposit-storage/src/tests/reclaim_deposit.rs new file mode 100644 index 000000000..6ad9afece --- /dev/null +++ b/pallets/pallet-deposit-storage/src/tests/reclaim_deposit.rs @@ -0,0 +1,96 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The KILT Blockchain is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +use frame_support::{assert_noop, assert_ok, traits::fungible::InspectHold}; +use frame_system::RawOrigin; +use kilt_support::Deposit; +use sp_runtime::traits::Zero; + +use crate::{ + mock::{Balances, DepositNamespace, ExtBuilder, TestRuntime, OTHER_ACCOUNT, OWNER}, + DepositEntryOf, DepositKeyOf, Error, HoldReason, Pallet, +}; + +#[test] +fn reclaim_deposit_successful() { + let deposit = DepositEntryOf:: { + reason: HoldReason::Deposit.into(), + deposit: Deposit { + amount: 10_000, + owner: OWNER, + }, + }; + let namespace = DepositNamespace::ExampleNamespace; + let key = DepositKeyOf::::default(); + ExtBuilder::default() + .with_deposits(vec![(namespace.clone(), key.clone(), deposit)]) + .build() + .execute_with(|| { + assert!(Pallet::::deposits(&namespace, &key).is_some()); + assert_eq!(Balances::balance_on_hold(&HoldReason::Deposit.into(), &OWNER), 10_000); + + assert_ok!(Pallet::::reclaim_deposit( + RawOrigin::Signed(OWNER).into(), + namespace.clone(), + key.clone() + )); + + assert!(Pallet::::deposits(&namespace, &key).is_none()); + assert!(Balances::balance_on_hold(&HoldReason::Deposit.into(), &OWNER).is_zero()); + }); +} + +#[test] +fn reclaim_deposit_not_found() { + ExtBuilder::default().build().execute_with(|| { + assert_noop!( + Pallet::::reclaim_deposit( + RawOrigin::Signed(OWNER).into(), + DepositNamespace::ExampleNamespace, + DepositKeyOf::::default() + ), + Error::::DepositNotFound + ); + }); +} + +#[test] +fn reclaim_deposit_unauthorized() { + let deposit = DepositEntryOf:: { + reason: HoldReason::Deposit.into(), + deposit: Deposit { + amount: 10_000, + owner: OWNER, + }, + }; + let namespace = DepositNamespace::ExampleNamespace; + let key = DepositKeyOf::::default(); + ExtBuilder::default() + .with_deposits(vec![(namespace.clone(), key.clone(), deposit)]) + .build() + .execute_with(|| { + assert_noop!( + Pallet::::reclaim_deposit( + RawOrigin::Signed(OTHER_ACCOUNT).into(), + namespace.clone(), + key.clone() + ), + Error::::Unauthorized + ); + }); +} diff --git a/pallets/pallet-deposit-storage/src/traits.rs b/pallets/pallet-deposit-storage/src/traits.rs index aa7c89093..c63c482da 100644 --- a/pallets/pallet-deposit-storage/src/traits.rs +++ b/pallets/pallet-deposit-storage/src/traits.rs @@ -35,10 +35,7 @@ where ) -> Result<(), Self::Error>; } -/// Dummy implementation of the [`DepositStorageHooks`] trait that does a noop. -pub struct NoopDepositStorageHooks; - -impl DepositStorageHooks for NoopDepositStorageHooks +impl DepositStorageHooks for () where Runtime: Config, { diff --git a/pallets/pallet-dip-provider/src/lib.rs b/pallets/pallet-dip-provider/src/lib.rs index 856d272d5..75ad0e4c5 100644 --- a/pallets/pallet-dip-provider/src/lib.rs +++ b/pallets/pallet-dip-provider/src/lib.rs @@ -27,11 +27,13 @@ mod benchmarking; #[cfg(test)] mod mock; +#[cfg(test)] +mod tests; pub use crate::{ default_weights::WeightInfo, pallet::*, - traits::{DefaultIdentityCommitmentGenerator, DefaultIdentityProvider, NoopHooks}, + traits::{DefaultIdentityCommitmentGenerator, DefaultIdentityProvider}, }; #[frame_support::pallet] diff --git a/pallets/pallet-dip-provider/src/mock.rs b/pallets/pallet-dip-provider/src/mock.rs index 215eb979f..2588d8da2 100644 --- a/pallets/pallet-dip-provider/src/mock.rs +++ b/pallets/pallet-dip-provider/src/mock.rs @@ -29,7 +29,10 @@ use frame_support::{ use frame_system::mocking::MockBlock; use kilt_support::mock::mock_origin::{self as mock_origin, DoubleOrigin, EnsureDoubleOrigin}; -use crate::{DefaultIdentityCommitmentGenerator, DefaultIdentityProvider, NoopHooks}; +use crate::{ + traits::{IdentityCommitmentGenerator, IdentityProvider}, + DefaultIdentityCommitmentGenerator, DefaultIdentityProvider, IdentityCommitmentOf, IdentityCommitmentVersion, +}; construct_runtime!( pub struct TestRuntime { @@ -71,7 +74,7 @@ impl crate::Config for TestRuntime { type Identifier = AccountId32; type IdentityCommitmentGenerator = DefaultIdentityCommitmentGenerator; type IdentityProvider = DefaultIdentityProvider; - type ProviderHooks = NoopHooks; + type ProviderHooks = (); type RuntimeEvent = RuntimeEvent; type WeightInfo = (); } @@ -82,17 +85,61 @@ impl mock_origin::Config for TestRuntime { type SubjectId = ::Identifier; } +pub(crate) const ACCOUNT_ID: AccountId32 = AccountId32::new([100u8; 32]); +pub(crate) const DID: AccountId32 = AccountId32::new([200u8; 32]); + +pub(crate) fn get_expected_commitment_for( + subject: &::Identifier, + version: IdentityCommitmentVersion, +) -> IdentityCommitmentOf { + let expected_identity_details = + <::IdentityProvider as IdentityProvider>::retrieve(subject) + .expect("Should not fail to generate identity details for the provided DID."); + <::IdentityCommitmentGenerator as IdentityCommitmentGenerator>::generate_commitment( + subject, + &expected_identity_details, + version, + ) + .expect("Should not fail to generate identity commitment for the provided DID.") +} + #[derive(Default)] -pub(crate) struct ExtBuilder; +pub(crate) struct ExtBuilder( + Vec<( + AccountId32, + IdentityCommitmentVersion, + IdentityCommitmentOf, + )>, +); impl ExtBuilder { - pub fn _build(self) -> sp_io::TestExternalities { - sp_io::TestExternalities::default() + pub(crate) fn with_commitments( + mut self, + commitments: Vec<( + AccountId32, + IdentityCommitmentVersion, + IdentityCommitmentOf, + )>, + ) -> Self { + self.0 = commitments; + self + } + + pub(crate) fn build(self) -> sp_io::TestExternalities { + let mut ext = sp_io::TestExternalities::default(); + + ext.execute_with(|| { + for (subject, commitment_version, commitment) in self.0 { + crate::pallet::IdentityCommitments::::insert(subject, commitment_version, commitment); + } + }); + + ext } #[cfg(feature = "runtime-benchmarks")] - pub fn build_with_keystore(self) -> sp_io::TestExternalities { - let mut ext = self._build(); + pub(crate) fn build_with_keystore(self) -> sp_io::TestExternalities { + let mut ext = self.build(); let keystore = sp_keystore::testing::MemoryKeystore::new(); ext.register_extension(sp_keystore::KeystoreExt(sp_std::sync::Arc::new(keystore))); ext diff --git a/pallets/pallet-dip-provider/src/tests/commit_identity.rs b/pallets/pallet-dip-provider/src/tests/commit_identity.rs new file mode 100644 index 000000000..501ee135a --- /dev/null +++ b/pallets/pallet-dip-provider/src/tests/commit_identity.rs @@ -0,0 +1,79 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The KILT Blockchain is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +use frame_support::assert_ok; +use kilt_support::mock::mock_origin::DoubleOrigin; + +use crate::mock::*; + +#[test] +fn commit_identity_multiple_commitments_for_same_subject() { + ExtBuilder::default().build().execute_with(|| { + assert_ok!(DipProvider::commit_identity( + DoubleOrigin(ACCOUNT_ID, DID).into(), + DID, + Some(0), + )); + let expected_identity_commitment = get_expected_commitment_for(&DID, 0); + assert_eq!( + DipProvider::identity_commitments(&DID, 0), + Some(expected_identity_commitment) + ); + + // A second commitment for a different version must be possbile. + assert_ok!(DipProvider::commit_identity( + DoubleOrigin(ACCOUNT_ID, DID).into(), + DID, + Some(1), + )); + let expected_identity_commitment = get_expected_commitment_for(&DID, 1); + assert_eq!( + crate::pallet::IdentityCommitments::::iter_key_prefix(&DID).count(), + 2 + ); + // Right now the commitment is the same as before, but it could be different in + // the future. This test should catch that. + assert_eq!( + DipProvider::identity_commitments(&DID, 1), + Some(expected_identity_commitment) + ); + }); +} + +#[test] +fn commit_identity_override_same_version_commitment() { + ExtBuilder::default() + .with_commitments(vec![(DID, 0, u32::MAX)]) + .build() + .execute_with(|| { + let expected_identity_commitment = get_expected_commitment_for(&DID, 0); + assert_ne!( + DipProvider::identity_commitments(&DID, 0), + Some(expected_identity_commitment) + ); + assert_ok!(DipProvider::commit_identity( + DoubleOrigin(ACCOUNT_ID, DID).into(), + DID, + Some(0), + )); + assert_eq!( + DipProvider::identity_commitments(&DID, 0), + Some(expected_identity_commitment) + ); + }); +} diff --git a/pallets/pallet-dip-provider/src/tests/delete_identity_commitment.rs b/pallets/pallet-dip-provider/src/tests/delete_identity_commitment.rs new file mode 100644 index 000000000..606045965 --- /dev/null +++ b/pallets/pallet-dip-provider/src/tests/delete_identity_commitment.rs @@ -0,0 +1,59 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The KILT Blockchain is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +use frame_support::{assert_noop, assert_ok}; +use kilt_support::mock::mock_origin::DoubleOrigin; + +use crate::mock::*; + +#[test] +fn delete_identity_commitment_multiple_versions() { + ExtBuilder::default() + .with_commitments(vec![(DID, 0, u32::MAX), (DID, 1, u32::MAX - 1)]) + .build() + .execute_with(|| { + assert_ok!(DipProvider::delete_identity_commitment( + DoubleOrigin(ACCOUNT_ID, DID).into(), + DID, + Some(0), + )); + assert_eq!( + crate::pallet::IdentityCommitments::::iter_key_prefix(&DID).count(), + 1 + ); + assert_ok!(DipProvider::delete_identity_commitment( + DoubleOrigin(ACCOUNT_ID, DID).into(), + DID, + Some(1), + )); + assert_eq!( + crate::pallet::IdentityCommitments::::iter_key_prefix(&DID).count(), + 0 + ); + }); +} + +#[test] +fn delete_identity_commitment_not_found() { + ExtBuilder::default().build().execute_with(|| { + assert_noop!( + DipProvider::delete_identity_commitment(DoubleOrigin(ACCOUNT_ID, DID).into(), DID, Some(0),), + crate::Error::::CommitmentNotFound + ); + }); +} diff --git a/pallets/pallet-dip-provider/src/tests/mod.rs b/pallets/pallet-dip-provider/src/tests/mod.rs new file mode 100644 index 000000000..5bce39f18 --- /dev/null +++ b/pallets/pallet-dip-provider/src/tests/mod.rs @@ -0,0 +1,20 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The KILT Blockchain is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +mod commit_identity; +mod delete_identity_commitment; diff --git a/pallets/pallet-dip-provider/src/traits.rs b/pallets/pallet-dip-provider/src/traits.rs index 62395fa94..5f5369b73 100644 --- a/pallets/pallet-dip-provider/src/traits.rs +++ b/pallets/pallet-dip-provider/src/traits.rs @@ -172,10 +172,7 @@ where ) -> Result<(), Self::Error>; } -/// Implement the [`ProviderHooks`] trait with noops. -pub struct NoopHooks; - -impl ProviderHooks for NoopHooks +impl ProviderHooks for () where Runtime: Config, { diff --git a/runtimes/common/Cargo.toml b/runtimes/common/Cargo.toml index a330c99ad..573a340a6 100644 --- a/runtimes/common/Cargo.toml +++ b/runtimes/common/Cargo.toml @@ -11,6 +11,8 @@ repository.workspace = true version.workspace = true [dev-dependencies] +did = {workspace = true, features = ["std", "mock"]} +kilt-dip-primitives = { workspace = true, features = ["std"] } sp-io = {workspace = true, features = ["std"]} [dependencies] @@ -23,8 +25,9 @@ smallvec.workspace = true attestation.workspace = true did.workspace = true -kilt-support.workspace = true kilt-dip-primitives.workspace = true +kilt-support.workspace = true +pallet-deposit-storage.workspace = true pallet-did-lookup.workspace = true pallet-dip-provider.workspace = true pallet-web3-names.workspace = true @@ -74,6 +77,7 @@ runtime-benchmarks = [ "frame-system/runtime-benchmarks", "kilt-dip-primitives/runtime-benchmarks", "kilt-support/runtime-benchmarks", + "pallet-deposit-storage/runtime-benchmarks", "pallet-balances/runtime-benchmarks", "pallet-membership/runtime-benchmarks", "pallet-multisig/runtime-benchmarks", @@ -97,8 +101,9 @@ std = [ "frame-support/std", "frame-system/std", "kilt-asset-dids/std", - "kilt-support/std", "kilt-dip-primitives/std", + "kilt-support/std", + "pallet-deposit-storage/std", "pallet-did-lookup/std", "pallet-dip-provider/std", "pallet-web3-names/std", diff --git a/runtimes/common/src/dip/deposit/mock.rs b/runtimes/common/src/dip/deposit/mock.rs new file mode 100644 index 000000000..1cf3d8939 --- /dev/null +++ b/runtimes/common/src/dip/deposit/mock.rs @@ -0,0 +1,132 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The KILT Blockchain is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +use frame_support::{construct_runtime, traits::Everything}; +use frame_system::{mocking::MockBlock, EnsureSigned, RawOrigin}; +use pallet_dip_provider::{DefaultIdentityCommitmentGenerator, DefaultIdentityProvider, IdentityCommitmentVersion}; +use sp_core::{ConstU128, ConstU32}; +use sp_runtime::traits::IdentityLookup; + +use crate::{ + constants::{deposit_storage::MAX_DEPOSIT_PALLET_KEY_LENGTH, KILT}, + dip::deposit::{DepositHooks, DepositNamespace}, + AccountId, Balance, BlockHashCount, BlockLength, BlockWeights, Hash, Hasher, Nonce, +}; + +construct_runtime!( + pub struct TestRuntime { + System: frame_system, + Balances: pallet_balances, + DipProvider: pallet_dip_provider, + StorageDepositPallet: pallet_deposit_storage, + } +); + +pub(crate) const SUBJECT: AccountId = AccountId::new([100u8; 32]); +pub(crate) const SUBMITTER: AccountId = AccountId::new([200u8; 32]); + +impl frame_system::Config for TestRuntime { + type AccountData = pallet_balances::AccountData; + type AccountId = AccountId; + type BaseCallFilter = Everything; + type Block = MockBlock; + type BlockHashCount = BlockHashCount; + type BlockLength = BlockLength; + type BlockWeights = BlockWeights; + type DbWeight = (); + type Hash = Hash; + type Hashing = Hasher; + type Lookup = IdentityLookup; + type MaxConsumers = ConstU32<16>; + type Nonce = Nonce; + type OnKilledAccount = (); + type OnNewAccount = (); + type OnSetCode = (); + type PalletInfo = PalletInfo; + type RuntimeCall = RuntimeCall; + type RuntimeEvent = RuntimeEvent; + type RuntimeOrigin = RuntimeOrigin; + type SS58Prefix = (); + type SystemWeightInfo = (); + type Version = (); +} + +impl pallet_balances::Config for TestRuntime { + type FreezeIdentifier = RuntimeFreezeReason; + type RuntimeHoldReason = RuntimeHoldReason; + type MaxFreezes = ConstU32<10>; + type MaxHolds = ConstU32<10>; + type Balance = Balance; + type DustRemoval = (); + type RuntimeEvent = RuntimeEvent; + type ExistentialDeposit = ConstU128; + type AccountStore = System; + type WeightInfo = (); + type MaxLocks = ConstU32<10>; + type MaxReserves = ConstU32<10>; + type ReserveIdentifier = [u8; 8]; +} + +impl pallet_dip_provider::Config for TestRuntime { + type CommitOrigin = AccountId; + type CommitOriginCheck = EnsureSigned; + type Identifier = AccountId; + type IdentityCommitmentGenerator = DefaultIdentityCommitmentGenerator; + type IdentityProvider = DefaultIdentityProvider; + type ProviderHooks = (); + type RuntimeEvent = RuntimeEvent; + type WeightInfo = (); +} + +impl pallet_deposit_storage::Config for TestRuntime { + type CheckOrigin = EnsureSigned; + type Currency = Balances; + type DepositHooks = DepositHooks; + type RuntimeEvent = RuntimeEvent; + type RuntimeHoldReason = RuntimeHoldReason; + type MaxKeyLength = ConstU32; + type Namespace = DepositNamespace; + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHooks = (); + type WeightInfo = (); +} + +#[derive(Default)] +pub(crate) struct ExtBuilder(Vec<(AccountId, IdentityCommitmentVersion, AccountId)>); + +impl ExtBuilder { + pub(crate) fn with_commitments( + mut self, + commitments: Vec<(AccountId, IdentityCommitmentVersion, AccountId)>, + ) -> Self { + self.0 = commitments; + self + } + + pub(crate) fn build(self) -> sp_io::TestExternalities { + let mut ext = sp_io::TestExternalities::default(); + + ext.execute_with(|| { + for (subject, version, submitter) in self.0 { + DipProvider::commit_identity(RawOrigin::Signed(submitter).into(), subject, Some(version)).unwrap(); + } + }); + + ext + } +} diff --git a/runtimes/peregrine/src/dip/deposit.rs b/runtimes/common/src/dip/deposit/mod.rs similarity index 85% rename from runtimes/peregrine/src/dip/deposit.rs rename to runtimes/common/src/dip/deposit/mod.rs index 4cec3f934..2e4714f4f 100644 --- a/runtimes/peregrine/src/dip/deposit.rs +++ b/runtimes/common/src/dip/deposit/mod.rs @@ -16,22 +16,34 @@ // If you feel like getting in touch with us, you can do so at info@botlabs.org -use crate::{DidIdentifier, Runtime}; use frame_support::traits::Get; use pallet_deposit_storage::{ traits::DepositStorageHooks, DepositEntryOf, DepositKeyOf, FixedDepositCollectorViaDepositsPallet, }; use pallet_dip_provider::IdentityCommitmentVersion; use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; -use runtime_common::{constants::dip_provider::COMMITMENT_DEPOSIT, AccountId}; use scale_info::TypeInfo; use sp_core::{ConstU128, RuntimeDebug}; +use crate::{constants::dip_provider::COMMITMENT_DEPOSIT, AccountId, DidIdentifier}; + +#[cfg(test)] +mod mock; +#[cfg(test)] +mod tests; + #[derive(Encode, Decode, MaxEncodedLen, TypeInfo, Clone, PartialEq, Eq, RuntimeDebug)] pub enum DepositNamespace { DipProvider, } +#[cfg(feature = "runtime-benchmarks")] +impl Default for DepositNamespace { + fn default() -> Self { + Self::DipProvider + } +} + /// The namespace to use in the [`pallet_deposit_storage::Pallet`] to store /// all deposits related to DIP commitments. pub struct DipProviderDepositNamespace; @@ -66,6 +78,7 @@ impl From<(DidIdentifier, AccountId, IdentityCommitmentVersion)> for DepositKey pub type DepositCollectorHooks = FixedDepositCollectorViaDepositsPallet, DepositKey>; +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] pub enum CommitmentDepositRemovalHookError { DecodeKey, Internal, @@ -91,7 +104,10 @@ impl From for u16 { /// or release deposits from the [`pallet_deposit_storage::Pallet`] pallet. pub struct DepositHooks; -impl DepositStorageHooks for DepositHooks { +impl DepositStorageHooks for DepositHooks +where + Runtime: pallet_deposit_storage::Config + pallet_dip_provider::Config, +{ type Error = CommitmentDepositRemovalHookError; fn on_deposit_reclaimed( @@ -123,13 +139,18 @@ impl DepositStorageHooks for DepositHooks { pub struct PalletDepositStorageBenchmarkHooks; #[cfg(feature = "runtime-benchmarks")] -impl pallet_deposit_storage::traits::BenchmarkHooks for PalletDepositStorageBenchmarkHooks { +impl pallet_deposit_storage::traits::BenchmarkHooks for PalletDepositStorageBenchmarkHooks +where + Runtime: pallet_deposit_storage::Config + + pallet_dip_provider::Config, + pallet_dip_provider::IdentityCommitmentOf: From, +{ fn pre_reclaim_deposit() -> ( ::AccountId, ::Namespace, sp_runtime::BoundedVec::MaxKeyLength>, ) { - let submitter = runtime_common::AccountId::from([100u8; 32]); + let submitter = AccountId::from([100u8; 32]); let namespace = DepositNamespace::DipProvider; let did_identifier = DidIdentifier::from([200u8; 32]); let commitment_version = 0u16; @@ -142,7 +163,7 @@ impl pallet_deposit_storage::traits::BenchmarkHooks for PalletDepositSt pallet_dip_provider::IdentityCommitments::::insert( &did_identifier, commitment_version, - ::Hash::default(), + pallet_dip_provider::IdentityCommitmentOf::::from(crate::Hash::default()), ); assert!(pallet_dip_provider::IdentityCommitments::::get(did_identifier, commitment_version).is_some()); diff --git a/runtimes/common/src/dip/deposit/tests/mod.rs b/runtimes/common/src/dip/deposit/tests/mod.rs new file mode 100644 index 000000000..c63b1615e --- /dev/null +++ b/runtimes/common/src/dip/deposit/tests/mod.rs @@ -0,0 +1,19 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The KILT Blockchain is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +mod on_deposit_reclaimed; diff --git a/runtimes/common/src/dip/deposit/tests/on_deposit_reclaimed.rs b/runtimes/common/src/dip/deposit/tests/on_deposit_reclaimed.rs new file mode 100644 index 000000000..79f97c1e7 --- /dev/null +++ b/runtimes/common/src/dip/deposit/tests/on_deposit_reclaimed.rs @@ -0,0 +1,75 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The KILT Blockchain is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +use frame_support::{assert_noop, assert_ok}; +use kilt_support::Deposit; +use pallet_deposit_storage::{traits::DepositStorageHooks, DepositEntryOf, DepositKeyOf, HoldReason}; +use parity_scale_codec::Encode; + +use crate::dip::deposit::{ + mock::{DipProvider, ExtBuilder, TestRuntime, SUBJECT, SUBMITTER}, + CommitmentDepositRemovalHookError, DepositKey, DepositNamespace, +}; + +#[test] +fn on_deposit_reclaimed_successful() { + ExtBuilder::default() + .with_commitments(vec![(SUBJECT, 0, SUBMITTER)]) + .build() + .execute_with(|| { + let deposit_key: DepositKeyOf = DepositKey::DipProvider { + identifier: SUBJECT, + version: 0, + } + .encode() + .try_into() + .unwrap(); + assert_ok!(<::DepositHooks as DepositStorageHooks>::on_deposit_reclaimed( + &DepositNamespace::DipProvider, + &deposit_key, + DepositEntryOf:: { + reason: HoldReason::Deposit.into(), + deposit: Deposit { + amount: 10_000, + owner: SUBMITTER + } + } + )); + + assert!(DipProvider::identity_commitments(SUBJECT, 0).is_none()); + }); +} + +#[test] +fn on_deposit_reclaimed_key_decoding_error() { + ExtBuilder::default() + .build() + .execute_with(|| { + assert_noop!(<::DepositHooks as DepositStorageHooks>::on_deposit_reclaimed( + &DepositNamespace::DipProvider, + &DepositKeyOf::::default(), + DepositEntryOf:: { + reason: HoldReason::Deposit.into(), + deposit: Deposit { + amount: 10_000, + owner: SUBMITTER + } + } + ), CommitmentDepositRemovalHookError::DecodeKey); + }); +} diff --git a/runtimes/common/src/dip/did.rs b/runtimes/common/src/dip/did/mod.rs similarity index 98% rename from runtimes/common/src/dip/did.rs rename to runtimes/common/src/dip/did/mod.rs index 212166cdc..660fc945d 100644 --- a/runtimes/common/src/dip/did.rs +++ b/runtimes/common/src/dip/did/mod.rs @@ -26,12 +26,15 @@ use parity_scale_codec::{Decode, Encode}; use scale_info::TypeInfo; use sp_core::ConstU32; use sp_runtime::{BoundedVec, SaturatedConversion}; -use sp_std::vec::Vec; +use sp_std::{fmt::Debug, vec::Vec}; #[cfg(feature = "runtime-benchmarks")] use kilt_support::{benchmark::IdentityContext, traits::GetWorstCase}; -#[derive(Encode, Decode, TypeInfo, Debug)] +#[cfg(test)] +mod tests; + +#[derive(Encode, Decode, TypeInfo, Debug, PartialEq, Eq)] pub enum LinkedDidInfoProviderError { DidNotFound, DidDeleted, @@ -59,6 +62,7 @@ pub type Web3OwnershipOf = /// Identity information related to a KILT DID relevant for cross-chain /// transactions via the DIP protocol. +#[derive(Debug, Clone, PartialEq)] pub struct LinkedDidInfoOf where Runtime: did::Config + pallet_web3_names::Config, diff --git a/runtimes/common/src/dip/did/tests/mod.rs b/runtimes/common/src/dip/did/tests/mod.rs new file mode 100644 index 000000000..0b9d1fee4 --- /dev/null +++ b/runtimes/common/src/dip/did/tests/mod.rs @@ -0,0 +1,19 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The KILT Blockchain is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +mod retrieve; diff --git a/runtimes/common/src/dip/did/tests/retrieve.rs b/runtimes/common/src/dip/did/tests/retrieve.rs new file mode 100644 index 000000000..fc6b542d6 --- /dev/null +++ b/runtimes/common/src/dip/did/tests/retrieve.rs @@ -0,0 +1,137 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The KILT Blockchain is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +use did::did_details::DidVerificationKey; +use frame_support::assert_noop; +use pallet_dip_provider::traits::IdentityProvider; + +use crate::{ + constants::dip_provider::MAX_LINKED_ACCOUNTS, + dip::{ + did::{LinkedDidInfoOf, LinkedDidInfoProvider, LinkedDidInfoProviderError, Web3OwnershipOf}, + mock::{create_linked_info, ExtBuilder, TestRuntime, ACCOUNT, DID_IDENTIFIER, SUBMITTER}, + }, +}; + +#[test] +fn linked_did_info_provider_retrieve_max_capacity() { + let auth_key = DidVerificationKey::Account(ACCOUNT); + let LinkedDidInfoOf { + did_details, + web3_name_details, + linked_accounts, + } = create_linked_info(auth_key, Some(b"ntn_x2"), MAX_LINKED_ACCOUNTS); + let web3_name: Option> = + web3_name_details.map(|n| n.web3_name); + + ExtBuilder::default() + .with_dids(vec![( + DID_IDENTIFIER, + did_details.clone(), + web3_name.clone(), + linked_accounts.clone().into_inner(), + SUBMITTER, + )]) + .build() + .execute_with(|| { + let identity: LinkedDidInfoOf = + LinkedDidInfoProvider::retrieve(&DID_IDENTIFIER).expect("Should not fail to fetch identity details."); + assert_eq!(identity.did_details, did_details); + assert_eq!( + identity.web3_name_details, + Some(Web3OwnershipOf:: { + web3_name: web3_name.unwrap(), + claimed_at: 0 + }) + ); + assert!(identity.linked_accounts.iter().all(|i| linked_accounts.contains(i))); + assert!(linked_accounts.iter().all(|i| identity.linked_accounts.contains(i))); + assert_eq!(identity.linked_accounts.len(), MAX_LINKED_ACCOUNTS as usize); + }); +} + +#[test] +fn linked_did_info_provider_retrieve_only_did_details() { + let auth_key = DidVerificationKey::Account(ACCOUNT); + let LinkedDidInfoOf { did_details, .. } = create_linked_info(auth_key, Option::>::None, 0); + + ExtBuilder::default() + .with_dids(vec![(DID_IDENTIFIER, did_details.clone(), None, vec![], SUBMITTER)]) + .build() + .execute_with(|| { + let identity: LinkedDidInfoOf = + LinkedDidInfoProvider::retrieve(&DID_IDENTIFIER).expect("Should not fail to fetch identity details."); + assert_eq!(identity.did_details, did_details); + assert_eq!(identity.linked_accounts, vec![]); + assert!(identity.web3_name_details.is_none()) + }); +} + +#[test] +fn linked_did_info_provider_retrieve_delete_did() { + ExtBuilder::default() + .with_deleted_dids(vec![DID_IDENTIFIER]) + .build() + .execute_with(|| { + assert_noop!( + LinkedDidInfoProvider::retrieve(&DID_IDENTIFIER) + as Result, _>, + LinkedDidInfoProviderError::DidDeleted + ); + }); +} + +#[test] +fn linked_did_info_provider_retrieve_did_not_found() { + ExtBuilder::default().build().execute_with(|| { + assert_noop!( + LinkedDidInfoProvider::retrieve(&DID_IDENTIFIER) + as Result, _>, + LinkedDidInfoProviderError::DidNotFound + ); + }); +} + +#[test] +#[should_panic = "Cannot cast generated vector of linked accounts with length 11 to BoundedVec with max limit of 10."] +fn linked_did_info_provider_retrieve_too_many_linked_accounts() { + let auth_key = DidVerificationKey::Account(ACCOUNT); + let LinkedDidInfoOf { + did_details, + web3_name_details, + linked_accounts, + } = create_linked_info(auth_key, Some(b"ntn_x2"), MAX_LINKED_ACCOUNTS + 1); + let web3_name = web3_name_details.map(|n| n.web3_name); + + ExtBuilder::default() + .with_dids(vec![( + DID_IDENTIFIER, + did_details, + web3_name, + linked_accounts.into_inner(), + SUBMITTER, + )]) + .build() + .execute_with(|| { + assert_noop!( + LinkedDidInfoProvider::retrieve(&DID_IDENTIFIER) + as Result, _>, + LinkedDidInfoProviderError::TooManyLinkedAccounts + ); + }); +} diff --git a/runtimes/common/src/dip/merkle.rs b/runtimes/common/src/dip/merkle.rs deleted file mode 100644 index 73f92e83d..000000000 --- a/runtimes/common/src/dip/merkle.rs +++ /dev/null @@ -1,415 +0,0 @@ -// KILT Blockchain – https://botlabs.org -// Copyright (C) 2019-2024 BOTLabs GmbH - -// The KILT Blockchain is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// The KILT Blockchain is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -// If you feel like getting in touch with us, you can do so at info@botlabs.org -use did::{DidVerificationKeyRelationship, KeyIdOf}; -use frame_support::RuntimeDebug; -use frame_system::pallet_prelude::BlockNumberFor; -use kilt_dip_primitives::DidMerkleProof; -use pallet_did_lookup::linkable_account::LinkableAccountId; -use pallet_dip_provider::{ - traits::{IdentityCommitmentGenerator, IdentityProvider}, - IdentityCommitmentVersion, IdentityOf, -}; -use parity_scale_codec::{Decode, Encode}; -use scale_info::TypeInfo; -use sp_std::{borrow::ToOwned, marker::PhantomData, vec::Vec}; -use sp_trie::{generate_trie_proof, LayoutV1, MemoryDB, TrieDBMutBuilder, TrieHash, TrieMut}; - -use crate::dip::did::LinkedDidInfoOf; - -/// Type of the Merkle proof revealing parts of the DIP identity of a given DID -/// subject. -pub type DidMerkleProofOf = DidMerkleProof< - KeyIdOf, - ::AccountId, - BlockNumberFor, - ::Web3Name, - LinkableAccountId, ->; - -/// Type of a complete DIP Merkle proof. -#[derive(Encode, Decode, RuntimeDebug, PartialEq, Eq, TypeInfo)] -pub struct CompleteMerkleProof { - /// The Merkle root. - pub root: Root, - /// The Merkle proof revealing parts of the commitment that verify against - /// the provided root. - pub proof: Proof, -} - -#[derive(Clone, RuntimeDebug, Encode, Decode, TypeInfo, PartialEq)] -pub enum DidMerkleProofError { - UnsupportedVersion, - KeyNotFound, - LinkedAccountNotFound, - Web3NameNotFound, - TooManyLeaves, - Internal, -} - -impl From for u16 { - fn from(value: DidMerkleProofError) -> Self { - match value { - // DO NOT USE 0 - // Errors of different sub-parts are separated by a `u8::MAX`. - // A value of 0 would make it confusing whether it's the previous sub-part error (u8::MAX) - // or the new sub-part error (u8::MAX + 0). - DidMerkleProofError::UnsupportedVersion => 1, - DidMerkleProofError::KeyNotFound => 2, - DidMerkleProofError::LinkedAccountNotFound => 3, - DidMerkleProofError::Web3NameNotFound => 4, - DidMerkleProofError::TooManyLeaves => 5, - DidMerkleProofError::Internal => u16::MAX, - } - } -} - -pub mod v0 { - use did::did_details::DidDetails; - use kilt_dip_primitives::{ - DidKeyRelationship, RevealedAccountId, RevealedDidKey, RevealedDidMerkleProofLeaf, RevealedWeb3Name, - }; - use pallet_web3_names::Web3NameOf; - use sp_std::vec; - - use crate::dip::did::Web3OwnershipOf; - - use super::*; - - fn get_auth_leaves( - did_details: &DidDetails, - ) -> Result< - impl Iterator, BlockNumberFor, Runtime::AccountId>>, - DidMerkleProofError, - > - where - Runtime: did::Config, - { - let auth_key_details = did_details - .public_keys - .get(&did_details.authentication_key) - .ok_or_else(|| { - log::error!("Authentication key should be part of the public keys."); - DidMerkleProofError::Internal - })?; - Ok([RevealedDidKey { - id: did_details.authentication_key, - relationship: DidVerificationKeyRelationship::Authentication.into(), - details: auth_key_details.clone(), - }] - .into_iter()) - } - - fn get_att_leaves( - did_details: &DidDetails, - ) -> Result< - impl Iterator, BlockNumberFor, Runtime::AccountId>>, - DidMerkleProofError, - > - where - Runtime: did::Config, - { - let Some(att_key_id) = did_details.attestation_key else { - return Ok(vec![].into_iter()); - }; - let att_key_details = did_details.public_keys.get(&att_key_id).ok_or_else(|| { - log::error!("Attestation key should be part of the public keys."); - DidMerkleProofError::Internal - })?; - Ok(vec![RevealedDidKey { - id: att_key_id, - relationship: DidVerificationKeyRelationship::AssertionMethod.into(), - details: att_key_details.clone(), - }] - .into_iter()) - } - - fn get_del_leaves( - did_details: &DidDetails, - ) -> Result< - impl Iterator, BlockNumberFor, Runtime::AccountId>>, - DidMerkleProofError, - > - where - Runtime: did::Config, - { - let Some(del_key_id) = did_details.delegation_key else { - return Ok(vec![].into_iter()); - }; - let del_key_details = did_details.public_keys.get(&del_key_id).ok_or_else(|| { - log::error!("Delegation key should be part of the public keys."); - DidMerkleProofError::Internal - })?; - Ok(vec![RevealedDidKey { - id: del_key_id, - relationship: DidVerificationKeyRelationship::CapabilityDelegation.into(), - details: del_key_details.clone(), - }] - .into_iter()) - } - - fn get_enc_leaves( - did_details: &DidDetails, - ) -> Result< - impl Iterator, BlockNumberFor, Runtime::AccountId>>, - DidMerkleProofError, - > - where - Runtime: did::Config, - { - let keys = did_details - .key_agreement_keys - .iter() - .map(|id| { - let key_agreement_details = did_details.public_keys.get(id).ok_or_else(|| { - log::error!("Key agreement key should be part of the public keys."); - DidMerkleProofError::Internal - })?; - Ok(RevealedDidKey { - id: *id, - relationship: DidKeyRelationship::Encryption, - details: key_agreement_details.clone(), - }) - }) - .collect::, _>>()?; - Ok(keys.into_iter()) - } - - fn get_linked_account_leaves( - linked_accounts: &[LinkableAccountId], - ) -> impl Iterator> + '_ { - linked_accounts.iter().cloned().map(RevealedAccountId) - } - - fn get_web3name_leaf( - web3name_details: &Web3OwnershipOf, - ) -> RevealedWeb3Name, BlockNumberFor> - where - Runtime: pallet_web3_names::Config, - { - RevealedWeb3Name { - web3_name: web3name_details.web3_name.clone(), - claimed_at: web3name_details.claimed_at, - } - } - - /// Given the provided DID info, it calculates the Merkle commitment (root) - /// using the provided in-memory DB. - pub(super) fn calculate_root_with_db( - identity: &LinkedDidInfoOf, - db: &mut MemoryDB, - ) -> Result - where - Runtime: did::Config + pallet_did_lookup::Config + pallet_web3_names::Config, - { - let LinkedDidInfoOf { - did_details, - web3_name_details, - linked_accounts, - } = identity; - let mut trie = TrieHash::>::default(); - let mut trie_builder = TrieDBMutBuilder::>::new(db, &mut trie).build(); - - // Authentication key. - let auth_leaves = get_auth_leaves(did_details)?; - // Attestation key, if present. - let att_leaves = get_att_leaves(did_details)?; - // Delegation key, if present. - let del_leaves = get_del_leaves(did_details)?; - // Key agreement keys. - let enc_leaves = get_enc_leaves(did_details)?; - // Linked accounts. - let linked_accounts = get_linked_account_leaves(linked_accounts); - // Web3name. - let web3_name = web3_name_details.as_ref().map(get_web3name_leaf::); - - // Add all leaves to the proof builder. - let keys = auth_leaves - .chain(att_leaves) - .chain(del_leaves) - .chain(enc_leaves) - .map(RevealedDidMerkleProofLeaf::from); - let linked_accounts = linked_accounts.map(RevealedDidMerkleProofLeaf::from); - let web3_names = web3_name - .map(|n| vec![n]) - .unwrap_or_default() - .into_iter() - .map(RevealedDidMerkleProofLeaf::from); - - keys.chain(linked_accounts).chain(web3_names).try_for_each(|leaf| { - trie_builder - .insert(leaf.encoded_key().as_slice(), leaf.encoded_value().as_slice()) - .map_err(|_| { - log::error!("Failed to insert leaf in the trie builder. Leaf: {:#?}", leaf); - DidMerkleProofError::Internal - })?; - Ok(()) - })?; - - trie_builder.commit(); - Ok(trie_builder.root().to_owned()) - } - - /// Given the provided DID info, and a set of DID key IDs, account IDs and a - /// web3name, generates a Merkle proof that reveals only the provided - /// identity components. The function fails if no key or account with the - /// specified ID can be found, or if a web3name is requested to be revealed - /// in the proof but is not present in the provided identity details. - pub(super) fn generate_proof<'a, Runtime, K, A, const MAX_LINKED_ACCOUNT: u32>( - identity: &LinkedDidInfoOf, - key_ids: K, - should_include_web3_name: bool, - account_ids: A, - ) -> Result>, DidMerkleProofError> - where - Runtime: did::Config + pallet_did_lookup::Config + pallet_web3_names::Config, - K: Iterator>, - A: Iterator, - { - let LinkedDidInfoOf { - did_details, - web3_name_details, - linked_accounts, - } = identity; - - let mut db = MemoryDB::default(); - let root = calculate_root_with_db(identity, &mut db)?; - - let mut leaves = key_ids - .map(|key_id| -> Result<_, DidMerkleProofError> { - let key_details = did_details - .public_keys - .get(key_id) - .ok_or(DidMerkleProofError::KeyNotFound)?; - // Create the merkle leaf key depending on the relationship of the key to the - // DID document. - let key_relationship = if *key_id == did_details.authentication_key { - Ok(DidVerificationKeyRelationship::Authentication.into()) - } else if Some(*key_id) == did_details.attestation_key { - Ok(DidVerificationKeyRelationship::AssertionMethod.into()) - } else if Some(*key_id) == did_details.delegation_key { - Ok(DidVerificationKeyRelationship::CapabilityDelegation.into()) - } else if did_details.key_agreement_keys.contains(key_id) { - Ok(DidKeyRelationship::Encryption) - } else { - log::error!("Unknown key ID {:#?} retrieved from DID details.", key_id); - Err(DidMerkleProofError::Internal) - }?; - Ok(RevealedDidMerkleProofLeaf::from(RevealedDidKey { - id: *key_id, - relationship: key_relationship, - details: key_details.clone(), - })) - }) - .chain(account_ids.map(|account_id| -> Result<_, DidMerkleProofError> { - if linked_accounts.contains(account_id) { - Ok(RevealedDidMerkleProofLeaf::from(RevealedAccountId(account_id.clone()))) - } else { - Err(DidMerkleProofError::LinkedAccountNotFound) - } - })) - .collect::, _>>()?; - - match (should_include_web3_name, web3_name_details) { - // If web3name should be included and it exists, add to the leaves to be revealed... - (true, Some(web3name_details)) => { - leaves.push(RevealedDidMerkleProofLeaf::from(RevealedWeb3Name { - web3_name: web3name_details.web3_name.clone(), - claimed_at: web3name_details.claimed_at, - })); - } - // ...else if web3name should be included and it DOES NOT exist, return an error... - (true, None) => return Err(DidMerkleProofError::Web3NameNotFound), - // ...else (if web3name should NOT be included), skip. - (false, _) => {} - }; - - let encoded_keys: Vec> = leaves.iter().map(|l| l.encoded_key()).collect(); - let proof = - generate_trie_proof::, _, _, _>(&db, root, &encoded_keys).map_err(|_| { - log::error!( - "Failed to generate a merkle proof for the encoded keys: {:#?}", - encoded_keys - ); - DidMerkleProofError::Internal - })?; - - Ok(CompleteMerkleProof { - root, - proof: DidMerkleProofOf::::new(proof.into_iter().into(), leaves), - }) - } - - /// Given the provided DID info, generates a Merkle commitment (root). - pub(super) fn generate_commitment( - identity: &IdentityOf, - ) -> Result - where - Runtime: did::Config + pallet_did_lookup::Config + pallet_web3_names::Config + pallet_dip_provider::Config, - Runtime::IdentityProvider: IdentityProvider>, - { - let mut db = MemoryDB::default(); - calculate_root_with_db(identity, &mut db) - } -} - -/// Type implementing the [`IdentityCommitmentGenerator`] and generating a -/// Merkle root of the provided identity details, according to the description -/// provided in the [README.md](./README.md), -pub struct DidMerkleRootGenerator(PhantomData); - -impl IdentityCommitmentGenerator for DidMerkleRootGenerator -where - Runtime: did::Config + pallet_did_lookup::Config + pallet_web3_names::Config + pallet_dip_provider::Config, - Runtime::IdentityProvider: IdentityProvider>, -{ - type Error = DidMerkleProofError; - type Output = Runtime::Hash; - - fn generate_commitment( - _identifier: &Runtime::Identifier, - identity: &IdentityOf, - version: IdentityCommitmentVersion, - ) -> Result { - match version { - 0 => v0::generate_commitment::(identity), - _ => Err(DidMerkleProofError::UnsupportedVersion), - } - } -} - -impl DidMerkleRootGenerator -where - Runtime: did::Config + pallet_did_lookup::Config + pallet_web3_names::Config, -{ - pub fn generate_proof<'a, K, A, const MAX_LINKED_ACCOUNT: u32>( - identity: &LinkedDidInfoOf, - version: IdentityCommitmentVersion, - key_ids: K, - should_include_web3_name: bool, - account_ids: A, - ) -> Result>, DidMerkleProofError> - where - K: Iterator>, - A: Iterator, - { - match version { - 0 => v0::generate_proof(identity, key_ids, should_include_web3_name, account_ids), - _ => Err(DidMerkleProofError::UnsupportedVersion), - } - } -} diff --git a/runtimes/common/src/dip/merkle/mod.rs b/runtimes/common/src/dip/merkle/mod.rs new file mode 100644 index 000000000..7c5492c97 --- /dev/null +++ b/runtimes/common/src/dip/merkle/mod.rs @@ -0,0 +1,131 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The KILT Blockchain is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +use did::KeyIdOf; +use frame_support::RuntimeDebug; +use frame_system::pallet_prelude::BlockNumberFor; +use kilt_dip_primitives::DidMerkleProof; +use pallet_did_lookup::linkable_account::LinkableAccountId; +use pallet_dip_provider::{ + traits::{IdentityCommitmentGenerator, IdentityProvider}, + IdentityCommitmentVersion, IdentityOf, +}; +use parity_scale_codec::{Decode, Encode}; +use scale_info::TypeInfo; +use sp_std::marker::PhantomData; + +use crate::dip::did::LinkedDidInfoOf; + +pub mod v0; + +#[cfg(test)] +mod tests; + +/// Type of the Merkle proof revealing parts of the DIP identity of a given DID +/// subject. +pub type DidMerkleProofOf = DidMerkleProof< + KeyIdOf, + ::AccountId, + BlockNumberFor, + ::Web3Name, + LinkableAccountId, +>; + +/// Type of a complete DIP Merkle proof. +#[derive(Encode, Decode, RuntimeDebug, PartialEq, Eq, TypeInfo)] +pub struct CompleteMerkleProof { + /// The Merkle root. + pub root: Root, + /// The Merkle proof revealing parts of the commitment that verify against + /// the provided root. + pub proof: Proof, +} + +#[derive(Clone, RuntimeDebug, Encode, Decode, TypeInfo, PartialEq)] +pub enum DidMerkleProofError { + UnsupportedVersion, + KeyNotFound, + LinkedAccountNotFound, + Web3NameNotFound, + TooManyLeaves, + Internal, +} + +impl From for u16 { + fn from(value: DidMerkleProofError) -> Self { + match value { + // DO NOT USE 0 + // Errors of different sub-parts are separated by a `u8::MAX`. + // A value of 0 would make it confusing whether it's the previous sub-part error (u8::MAX) + // or the new sub-part error (u8::MAX + 0). + DidMerkleProofError::UnsupportedVersion => 1, + DidMerkleProofError::KeyNotFound => 2, + DidMerkleProofError::LinkedAccountNotFound => 3, + DidMerkleProofError::Web3NameNotFound => 4, + DidMerkleProofError::TooManyLeaves => 5, + DidMerkleProofError::Internal => u16::MAX, + } + } +} + +/// Type implementing the [`IdentityCommitmentGenerator`] and generating a +/// Merkle root of the provided identity details, according to the description +/// provided in the [README.md](./README.md), +pub struct DidMerkleRootGenerator(PhantomData); + +impl IdentityCommitmentGenerator for DidMerkleRootGenerator +where + Runtime: did::Config + pallet_did_lookup::Config + pallet_web3_names::Config + pallet_dip_provider::Config, + Runtime::IdentityProvider: IdentityProvider>, +{ + type Error = DidMerkleProofError; + type Output = Runtime::Hash; + + fn generate_commitment( + _identifier: &Runtime::Identifier, + identity: &IdentityOf, + version: IdentityCommitmentVersion, + ) -> Result { + match version { + 0 => v0::generate_commitment::(identity), + _ => Err(DidMerkleProofError::UnsupportedVersion), + } + } +} + +impl DidMerkleRootGenerator +where + Runtime: did::Config + pallet_did_lookup::Config + pallet_web3_names::Config, +{ + pub fn generate_proof<'a, K, A, const MAX_LINKED_ACCOUNT: u32>( + identity: &LinkedDidInfoOf, + version: IdentityCommitmentVersion, + key_ids: K, + should_include_web3_name: bool, + account_ids: A, + ) -> Result>, DidMerkleProofError> + where + K: Iterator>, + A: Iterator, + { + match version { + 0 => v0::generate_proof(identity, key_ids, should_include_web3_name, account_ids), + _ => Err(DidMerkleProofError::UnsupportedVersion), + } + } +} diff --git a/runtimes/common/src/dip/merkle/tests/generate_commitment.rs b/runtimes/common/src/dip/merkle/tests/generate_commitment.rs new file mode 100644 index 000000000..0b352a338 --- /dev/null +++ b/runtimes/common/src/dip/merkle/tests/generate_commitment.rs @@ -0,0 +1,42 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The KILT Blockchain is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +use did::did_details::DidVerificationKey; +use frame_support::assert_err; +use pallet_dip_provider::traits::IdentityCommitmentGenerator; + +use crate::{ + constants::dip_provider::MAX_LINKED_ACCOUNTS, + dip::{ + merkle::{DidMerkleProofError, DidMerkleRootGenerator}, + mock::{create_linked_info, TestRuntime, ACCOUNT, DID_IDENTIFIER}, + }, +}; + +#[test] +fn generate_commitment_unsupported_version() { + let linked_info = create_linked_info( + DidVerificationKey::Account(ACCOUNT), + Some(b"ntn_x2"), + MAX_LINKED_ACCOUNTS, + ); + assert_err!( + DidMerkleRootGenerator::::generate_commitment(&DID_IDENTIFIER, &linked_info, 1,), + DidMerkleProofError::UnsupportedVersion + ); +} diff --git a/runtimes/common/src/dip/merkle/tests/generate_proof.rs b/runtimes/common/src/dip/merkle/tests/generate_proof.rs new file mode 100644 index 000000000..76c24fe02 --- /dev/null +++ b/runtimes/common/src/dip/merkle/tests/generate_proof.rs @@ -0,0 +1,41 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The KILT Blockchain is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +use did::did_details::DidVerificationKey; +use frame_support::assert_err; + +use crate::{ + constants::dip_provider::MAX_LINKED_ACCOUNTS, + dip::{ + merkle::{DidMerkleProofError, DidMerkleRootGenerator}, + mock::{create_linked_info, TestRuntime, ACCOUNT}, + }, +}; + +#[test] +fn generate_proof_unsupported_version() { + let linked_info = create_linked_info( + DidVerificationKey::Account(ACCOUNT), + Some(b"ntn_x2"), + MAX_LINKED_ACCOUNTS, + ); + assert_err!( + DidMerkleRootGenerator::::generate_proof(&linked_info, 1, [].into_iter(), false, [].into_iter()), + DidMerkleProofError::UnsupportedVersion + ); +} diff --git a/runtimes/common/src/dip/merkle/tests/mod.rs b/runtimes/common/src/dip/merkle/tests/mod.rs new file mode 100644 index 000000000..6078b8cd1 --- /dev/null +++ b/runtimes/common/src/dip/merkle/tests/mod.rs @@ -0,0 +1,20 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The KILT Blockchain is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +mod generate_commitment; +mod generate_proof; diff --git a/runtimes/common/src/dip/merkle/v0/mod.rs b/runtimes/common/src/dip/merkle/v0/mod.rs new file mode 100644 index 000000000..fdb66b959 --- /dev/null +++ b/runtimes/common/src/dip/merkle/v0/mod.rs @@ -0,0 +1,333 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The KILT Blockchain is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +use did::{did_details::DidDetails, DidVerificationKeyRelationship, KeyIdOf}; +use frame_system::pallet_prelude::BlockNumberFor; +use kilt_dip_primitives::{ + DidKeyRelationship, RevealedAccountId, RevealedDidKey, RevealedDidMerkleProofLeaf, RevealedWeb3Name, +}; +use pallet_did_lookup::linkable_account::LinkableAccountId; +use pallet_dip_provider::{traits::IdentityProvider, IdentityOf}; +use pallet_web3_names::Web3NameOf; +use sp_std::{prelude::ToOwned, vec, vec::Vec}; +use sp_trie::{generate_trie_proof, LayoutV1, MemoryDB, TrieDBMutBuilder, TrieHash, TrieMut}; + +use crate::dip::{ + did::{LinkedDidInfoOf, Web3OwnershipOf}, + merkle::{CompleteMerkleProof, DidMerkleProofError, DidMerkleProofOf}, +}; + +#[cfg(test)] +mod tests; + +fn get_auth_leaves( + did_details: &DidDetails, +) -> Result< + impl Iterator, BlockNumberFor, Runtime::AccountId>>, + DidMerkleProofError, +> +where + Runtime: did::Config, +{ + let auth_key_details = did_details + .public_keys + .get(&did_details.authentication_key) + .ok_or_else(|| { + log::error!("Authentication key should be part of the public keys."); + DidMerkleProofError::Internal + })?; + Ok([RevealedDidKey { + id: did_details.authentication_key, + relationship: DidVerificationKeyRelationship::Authentication.into(), + details: auth_key_details.clone(), + }] + .into_iter()) +} + +fn get_att_leaves( + did_details: &DidDetails, +) -> Result< + impl Iterator, BlockNumberFor, Runtime::AccountId>>, + DidMerkleProofError, +> +where + Runtime: did::Config, +{ + let Some(att_key_id) = did_details.attestation_key else { + return Ok(vec![].into_iter()); + }; + let att_key_details = did_details.public_keys.get(&att_key_id).ok_or_else(|| { + log::error!("Attestation key should be part of the public keys."); + DidMerkleProofError::Internal + })?; + Ok(vec![RevealedDidKey { + id: att_key_id, + relationship: DidVerificationKeyRelationship::AssertionMethod.into(), + details: att_key_details.clone(), + }] + .into_iter()) +} + +fn get_del_leaves( + did_details: &DidDetails, +) -> Result< + impl Iterator, BlockNumberFor, Runtime::AccountId>>, + DidMerkleProofError, +> +where + Runtime: did::Config, +{ + let Some(del_key_id) = did_details.delegation_key else { + return Ok(vec![].into_iter()); + }; + let del_key_details = did_details.public_keys.get(&del_key_id).ok_or_else(|| { + log::error!("Delegation key should be part of the public keys."); + DidMerkleProofError::Internal + })?; + Ok(vec![RevealedDidKey { + id: del_key_id, + relationship: DidVerificationKeyRelationship::CapabilityDelegation.into(), + details: del_key_details.clone(), + }] + .into_iter()) +} + +fn get_enc_leaves( + did_details: &DidDetails, +) -> Result< + impl Iterator, BlockNumberFor, Runtime::AccountId>>, + DidMerkleProofError, +> +where + Runtime: did::Config, +{ + let keys = did_details + .key_agreement_keys + .iter() + .map(|id| { + let key_agreement_details = did_details.public_keys.get(id).ok_or_else(|| { + log::error!("Key agreement key should be part of the public keys."); + DidMerkleProofError::Internal + })?; + Ok(RevealedDidKey { + id: *id, + relationship: DidKeyRelationship::Encryption, + details: key_agreement_details.clone(), + }) + }) + .collect::, _>>()?; + Ok(keys.into_iter()) +} + +fn get_linked_account_leaves( + linked_accounts: &[LinkableAccountId], +) -> impl Iterator> + '_ { + linked_accounts.iter().cloned().map(RevealedAccountId) +} + +fn get_web3name_leaf( + web3name_details: &Web3OwnershipOf, +) -> RevealedWeb3Name, BlockNumberFor> +where + Runtime: pallet_web3_names::Config, +{ + RevealedWeb3Name { + web3_name: web3name_details.web3_name.clone(), + claimed_at: web3name_details.claimed_at, + } +} + +/// Given the provided DID info, it calculates the Merkle commitment (root) +/// using the provided in-memory DB. +pub(super) fn calculate_root_with_db( + identity: &LinkedDidInfoOf, + db: &mut MemoryDB, +) -> Result +where + Runtime: did::Config + pallet_did_lookup::Config + pallet_web3_names::Config, +{ + let LinkedDidInfoOf { + did_details, + web3_name_details, + linked_accounts, + } = identity; + let mut trie = TrieHash::>::default(); + let mut trie_builder = TrieDBMutBuilder::>::new(db, &mut trie).build(); + + // Authentication key. + let auth_leaves = get_auth_leaves(did_details)?; + // Attestation key, if present. + let att_leaves = get_att_leaves(did_details)?; + // Delegation key, if present. + let del_leaves = get_del_leaves(did_details)?; + // Key agreement keys. + let enc_leaves = get_enc_leaves(did_details)?; + // Linked accounts. + let linked_accounts = get_linked_account_leaves(linked_accounts); + // Web3name. + let web3_name = web3_name_details.as_ref().map(get_web3name_leaf::); + + // Add all leaves to the proof builder. + let keys = auth_leaves + .chain(att_leaves) + .chain(del_leaves) + .chain(enc_leaves) + .map(RevealedDidMerkleProofLeaf::from); + let linked_accounts = linked_accounts.map(RevealedDidMerkleProofLeaf::from); + let web3_names = web3_name + .map(|n| vec![n]) + .unwrap_or_default() + .into_iter() + .map(RevealedDidMerkleProofLeaf::from); + + keys.chain(linked_accounts).chain(web3_names).try_for_each(|leaf| { + trie_builder + .insert(leaf.encoded_key().as_slice(), leaf.encoded_value().as_slice()) + .map_err(|_| { + log::error!("Failed to insert leaf in the trie builder. Leaf: {:#?}", leaf); + DidMerkleProofError::Internal + })?; + Ok(()) + })?; + + trie_builder.commit(); + Ok(trie_builder.root().to_owned()) +} + +/// Given the provided DID info, and a set of DID key IDs, account IDs and a +/// web3name, generates a Merkle proof that reveals only the provided +/// identity components. The function fails if no key or account with the +/// specified ID can be found, or if a web3name is requested to be revealed +/// in the proof but is not present in the provided identity details. +pub(super) fn generate_proof<'a, Runtime, K, A, const MAX_LINKED_ACCOUNT: u32>( + identity: &LinkedDidInfoOf, + key_ids: K, + should_include_web3_name: bool, + account_ids: A, +) -> Result>, DidMerkleProofError> +where + Runtime: did::Config + pallet_did_lookup::Config + pallet_web3_names::Config, + K: Iterator>, + A: Iterator, +{ + let LinkedDidInfoOf { + did_details, + web3_name_details, + linked_accounts, + } = identity; + + let mut db = MemoryDB::default(); + let root = calculate_root_with_db(identity, &mut db)?; + + let did_key_leaves_iter = key_ids.map(|key_id| -> Result<_, DidMerkleProofError> { + let key_details = did_details + .public_keys + .get(key_id) + .ok_or(DidMerkleProofError::KeyNotFound)?; + let key_relationships = { + if did_details.key_agreement_keys.contains(key_id) { + Ok(vec![DidKeyRelationship::Encryption]) + } else { + let mut key_relationships = Vec::::with_capacity(3); + if *key_id == did_details.authentication_key { + key_relationships.push(DidVerificationKeyRelationship::Authentication.into()); + } + if Some(*key_id) == did_details.attestation_key { + key_relationships.push(DidVerificationKeyRelationship::AssertionMethod.into()); + } + if Some(*key_id) == did_details.delegation_key { + key_relationships.push(DidVerificationKeyRelationship::CapabilityDelegation.into()); + } + if key_relationships.is_empty() { + log::error!("Unknown key ID {:#?} retrieved from DID details.", key_id); + Err(DidMerkleProofError::Internal) + } else { + Ok(key_relationships) + } + } + }?; + let leaves_for_key = key_relationships + .into_iter() + .map(|relationship| { + RevealedDidMerkleProofLeaf::from(RevealedDidKey { + id: *key_id, + relationship, + details: key_details.clone(), + }) + }) + .collect::>(); + Ok(leaves_for_key) + }); + + let linked_accounts_iter = account_ids.map(|account_id| -> Result<_, DidMerkleProofError> { + if linked_accounts.contains(account_id) { + Ok(vec![RevealedDidMerkleProofLeaf::from(RevealedAccountId( + account_id.clone(), + ))]) + } else { + Err(DidMerkleProofError::LinkedAccountNotFound) + } + }); + + let mut leaves = did_key_leaves_iter + .chain(linked_accounts_iter) + .collect::, _>>()?; + + match (should_include_web3_name, web3_name_details) { + // If web3name should be included and it exists, add to the leaves to be revealed... + (true, Some(web3name_details)) => { + leaves.push(vec![RevealedDidMerkleProofLeaf::from(RevealedWeb3Name { + web3_name: web3name_details.web3_name.clone(), + claimed_at: web3name_details.claimed_at, + })]); + } + // ...else if web3name should be included and it DOES NOT exist, return an error... + (true, None) => return Err(DidMerkleProofError::Web3NameNotFound), + // ...else (if web3name should NOT be included), skip. + (false, _) => {} + }; + + let encoded_keys: Vec> = leaves.iter().flatten().map(|l| l.encoded_key()).collect(); + let proof = generate_trie_proof::, _, _, _>(&db, root, &encoded_keys).map_err(|_| { + log::error!( + "Failed to generate a merkle proof for the encoded keys: {:#?}", + encoded_keys + ); + DidMerkleProofError::Internal + })?; + + Ok(CompleteMerkleProof { + root, + proof: DidMerkleProofOf::::new( + proof.into_iter().into(), + leaves.into_iter().flatten().collect::>(), + ), + }) +} + +/// Given the provided DID info, generates a Merkle commitment (root). +pub(super) fn generate_commitment( + identity: &IdentityOf, +) -> Result +where + Runtime: did::Config + pallet_did_lookup::Config + pallet_web3_names::Config + pallet_dip_provider::Config, + Runtime::IdentityProvider: IdentityProvider>, +{ + let mut db = MemoryDB::default(); + calculate_root_with_db(identity, &mut db) +} diff --git a/runtimes/common/src/dip/merkle/v0/tests/generate_commitment.rs b/runtimes/common/src/dip/merkle/v0/tests/generate_commitment.rs new file mode 100644 index 000000000..5bd23d08f --- /dev/null +++ b/runtimes/common/src/dip/merkle/v0/tests/generate_commitment.rs @@ -0,0 +1,45 @@ +use did::did_details::DidVerificationKey; + +use crate::{ + constants::dip_provider::MAX_LINKED_ACCOUNTS, + dip::{ + merkle::v0::generate_commitment, + mock::{create_linked_info, TestRuntime, ACCOUNT}, + }, +}; + +#[test] +fn generate_commitment_for_complete_info() { + let linked_info = create_linked_info( + DidVerificationKey::Account(ACCOUNT), + Some(b"ntn_x2"), + MAX_LINKED_ACCOUNTS, + ); + let commitment_result = generate_commitment::(&linked_info); + assert!(commitment_result.is_ok()); +} + +#[test] +fn generate_commitment_for_did_details() { + let linked_info = create_linked_info(DidVerificationKey::Account(ACCOUNT), Option::>::None, 0); + let commitment_result = generate_commitment::(&linked_info); + assert!(commitment_result.is_ok()); +} + +#[test] +fn generate_commitment_for_did_details_and_web3name() { + let linked_info = create_linked_info(DidVerificationKey::Account(ACCOUNT), Some(b"ntn_x2"), 0); + let commitment_result = generate_commitment::(&linked_info); + assert!(commitment_result.is_ok()); +} + +#[test] +fn generate_commitment_for_did_details_and_max_linked_accounts() { + let linked_info = create_linked_info( + DidVerificationKey::Account(ACCOUNT), + Option::>::None, + MAX_LINKED_ACCOUNTS, + ); + let commitment_result = generate_commitment::(&linked_info); + assert!(commitment_result.is_ok()); +} diff --git a/runtimes/common/src/dip/merkle/v0/tests/generate_proof.rs b/runtimes/common/src/dip/merkle/v0/tests/generate_proof.rs new file mode 100644 index 000000000..5948ff4c1 --- /dev/null +++ b/runtimes/common/src/dip/merkle/v0/tests/generate_proof.rs @@ -0,0 +1,496 @@ +use did::{ + did_details::{DidDetails, DidPublicKeyDetails, DidVerificationKey}, + DidVerificationKeyRelationship, KeyIdOf, +}; +use frame_support::{assert_err, assert_ok}; +use kilt_dip_primitives::{ + DidKeyRelationship, DipDidProofWithVerifiedSubjectCommitment, RevealedDidKey, RevealedDidMerkleProofLeaf, + RevealedWeb3Name, TimeBoundDidSignature, +}; +use pallet_web3_names::Web3NameOf; +use parity_scale_codec::Encode; +use sp_core::{ed25519, sr25519, Pair}; +use sp_runtime::AccountId32; + +use crate::{ + constants::{ + did::{MAX_KEY_AGREEMENT_KEYS, MAX_PUBLIC_KEYS_PER_DID}, + dip_provider::MAX_LINKED_ACCOUNTS, + }, + dip::{ + merkle::{v0::generate_proof, CompleteMerkleProof, DidMerkleProofError}, + mock::{create_linked_info, TestRuntime}, + }, + AccountId, BlockNumber, Hasher, +}; + +const MAX_LEAVES_REVEALED: u32 = MAX_LINKED_ACCOUNTS + MAX_PUBLIC_KEYS_PER_DID + 1; + +// Verify if a given DID key revealed in a DIP proof matches the key from the +// provided DID Document. The comparison checks for the actual key information +// (public key and creation block number) and for its relationship to the DID +// Document. +fn do_stored_key_and_revealed_key_match( + did_details: &DidDetails, + stored_key: &DidPublicKeyDetails, + revealed_key: &RevealedDidKey, BlockNumber, AccountId>, +) -> bool { + let RevealedDidKey { + id: revealed_key_id, + relationship: revealed_key_relationship, + details: revealed_key_details, + } = revealed_key; + let is_same_key_material = revealed_key_details == stored_key; + let is_of_right_relationship = match revealed_key_relationship { + DidKeyRelationship::Encryption => did_details.key_agreement_keys.contains(revealed_key_id), + DidKeyRelationship::Verification(DidVerificationKeyRelationship::Authentication) => { + did_details.authentication_key == *revealed_key_id + } + DidKeyRelationship::Verification(DidVerificationKeyRelationship::AssertionMethod) => { + did_details.attestation_key == Some(*revealed_key_id) + } + DidKeyRelationship::Verification(DidVerificationKeyRelationship::CapabilityDelegation) => { + did_details.delegation_key == Some(*revealed_key_id) + } + DidKeyRelationship::Verification(DidVerificationKeyRelationship::CapabilityInvocation) => { + panic!("DID document should not have any key for capability delegation.") + } + }; + is_same_key_material && is_of_right_relationship +} + +#[test] +fn generate_proof_for_complete_linked_info() { + let auth_key = ed25519::Pair::from_seed(&[10u8; 32]); + let did_auth_key = DidVerificationKey::Ed25519(auth_key.public()); + let linked_info = create_linked_info(did_auth_key, Some(b"ntn_x2"), MAX_LINKED_ACCOUNTS); + let signature = auth_key.sign(&().encode()); + + // 1. Generate a proof over all the linked info. + let CompleteMerkleProof { proof, root } = generate_proof( + &linked_info, + linked_info.did_details.public_keys.keys(), + true, + linked_info.linked_accounts.iter(), + ) + .unwrap(); + let cross_chain_proof = DipDidProofWithVerifiedSubjectCommitment::new( + root, + proof, + TimeBoundDidSignature::new(signature.clone().into(), 100), + ); + + let dip_origin_info = cross_chain_proof + .verify_dip_proof::() + .and_then(|r| r.verify_signature_time(&50)) + .and_then(|r| r.retrieve_signing_leaf_for_payload(&().encode())) + .unwrap(); + // All key agreement keys, plus authentication, attestation, and delegation key, + // plus all linked accounts, plus web3name. + let expected_leaves_revealed = (MAX_KEY_AGREEMENT_KEYS + 3 + MAX_LINKED_ACCOUNTS + 1) as usize; + assert_eq!(dip_origin_info.iter_leaves().count(), expected_leaves_revealed); + + let did_keys = dip_origin_info + .iter_leaves() + .cloned() + .filter_map(|leaf| { + if let RevealedDidMerkleProofLeaf::DidKey(key) = leaf { + Some(key) + } else { + None + } + }) + .collect::>(); + // Make sure the revealed keys all belong to the DID Document... + assert!(did_keys.iter().all(|revealed_did_key| { + let stored_key = linked_info.did_details.public_keys.get(&revealed_did_key.id).unwrap(); + do_stored_key_and_revealed_key_match(&linked_info.did_details, stored_key, revealed_did_key) + })); + // ...and that no key from the DID document is left out. + assert!(linked_info + .did_details + .public_keys + .iter() + .all(|(stored_key_id, stored_key_details)| { + let matching_revealed_key = did_keys.iter().find(|did_key| did_key.id == *stored_key_id).unwrap(); + do_stored_key_and_revealed_key_match(&linked_info.did_details, stored_key_details, matching_revealed_key) + })); + + let web3names = dip_origin_info + .iter_leaves() + .cloned() + .filter_map(|leaf| { + if let RevealedDidMerkleProofLeaf::Web3Name(name) = leaf { + Some(name) + } else { + None + } + }) + .collect::>(); + // Make sure the only web3name is revealed and it is the correct one. + assert_eq!(web3names.len(), 1); + assert_eq!( + web3names.first(), + Some(&RevealedWeb3Name { + web3_name: b"ntn_x2".to_vec().try_into().unwrap(), + claimed_at: BlockNumber::default() + }) + ); + + let linked_accounts = dip_origin_info + .iter_leaves() + .cloned() + .filter_map(|leaf| { + if let RevealedDidMerkleProofLeaf::LinkedAccount(acc) = leaf { + Some(acc) + } else { + None + } + }) + .collect::>(); + // Make sure the revealed accounts all belong to the DID Document... + assert!(linked_accounts + .iter() + .all(|revealed_account| { linked_info.linked_accounts.contains(&revealed_account.0) })); + // ...and that no account from the ones linked to the DID document is left out. + assert!(linked_info + .linked_accounts + .iter() + .all(|linked_account| { linked_accounts.iter().any(|l| l.0 == *linked_account) })); + + // 2. Generate a proof without any parts revealed. + let CompleteMerkleProof { proof, root } = generate_proof(&linked_info, [].iter(), false, [].iter()).unwrap(); + let cross_chain_proof = DipDidProofWithVerifiedSubjectCommitment::new( + root, + proof, + TimeBoundDidSignature::new(signature.clone().into(), 100), + ); + // Should verify the merkle proof successfully. + assert_ok!(cross_chain_proof.verify_dip_proof::()); + + // 3. Generate a proof with only the authentication key revealed. + let CompleteMerkleProof { proof, root } = generate_proof( + &linked_info, + [linked_info.did_details.authentication_key].iter(), + false, + [].iter(), + ) + .unwrap(); + let cross_chain_proof = DipDidProofWithVerifiedSubjectCommitment::new( + root, + proof, + TimeBoundDidSignature::new(signature.clone().into(), 100), + ); + + let dip_origin_info = cross_chain_proof + .verify_dip_proof::() + .and_then(|r| r.verify_signature_time(&50)) + .and_then(|r| r.retrieve_signing_leaf_for_payload(&().encode())) + .unwrap(); + // Only the authentication key. + let expected_leaves_revealed = 1; + assert_eq!(dip_origin_info.iter_leaves().count(), expected_leaves_revealed); + + let did_key = &dip_origin_info + .iter_leaves() + .cloned() + .filter_map(|leaf| { + if let RevealedDidMerkleProofLeaf::DidKey(key) = leaf { + Some(key) + } else { + None + } + }) + .collect::>()[0]; + assert_eq!(did_key.id, linked_info.did_details.authentication_key); + assert!(do_stored_key_and_revealed_key_match( + &linked_info.did_details, + linked_info + .did_details + .public_keys + .get(&linked_info.did_details.authentication_key) + .unwrap(), + did_key + )); + + // 4. Generate a proof with only the web3name revealed. + let CompleteMerkleProof { proof, root } = generate_proof(&linked_info, [].iter(), true, [].iter()).unwrap(); + let cross_chain_proof = DipDidProofWithVerifiedSubjectCommitment::new( + root, + proof, + TimeBoundDidSignature::new(signature.clone().into(), 100), + ); + // Should verify the merkle proof successfully. + assert_ok!(cross_chain_proof.verify_dip_proof::()); + + // 5. Generate a proof with only one linked account revealed. + let CompleteMerkleProof { proof, root } = generate_proof( + &linked_info, + [].iter(), + true, + [linked_info.linked_accounts[0].clone()].iter(), + ) + .unwrap(); + let cross_chain_proof = DipDidProofWithVerifiedSubjectCommitment::new( + root, + proof, + TimeBoundDidSignature::new(signature.clone().into(), 100), + ); + // Should verify the merkle proof successfully. + assert_ok!(cross_chain_proof.verify_dip_proof::()); + + // 6. Generate a proof with only the authentication key and the web3name + // revealed. + let CompleteMerkleProof { proof, root } = generate_proof( + &linked_info, + [linked_info.did_details.authentication_key].iter(), + true, + [].iter(), + ) + .unwrap(); + let cross_chain_proof = DipDidProofWithVerifiedSubjectCommitment::new( + root, + proof, + TimeBoundDidSignature::new(signature.clone().into(), 100), + ); + let dip_origin_info = cross_chain_proof + .verify_dip_proof::() + .and_then(|r| r.verify_signature_time(&50)) + .and_then(|r| r.retrieve_signing_leaf_for_payload(&().encode())) + .unwrap(); + // The authentication key and the web3name. + let expected_leaves_revealed = 2; + assert_eq!(dip_origin_info.iter_leaves().count(), expected_leaves_revealed); + + let did_key = &dip_origin_info + .iter_leaves() + .cloned() + .filter_map(|leaf| { + if let RevealedDidMerkleProofLeaf::DidKey(key) = leaf { + Some(key) + } else { + None + } + }) + .collect::>()[0]; + assert_eq!(did_key.id, linked_info.did_details.authentication_key); + assert!(do_stored_key_and_revealed_key_match( + &linked_info.did_details, + linked_info + .did_details + .public_keys + .get(&linked_info.did_details.authentication_key) + .unwrap(), + did_key + )); + let web3_name = &dip_origin_info + .iter_leaves() + .cloned() + .filter_map(|leaf| { + if let RevealedDidMerkleProofLeaf::Web3Name(web3_name) = leaf { + Some(web3_name) + } else { + None + } + }) + .collect::>()[0]; + assert_eq!(linked_info.web3_name_details.as_ref(), Some(web3_name)); + + // 7. Generate a proof with only the authentication key and one linked account + // revealed. + let CompleteMerkleProof { proof, root } = generate_proof( + &linked_info, + [linked_info.did_details.authentication_key].iter(), + false, + [linked_info.linked_accounts[0].clone()].iter(), + ) + .unwrap(); + let cross_chain_proof = + DipDidProofWithVerifiedSubjectCommitment::new(root, proof, TimeBoundDidSignature::new(signature.into(), 100)); + let dip_origin_info = cross_chain_proof + .verify_dip_proof::() + .and_then(|r| r.verify_signature_time(&50)) + .and_then(|r| r.retrieve_signing_leaf_for_payload(&().encode())) + .unwrap(); + // The authentication key and the web3name. + let expected_leaves_revealed = 2; + assert_eq!(dip_origin_info.iter_leaves().count(), expected_leaves_revealed); + + let did_key = &dip_origin_info + .iter_leaves() + .cloned() + .filter_map(|leaf| { + if let RevealedDidMerkleProofLeaf::DidKey(key) = leaf { + Some(key) + } else { + None + } + }) + .collect::>()[0]; + assert_eq!(did_key.id, linked_info.did_details.authentication_key); + assert!(do_stored_key_and_revealed_key_match( + &linked_info.did_details, + linked_info + .did_details + .public_keys + .get(&linked_info.did_details.authentication_key) + .unwrap(), + did_key + )); + let linked_account = &dip_origin_info + .iter_leaves() + .cloned() + .filter_map(|leaf| { + if let RevealedDidMerkleProofLeaf::LinkedAccount(linked_account) = leaf { + Some(linked_account) + } else { + None + } + }) + .collect::>()[0]; + assert!(linked_info.linked_accounts.contains(&linked_account.0)); + + // 8. Fails to generate the proof for a key that does not exist. + assert_err!( + generate_proof( + &linked_info, + [KeyIdOf::::default()].iter(), + false, + [].iter(), + ), + DidMerkleProofError::KeyNotFound + ); + + // 9. Fails to generate the proof for an account that does not exist. + assert_err!( + generate_proof( + &linked_info, + [].iter(), + false, + [AccountId32::new([u8::MAX; 32]).into()].iter(), + ), + DidMerkleProofError::LinkedAccountNotFound + ); +} + +#[test] +fn generate_proof_with_only_auth_key() { + let auth_key = sr25519::Pair::from_seed(&[10u8; 32]); + let did_auth_key = DidVerificationKey::Sr25519(auth_key.public()); + let linked_info = create_linked_info(did_auth_key, Option::>::None, 0); + + // 1. Fails to generate the proof for a key that does not exist. + assert_err!( + generate_proof( + &linked_info, + [KeyIdOf::::default()].iter(), + false, + [].iter(), + ), + DidMerkleProofError::KeyNotFound + ); + + // 2. Fails to generate the proof for the web3name. + assert_err!( + generate_proof(&linked_info, [].iter(), true, [].iter(),), + DidMerkleProofError::Web3NameNotFound + ); + + // 3. Fails to generate the proof for an account that does not exist. + assert_err!( + generate_proof( + &linked_info, + [].iter(), + false, + [AccountId32::new([u8::MAX; 32]).into()].iter(), + ), + DidMerkleProofError::LinkedAccountNotFound + ); +} + +#[test] +fn generate_proof_with_two_keys_with_same_id() { + let auth_key = ed25519::Pair::from_seed(&[10u8; 32]); + let did_auth_key = DidVerificationKey::Ed25519(auth_key.public()); + let linked_info = { + let mut info = create_linked_info(did_auth_key.clone(), Option::>::None, 0); + info.did_details + .update_attestation_key(did_auth_key, BlockNumber::default()) + .unwrap(); + // Remove all key agreement keys + let key_agreement_key_ids = info + .did_details + .key_agreement_keys + .clone() + .into_iter() + .collect::>(); + key_agreement_key_ids.into_iter().for_each(|k: sp_core::H256| { + info.did_details.remove_key_agreement_key(k).unwrap(); + }); + // Remove delegation key, if present + let _ = info.did_details.remove_delegation_key(); + info + }; + let signature = auth_key.sign(&().encode()); + + let CompleteMerkleProof { proof, root } = generate_proof( + &linked_info, + linked_info.did_details.public_keys.keys(), + false, + linked_info.linked_accounts.iter(), + ) + .unwrap(); + let cross_chain_proof = + DipDidProofWithVerifiedSubjectCommitment::new(root, proof, TimeBoundDidSignature::new(signature.into(), 100)); + + let dip_origin_info = cross_chain_proof + .verify_dip_proof::() + .and_then(|r| r.verify_signature_time(&50)) + .and_then(|r| r.retrieve_signing_leaf_for_payload(&().encode())) + .unwrap(); + // Authentication key and attestation key have the same key ID, but they are + // different keys, so there should be 2 leaves. + let expected_leaves_revealed = 2; + assert_eq!(dip_origin_info.iter_leaves().count(), expected_leaves_revealed); + + let did_keys = { + let mut did_keys = dip_origin_info + .iter_leaves() + .cloned() + .filter_map(|leaf| { + if let RevealedDidMerkleProofLeaf::DidKey(key) = leaf { + Some(key) + } else { + None + } + }) + .collect::>(); + did_keys.sort(); + did_keys + }; + assert_eq!( + did_keys, + vec![ + RevealedDidKey { + id: linked_info.did_details.authentication_key, + relationship: DidVerificationKeyRelationship::Authentication.into(), + details: linked_info + .did_details + .public_keys + .get(&linked_info.did_details.authentication_key) + .unwrap() + .clone() + }, + RevealedDidKey { + id: linked_info.did_details.attestation_key.unwrap(), + relationship: DidVerificationKeyRelationship::AssertionMethod.into(), + details: linked_info + .did_details + .public_keys + .get(&linked_info.did_details.attestation_key.unwrap()) + .unwrap() + .clone() + } + ] + ); +} diff --git a/runtimes/common/src/dip/merkle/v0/tests/mod.rs b/runtimes/common/src/dip/merkle/v0/tests/mod.rs new file mode 100644 index 000000000..6078b8cd1 --- /dev/null +++ b/runtimes/common/src/dip/merkle/v0/tests/mod.rs @@ -0,0 +1,20 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The KILT Blockchain is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +mod generate_commitment; +mod generate_proof; diff --git a/runtimes/common/src/dip/mock.rs b/runtimes/common/src/dip/mock.rs new file mode 100644 index 000000000..8d4e68e1f --- /dev/null +++ b/runtimes/common/src/dip/mock.rs @@ -0,0 +1,319 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The KILT Blockchain is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +use did::{ + did_details::{DidDetails, DidEncryptionKey, DidVerificationKey}, + mock_utils::generate_base_did_details, + DeriveDidCallAuthorizationVerificationKeyRelationship, +}; +use frame_support::{ + construct_runtime, + traits::{Currency, Everything}, + Hashable, +}; +use frame_system::{mocking::MockBlock, pallet_prelude::BlockNumberFor, EnsureRoot, EnsureSigned}; +use kilt_dip_primitives::RevealedWeb3Name; +use pallet_did_lookup::{account::AccountId20, linkable_account::LinkableAccountId}; +use pallet_web3_names::{web3_name::AsciiWeb3Name, Web3NameOf}; +use sp_core::{sr25519, ConstU128, ConstU16, ConstU32, ConstU64}; +use sp_runtime::{traits::IdentityLookup, AccountId32, BoundedVec}; + +use crate::{ + constants::{ + did::{ + MaxNewKeyAgreementKeys, MaxNumberOfServicesPerDid, MaxNumberOfTypesPerService, MaxNumberOfUrlsPerService, + MaxPublicKeysPerDid, MaxServiceIdLength, MaxServiceTypeLength, MaxServiceUrlLength, + MaxTotalKeyAgreementKeys, MAX_KEY_AGREEMENT_KEYS, + }, + dip_provider::MAX_LINKED_ACCOUNTS, + web3_names::{MaxNameLength, MinNameLength}, + KILT, + }, + dip::{ + did::{LinkedDidInfoOf, LinkedDidInfoProvider}, + merkle::DidMerkleRootGenerator, + }, + AccountId, Balance, BlockHashCount, BlockLength, BlockWeights, DidIdentifier, Hash, Hasher, Nonce, +}; + +construct_runtime!( + pub struct TestRuntime { + System: frame_system, + Balances: pallet_balances, + Did: did, + Web3Names: pallet_web3_names, + DidLookup: pallet_did_lookup, + DipProvider: pallet_dip_provider, + } +); + +impl frame_system::Config for TestRuntime { + type AccountData = pallet_balances::AccountData; + type AccountId = AccountId; + type BaseCallFilter = Everything; + type Block = MockBlock; + type BlockHashCount = BlockHashCount; + type BlockLength = BlockLength; + type BlockWeights = BlockWeights; + type DbWeight = (); + type Hash = Hash; + type Hashing = Hasher; + type Lookup = IdentityLookup; + type MaxConsumers = ConstU32<16>; + type Nonce = Nonce; + type OnKilledAccount = (); + type OnNewAccount = (); + type OnSetCode = (); + type PalletInfo = PalletInfo; + type RuntimeCall = RuntimeCall; + type RuntimeEvent = RuntimeEvent; + type RuntimeOrigin = RuntimeOrigin; + type SS58Prefix = ConstU16<1>; + type SystemWeightInfo = (); + type Version = (); +} + +impl pallet_balances::Config for TestRuntime { + type AccountStore = System; + type Balance = Balance; + type DustRemoval = (); + type ExistentialDeposit = ConstU128; + type FreezeIdentifier = RuntimeFreezeReason; + type MaxFreezes = ConstU32<50>; + type MaxHolds = ConstU32<50>; + type MaxLocks = ConstU32<50>; + type MaxReserves = ConstU32<50>; + type ReserveIdentifier = [u8; 8]; + type RuntimeEvent = RuntimeEvent; + type RuntimeHoldReason = RuntimeHoldReason; + type WeightInfo = (); +} + +impl DeriveDidCallAuthorizationVerificationKeyRelationship for RuntimeCall { + fn derive_verification_key_relationship(&self) -> did::DeriveDidCallKeyRelationshipResult { + Ok(did::DidVerificationKeyRelationship::Authentication) + } + + #[cfg(feature = "runtime-benchmarks")] + fn get_call_for_did_call_benchmark() -> Self { + RuntimeCall::System(frame_system::Call::remark { remark: sp_std::vec![] }) + } +} + +impl did::Config for TestRuntime { + type BalanceMigrationManager = (); + type BaseDeposit = ConstU128; + type Currency = Balances; + type DidIdentifier = DidIdentifier; + type EnsureOrigin = EnsureSigned; + type Fee = ConstU128; + type FeeCollector = (); + type KeyDeposit = ConstU128; + type MaxBlocksTxValidity = ConstU64<10>; + type MaxNewKeyAgreementKeys = MaxNewKeyAgreementKeys; + type MaxNumberOfServicesPerDid = MaxNumberOfServicesPerDid; + type MaxNumberOfTypesPerService = MaxNumberOfTypesPerService; + type MaxNumberOfUrlsPerService = MaxNumberOfUrlsPerService; + type MaxPublicKeysPerDid = MaxPublicKeysPerDid; + type MaxServiceIdLength = MaxServiceIdLength; + type MaxServiceTypeLength = MaxServiceTypeLength; + type MaxServiceUrlLength = MaxServiceUrlLength; + type MaxTotalKeyAgreementKeys = MaxTotalKeyAgreementKeys; + type OriginSuccess = AccountId; + type RuntimeCall = RuntimeCall; + type RuntimeEvent = RuntimeEvent; + type RuntimeHoldReason = RuntimeHoldReason; + type RuntimeOrigin = RuntimeOrigin; + type ServiceEndpointDeposit = ConstU128; + type WeightInfo = (); +} + +impl pallet_web3_names::Config for TestRuntime { + type BalanceMigrationManager = (); + type BanOrigin = EnsureRoot; + type Currency = Balances; + type Deposit = ConstU128; + type MaxNameLength = MaxNameLength; + type MinNameLength = MinNameLength; + type OriginSuccess = AccountId; + type OwnerOrigin = EnsureSigned; + type RuntimeEvent = RuntimeEvent; + type RuntimeHoldReason = RuntimeHoldReason; + type Web3Name = AsciiWeb3Name; + type Web3NameOwner = DidIdentifier; + type WeightInfo = (); +} + +impl pallet_did_lookup::Config for TestRuntime { + type BalanceMigrationManager = (); + type Currency = Balances; + type Deposit = ConstU128; + type DidIdentifier = DidIdentifier; + type EnsureOrigin = EnsureSigned; + type OriginSuccess = AccountId; + type RuntimeEvent = RuntimeEvent; + type RuntimeHoldReason = RuntimeHoldReason; + type WeightInfo = (); +} + +impl pallet_dip_provider::Config for TestRuntime { + type CommitOrigin = AccountId; + type CommitOriginCheck = EnsureSigned; + type Identifier = DidIdentifier; + type IdentityCommitmentGenerator = DidMerkleRootGenerator; + type IdentityProvider = LinkedDidInfoProvider; + type ProviderHooks = (); + type RuntimeEvent = RuntimeEvent; + type WeightInfo = (); +} + +pub(crate) const ACCOUNT: AccountId = AccountId::new([100u8; 32]); +pub(crate) const DID_IDENTIFIER: DidIdentifier = DidIdentifier::new([150u8; 32]); +pub(crate) const SUBMITTER: AccountId = AccountId::new([150u8; 32]); + +pub(crate) fn create_linked_info( + auth_key: DidVerificationKey, + web3_name: Option>, + linked_accounts: u32, +) -> LinkedDidInfoOf { + let did_details = { + let mut details = generate_base_did_details(auth_key.clone(), Some(SUBMITTER)); + let att_key = DidVerificationKey::Sr25519(sr25519::Public(auth_key.blake2_256())); + let del_key = DidVerificationKey::Account(SUBMITTER); + details + .update_attestation_key(att_key, BlockNumberFor::::default()) + .expect("Should not fail to add attestation key to DID."); + details + .update_delegation_key(del_key, BlockNumberFor::::default()) + .expect("Should not fail to add delegation key to DID."); + (0..MAX_KEY_AGREEMENT_KEYS).for_each(|i| { + let bytes = i.to_be_bytes(); + let mut buffer = <[u8; 32]>::default(); + buffer[..4].copy_from_slice(&bytes); + let key_agreement_key = DidEncryptionKey::X25519(buffer); + details + .add_key_agreement_key(key_agreement_key, BlockNumberFor::::default()) + .expect("Should not fail to add key agreement key to DID."); + }); + details + }; + let web3_name = if let Some(web3_name) = web3_name { + let claimed_at = BlockNumberFor::::default(); + Some(RevealedWeb3Name { + web3_name: web3_name.as_ref().to_vec().try_into().unwrap(), + claimed_at, + }) + } else { + None + }; + let linked_accounts_iter = (0..linked_accounts).map(|i| { + let bytes = i.to_be_bytes(); + if i % 2 == 0 { + let mut buffer = <[u8; 20]>::default(); + buffer[..4].copy_from_slice(&bytes); + LinkableAccountId::AccountId20(AccountId20(buffer)) + } else { + let mut buffer = <[u8; 32]>::default(); + buffer[..4].copy_from_slice(&bytes); + LinkableAccountId::AccountId32(AccountId32::new(buffer)) + } + }); + let linked_accounts: BoundedVec> = + linked_accounts_iter + .clone() + .collect::>() + .try_into() + .unwrap_or_else(|_| { + panic!("Cannot cast generated vector of linked accounts with length {} to BoundedVec with max limit of {}.", + linked_accounts_iter.count(), + MAX_LINKED_ACCOUNTS) + }); + LinkedDidInfoOf { + did_details, + web3_name_details: web3_name, + linked_accounts, + } +} + +#[derive(Default)] +pub(crate) struct ExtBuilder( + #[allow(clippy::type_complexity)] + Vec<( + DidIdentifier, + DidDetails, + Option>, + Vec, + AccountId, + )>, + Vec, +); + +impl ExtBuilder { + #[allow(clippy::type_complexity)] + pub(crate) fn with_dids( + mut self, + dids: Vec<( + DidIdentifier, + DidDetails, + Option>, + Vec, + AccountId, + )>, + ) -> Self { + self.0 = dids; + self + } + + pub(crate) fn with_deleted_dids(mut self, dids: Vec) -> Self { + self.1 = dids; + self + } + pub(crate) fn build(self) -> sp_io::TestExternalities { + let mut ext = sp_io::TestExternalities::default(); + + ext.execute_with(|| { + for (did_identifier, did_details, web3_name, linked_accounts, submitter) in self.0 { + Balances::make_free_balance_be(&submitter, 100_000 * KILT); + Did::try_insert_did(did_identifier.clone(), did_details, submitter.clone()) + .unwrap_or_else(|_| panic!("Failed to insert DID {:#?}.", did_identifier)); + if let Some(name) = web3_name { + Web3Names::register_name(name.clone(), did_identifier.clone(), submitter.clone()) + .unwrap_or_else(|_| panic!("Failed to insert web3name{:#?}.", name)); + } + for linked_account in linked_accounts { + DidLookup::add_association(submitter.clone(), did_identifier.clone(), linked_account.clone()) + .unwrap_or_else(|_| panic!("Failed to insert linked account{:#?}.", linked_account)); + } + } + + for did_identifier in self.1 { + Balances::make_free_balance_be(&SUBMITTER, 100_000 * KILT); + // Ignore error if the DID already exists + let _ = Did::try_insert_did( + did_identifier.clone(), + did::mock_utils::generate_base_did_details(DidVerificationKey::Account(ACCOUNT), Some(SUBMITTER)), + SUBMITTER, + ); + did::Pallet::::delete_did(did_identifier, 0) + .expect("Should not fail to mark DID as deleted."); + } + }); + + ext + } +} diff --git a/runtimes/common/src/dip/mod.rs b/runtimes/common/src/dip/mod.rs index 493b54cc0..65ccc0af1 100644 --- a/runtimes/common/src/dip/mod.rs +++ b/runtimes/common/src/dip/mod.rs @@ -18,7 +18,12 @@ #![doc = include_str!("./README.md")] +/// Logic for deposit-related functionalities. +pub mod deposit; /// Logic for collecting information related to a KILT DID. pub mod did; /// Logic for generating Merkle commitments of a KILT DID identity. pub mod merkle; + +#[cfg(test)] +mod mock; diff --git a/runtimes/peregrine/src/dip/mod.rs b/runtimes/peregrine/src/dip/mod.rs index 22d00a44e..4c1b22a5c 100644 --- a/runtimes/peregrine/src/dip/mod.rs +++ b/runtimes/peregrine/src/dip/mod.rs @@ -20,17 +20,17 @@ use did::{DidRawOrigin, EnsureDidOrigin}; use frame_system::EnsureSigned; use runtime_common::{ constants::{deposit_storage::MAX_DEPOSIT_PALLET_KEY_LENGTH, dip_provider::MAX_LINKED_ACCOUNTS}, - dip::{did::LinkedDidInfoProvider, merkle::DidMerkleRootGenerator}, + dip::{ + deposit::{DepositCollectorHooks, DepositHooks, DepositNamespace}, + did::LinkedDidInfoProvider, + merkle::DidMerkleRootGenerator, + }, AccountId, DidIdentifier, }; use sp_core::ConstU32; -use crate::{ - dip::deposit::{DepositCollectorHooks, DepositHooks, DepositNamespace}, - weights, Balances, Runtime, RuntimeEvent, RuntimeHoldReason, -}; +use crate::{weights, Balances, Runtime, RuntimeEvent, RuntimeHoldReason}; -pub(crate) mod deposit; pub(crate) mod runtime_api; impl pallet_dip_provider::Config for Runtime { @@ -52,7 +52,7 @@ impl pallet_dip_provider::Config for Runtime { impl pallet_deposit_storage::Config for Runtime { #[cfg(feature = "runtime-benchmarks")] - type BenchmarkHooks = deposit::PalletDepositStorageBenchmarkHooks; + type BenchmarkHooks = runtime_common::dip::deposit::PalletDepositStorageBenchmarkHooks; // Any signed origin can submit the tx, which will go through only if the // deposit payer matches the signed origin. type CheckOrigin = EnsureSigned; diff --git a/runtimes/peregrine/src/tests.rs b/runtimes/peregrine/src/tests.rs index 3a96b030c..2a416ceba 100644 --- a/runtimes/peregrine/src/tests.rs +++ b/runtimes/peregrine/src/tests.rs @@ -35,11 +35,10 @@ use runtime_common::{ web3_names::MAX_NAME_BYTE_LENGTH, MAX_INDICES_BYTE_LENGTH, }, + dip::deposit::DepositKey, AccountId, BlockNumber, }; -use crate::dip::deposit::DepositKey; - use super::{Runtime, RuntimeCall}; #[test]