diff --git a/Cargo.lock b/Cargo.lock index d44078959c..865b768a5f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6917,6 +6917,7 @@ dependencies = [ "frame-benchmarking", "frame-support", "frame-system", + "kilt-support", "log", "pallet-balances", "parity-scale-codec", @@ -10939,6 +10940,7 @@ dependencies = [ "cumulus-primitives-core", "did", "enum-iterator", + "env_logger 0.10.2", "frame-benchmarking", "frame-support", "frame-system", diff --git a/dip-template/runtimes/dip-provider/src/lib.rs b/dip-template/runtimes/dip-provider/src/lib.rs index c471152d64..15e2596d1d 100644 --- a/dip-template/runtimes/dip-provider/src/lib.rs +++ b/dip-template/runtimes/dip-provider/src/lib.rs @@ -401,6 +401,7 @@ impl did::Config for Runtime { type BaseDeposit = ConstU128; type Currency = Balances; type DidIdentifier = DidIdentifier; + type DidLifecycleHooks = (); type EnsureOrigin = EnsureDidOrigin; type Fee = ConstU128; type FeeCollector = (); diff --git a/pallets/did/src/lib.rs b/pallets/did/src/lib.rs index 39ec7a5054..913847d719 100644 --- a/pallets/did/src/lib.rs +++ b/pallets/did/src/lib.rs @@ -88,6 +88,7 @@ pub mod errors; pub mod migrations; pub mod origin; pub mod service_endpoints; +pub mod traits; #[cfg(test)] mod mock; @@ -171,6 +172,7 @@ pub mod pallet { DidEncryptionKey, DidSignature, DidVerifiableIdentifier, DidVerificationKey, RelationshipDeriveError, }, service_endpoints::{utils as service_endpoints_utils, ServiceEndpointId}, + traits::{DidDeletionHook, DidLifecycleHooks}, }; /// The current storage version. @@ -328,6 +330,10 @@ pub mod pallet { /// Migration manager to handle new created entries type BalanceMigrationManager: BalanceMigrationManager, BalanceOf>; + + /// Runtime-injected logic to be called at each stage of a DID's + /// lifecycle. + type DidLifecycleHooks: DidLifecycleHooks; } #[pallet::pallet] @@ -455,6 +461,9 @@ pub mod pallet { /// The number of service endpoints stored under the DID is larger than /// the number of endpoints to delete. MaxStoredEndpointsCountExceeded, + /// The DID cannot be deleted because the runtime logic returned an + /// error. + CannotDelete, /// An error that is not supposed to take place, yet it happened. Internal, } @@ -972,7 +981,10 @@ pub mod pallet { /// - Kills: Did entry associated to the DID identifier /// # #[pallet::call_index(10)] - #[pallet::weight(::WeightInfo::delete(*endpoints_to_remove))] + #[pallet::weight({ + let max_hook_weight = <>::DeletionHook as DidDeletionHook>::MAX_WEIGHT; + ::WeightInfo::delete(*endpoints_to_remove).saturating_add(max_hook_weight) + })] pub fn delete(origin: OriginFor, endpoints_to_remove: u32) -> DispatchResult { let source = T::EnsureOrigin::ensure_origin(origin)?; let did_subject = source.subject(); @@ -1002,7 +1014,10 @@ pub mod pallet { /// - Kills: Did entry associated to the DID identifier /// # #[pallet::call_index(11)] - #[pallet::weight(::WeightInfo::reclaim_deposit(*endpoints_to_remove))] + #[pallet::weight({ + let max_hook_weight = <>::DeletionHook as DidDeletionHook>::MAX_WEIGHT; + ::WeightInfo::reclaim_deposit(*endpoints_to_remove).saturating_add(max_hook_weight) + })] pub fn reclaim_deposit( origin: OriginFor, did_subject: DidIdentifierOf, @@ -1512,6 +1527,17 @@ pub mod pallet { // `take` calls `kill` internally let did_entry = Did::::take(&did_subject).ok_or(Error::::NotFound)?; + // Make sure this check happens after the line where we check if a DID exists, + // else we would start getting `CannotDelete` errors when we should be getting + // `NotFound`. + ensure!( + <>::DeletionHook as DidDeletionHook>::can_delete( + &did_subject, + ) + .is_ok(), + Error::::CannotDelete + ); + DidEndpointsCount::::remove(&did_subject); let is_key_migrated = diff --git a/pallets/did/src/mock.rs b/pallets/did/src/mock.rs index db59b5f3fa..dc6d421ac9 100644 --- a/pallets/did/src/mock.rs +++ b/pallets/did/src/mock.rs @@ -175,6 +175,7 @@ impl Config for Test { type MaxNumberOfTypesPerService = MaxNumberOfTypesPerService; type MaxNumberOfUrlsPerService = MaxNumberOfUrlsPerService; type BalanceMigrationManager = (); + type DidLifecycleHooks = (); } parameter_types! { diff --git a/pallets/did/src/traits/lifecycle_hooks/deletion/mod.rs b/pallets/did/src/traits/lifecycle_hooks/deletion/mod.rs new file mode 100644 index 0000000000..ef91931d65 --- /dev/null +++ b/pallets/did/src/traits/lifecycle_hooks/deletion/mod.rs @@ -0,0 +1,76 @@ +// 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 + +#[cfg(test)] +mod tests; + +use sp_std::marker::PhantomData; +use sp_weights::Weight; + +use crate::{Config, DidIdentifierOf}; + +/// Runtime logic evaluated by the DID pallet upon deleting an existing DID. +pub trait DidDeletionHook +where + T: Config, +{ + /// The statically computed maximum weight the implementation will consume + /// to verify if a DID can be deleted. + const MAX_WEIGHT: Weight; + + /// Return whether the DID can be deleted (`Ok(())`), or not. In case of + /// error, the consumed weight (less than or equal to `MAX_WEIGHT`) is + /// returned. + fn can_delete(did: &DidIdentifierOf) -> Result<(), Weight>; +} + +impl DidDeletionHook for () +where + T: Config, +{ + const MAX_WEIGHT: Weight = Weight::from_parts(0, 0); + + fn can_delete(_did: &DidIdentifierOf) -> Result<(), Weight> { + Ok(()) + } +} + +/// Implementation of [`DidDeletionHook`] that iterates over both +/// components, bailing out early if the first one fails. The `MAX_WEIGHT` is +/// the sum of both components. +pub struct RequireBoth(PhantomData<(A, B)>); + +impl DidDeletionHook for RequireBoth +where + T: Config, + A: DidDeletionHook, + B: DidDeletionHook, +{ + const MAX_WEIGHT: Weight = A::MAX_WEIGHT.saturating_add(B::MAX_WEIGHT); + + /// In case of failure, the returned weight is either the weight consumed by + /// the first component, or the sum of the first component's maximum weight + /// and the weight consumed by the second component. + fn can_delete(did: &DidIdentifierOf) -> Result<(), Weight> { + // Bail out early with A's weight if A fails. + A::can_delete(did)?; + // Bail out early with A's max weight + B's if B fails. + B::can_delete(did).map_err(|consumed_weight| A::MAX_WEIGHT.saturating_add(consumed_weight))?; + Ok(()) + } +} diff --git a/pallets/did/src/traits/lifecycle_hooks/deletion/tests.rs b/pallets/did/src/traits/lifecycle_hooks/deletion/tests.rs new file mode 100644 index 0000000000..067a8a5403 --- /dev/null +++ b/pallets/did/src/traits/lifecycle_hooks/deletion/tests.rs @@ -0,0 +1,84 @@ +// 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 + +//! Test module for the `RequireBoth` type. It verifies that the type works as +//! expected in case of failure of one of its components. + +use sp_runtime::AccountId32; +use sp_weights::Weight; + +use crate::{ + traits::lifecycle_hooks::{deletion::RequireBoth, mock::TestRuntime, DidDeletionHook}, + DidIdentifierOf, +}; + +struct AlwaysDeny; + +impl DidDeletionHook for AlwaysDeny { + const MAX_WEIGHT: Weight = Weight::from_all(10); + + fn can_delete(_did: &DidIdentifierOf) -> Result<(), Weight> { + Err(Weight::from_all(5)) + } +} + +struct AlwaysAllow; + +impl DidDeletionHook for AlwaysAllow { + const MAX_WEIGHT: Weight = Weight::from_all(20); + + fn can_delete(_did: &DidIdentifierOf) -> Result<(), Weight> { + Ok(()) + } +} + +#[test] +fn first_false() { + type TestSubject = RequireBoth; + + // Max weight is the sum. + assert_eq!(TestSubject::MAX_WEIGHT, Weight::from_all(30)); + // Failure consumes `False`'s weight. + assert_eq!( + TestSubject::can_delete(&AccountId32::new([0u8; 32])), + Err(Weight::from_all(5)) + ); +} + +#[test] +fn second_false() { + type TestSubject = RequireBoth; + + // Max weight is the sum. + assert_eq!(TestSubject::MAX_WEIGHT, Weight::from_all(30)); + // Failure consumes the sum of `True`'s max weight and `False`'s weight. + assert_eq!( + TestSubject::can_delete(&AccountId32::new([0u8; 32])), + Err(Weight::from_all(25)) + ); +} + +#[test] +fn both_true() { + type TestSubject = RequireBoth; + + // Max weight is the sum. + assert_eq!(TestSubject::MAX_WEIGHT, Weight::from_all(40)); + // Overall result is `Ok`. + assert_eq!(TestSubject::can_delete(&AccountId32::new([0u8; 32])), Ok(())); +} diff --git a/pallets/did/src/traits/lifecycle_hooks/mock.rs b/pallets/did/src/traits/lifecycle_hooks/mock.rs new file mode 100644 index 0000000000..73a2761c84 --- /dev/null +++ b/pallets/did/src/traits/lifecycle_hooks/mock.rs @@ -0,0 +1,117 @@ +// 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, parameter_types}; +use frame_system::{mocking::MockBlock, EnsureSigned}; +use kilt_support::mock::MockCurrency; +use parity_scale_codec::{Decode, Encode}; +use scale_info::TypeInfo; +use sp_core::{ConstU32, ConstU64, H256}; +use sp_runtime::{ + traits::{BlakeTwo256, IdentityLookup}, + AccountId32, +}; + +use crate::{ + Config, DeriveDidCallAuthorizationVerificationKeyRelationship, DeriveDidCallKeyRelationshipResult, + DidVerificationKeyRelationship, +}; + +construct_runtime!( + pub enum TestRuntime + { + System: frame_system, + Did: crate, + } +); + +impl frame_system::Config for TestRuntime { + type AccountData = (); + type AccountId = AccountId32; + type BaseCallFilter = (); + type Block = MockBlock; + type BlockHashCount = ConstU64<1>; + type BlockLength = (); + type BlockWeights = (); + type DbWeight = (); + type Hash = H256; + type Hashing = BlakeTwo256; + type Lookup = IdentityLookup; + type MaxConsumers = ConstU32<1>; + type Nonce = u64; + type OnKilledAccount = (); + type OnNewAccount = (); + type OnSetCode = (); + type PalletInfo = PalletInfo; + type RuntimeEvent = (); + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + type RuntimeTask = (); + type SS58Prefix = (); + type SystemWeightInfo = (); + type Version = (); +} + +parameter_types! { + #[derive(TypeInfo, Debug, PartialEq, Eq, Clone, Encode, Decode)] + pub const MaxNewKeyAgreementKeys: u32 = 1; + #[derive(TypeInfo, Debug, PartialEq, Eq, Clone, Encode, Decode)] + pub const MaxTotalKeyAgreementKeys: u32 = 1; +} + +impl DeriveDidCallAuthorizationVerificationKeyRelationship for RuntimeCall { + fn derive_verification_key_relationship(&self) -> DeriveDidCallKeyRelationshipResult { + Ok(DidVerificationKeyRelationship::Authentication) + } + + #[cfg(feature = "runtime-benchmarks")] + fn get_call_for_did_call_benchmark() -> Self { + Self::System(frame_system::Call::remark { + remark: b"test".to_vec(), + }) + } +} + +impl Config for TestRuntime { + type BalanceMigrationManager = (); + type BaseDeposit = ConstU64<1>; + type Currency = MockCurrency; + type DidIdentifier = AccountId32; + type DidLifecycleHooks = (); + type EnsureOrigin = EnsureSigned; + type Fee = ConstU64<1>; + type FeeCollector = (); + type KeyDeposit = ConstU64<1>; + type MaxBlocksTxValidity = ConstU64<1>; + type MaxNewKeyAgreementKeys = MaxNewKeyAgreementKeys; + type MaxNumberOfServicesPerDid = ConstU32<1>; + type MaxNumberOfTypesPerService = ConstU32<1>; + type MaxNumberOfUrlsPerService = ConstU32<1>; + type MaxPublicKeysPerDid = ConstU32<1>; + type MaxServiceIdLength = ConstU32<1>; + type MaxServiceTypeLength = ConstU32<1>; + type MaxServiceUrlLength = ConstU32<1>; + type MaxTotalKeyAgreementKeys = MaxTotalKeyAgreementKeys; + type OriginSuccess = AccountId32; + type RuntimeCall = RuntimeCall; + type RuntimeEvent = (); + type RuntimeHoldReason = RuntimeHoldReason; + type RuntimeOrigin = RuntimeOrigin; + type ServiceEndpointDeposit = ConstU64<1>; + type WeightInfo = (); +} diff --git a/pallets/did/src/traits/lifecycle_hooks/mod.rs b/pallets/did/src/traits/lifecycle_hooks/mod.rs new file mode 100644 index 0000000000..d6da3547ac --- /dev/null +++ b/pallets/did/src/traits/lifecycle_hooks/mod.rs @@ -0,0 +1,43 @@ +// 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 + +pub mod deletion; +pub use deletion::DidDeletionHook; + +// We need this mock since the trait requires the implementation of the did's +// `Config` trait. +#[cfg(test)] +mod mock; + +use crate::Config; + +/// A collection of hooks invoked during DID operations. +pub trait DidLifecycleHooks +where + T: Config, +{ + /// Hook called when a DID deletion is requested by an authorized entity. + type DeletionHook: DidDeletionHook; +} + +impl DidLifecycleHooks for () +where + T: Config, +{ + type DeletionHook = (); +} diff --git a/pallets/did/src/traits/mod.rs b/pallets/did/src/traits/mod.rs new file mode 100644 index 0000000000..552b58bab6 --- /dev/null +++ b/pallets/did/src/traits/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 lifecycle_hooks; +pub use lifecycle_hooks::{deletion, DidDeletionHook, DidLifecycleHooks}; diff --git a/pallets/pallet-asset-switch/Cargo.toml b/pallets/pallet-asset-switch/Cargo.toml index ace29bacf2..b0a2e8501d 100644 --- a/pallets/pallet-asset-switch/Cargo.toml +++ b/pallets/pallet-asset-switch/Cargo.toml @@ -15,6 +15,7 @@ targets = ["x86_64-unknown-linux-gnu"] [dev-dependencies] env_logger = { workspace = true } +kilt-support = { workspace = true, features = ["std"] } pallet-balances = { workspace = true, features = ["std"] } sp-keystore = { workspace = true } diff --git a/pallets/pallet-asset-switch/src/xcm/trade/xcm_fee_asset/mock.rs b/pallets/pallet-asset-switch/src/xcm/trade/xcm_fee_asset/mock.rs index 3054b0bf52..82b596b63d 100644 --- a/pallets/pallet-asset-switch/src/xcm/trade/xcm_fee_asset/mock.rs +++ b/pallets/pallet-asset-switch/src/xcm/trade/xcm_fee_asset/mock.rs @@ -16,22 +16,13 @@ // If you feel like getting in touch with us, you can do so at info@botlabs.org -use frame_support::{ - construct_runtime, parameter_types, - traits::{ - fungible::Dust, - tokens::{ - fungible::{Inspect as InspectFungible, Mutate as MutateFungible, Unbalanced as UnbalancedFungible}, - DepositConsequence, Fortitude, Preservation, Provenance, WithdrawConsequence, - }, - Everything, - }, -}; +use frame_support::{construct_runtime, parameter_types, traits::Everything}; use frame_system::{mocking::MockBlock, EnsureRoot, EnsureSigned}; +use kilt_support::mock::MockCurrency; use sp_core::{ConstU16, ConstU32, ConstU64, H256}; use sp_runtime::{ traits::{BlakeTwo256, IdentityLookup}, - AccountId32, DispatchError, + AccountId32, }; use xcm::v4::{InteriorLocation, Junctions::Here}; @@ -71,58 +62,6 @@ impl frame_system::Config for MockRuntime { type Version = (); } -// Currency is not used in this XCM component tests, so we mock the entire -// currency system. -pub struct MockCurrency; - -impl MutateFungible for MockCurrency {} - -impl InspectFungible for MockCurrency { - type Balance = u64; - - fn active_issuance() -> Self::Balance { - Self::Balance::default() - } - - fn balance(_who: &AccountId32) -> Self::Balance { - Self::Balance::default() - } - - fn can_deposit(_who: &AccountId32, _amount: Self::Balance, _provenance: Provenance) -> DepositConsequence { - DepositConsequence::Success - } - - fn can_withdraw(_who: &AccountId32, _amount: Self::Balance) -> WithdrawConsequence { - WithdrawConsequence::Success - } - - fn minimum_balance() -> Self::Balance { - Self::Balance::default() - } - - fn reducible_balance(_who: &AccountId32, _preservation: Preservation, _force: Fortitude) -> Self::Balance { - Self::Balance::default() - } - - fn total_balance(_who: &AccountId32) -> Self::Balance { - Self::Balance::default() - } - - fn total_issuance() -> Self::Balance { - Self::Balance::default() - } -} - -impl UnbalancedFungible for MockCurrency { - fn handle_dust(_dust: Dust) {} - - fn write_balance(_who: &AccountId32, _amount: Self::Balance) -> Result, DispatchError> { - Ok(Some(Self::Balance::default())) - } - - fn set_total_issuance(_amount: Self::Balance) {} -} - parameter_types! { pub const UniversalLocation: InteriorLocation = Here; } @@ -131,7 +70,7 @@ impl crate::Config for MockRuntime { type AccountIdConverter = (); type AssetTransactor = (); type FeeOrigin = EnsureRoot; - type LocalCurrency = MockCurrency; + type LocalCurrency = MockCurrency; type PauseOrigin = EnsureRoot; type RuntimeEvent = RuntimeEvent; type SubmitterOrigin = EnsureSigned; diff --git a/pallets/pallet-asset-switch/src/xcm/transfer/mock.rs b/pallets/pallet-asset-switch/src/xcm/transfer/mock.rs index 22101e6882..87d9b933e0 100644 --- a/pallets/pallet-asset-switch/src/xcm/transfer/mock.rs +++ b/pallets/pallet-asset-switch/src/xcm/transfer/mock.rs @@ -16,20 +16,14 @@ // If you feel like getting in touch with us, you can do so at info@botlabs.org -use frame_support::{ - construct_runtime, parameter_types, - traits::{ - fungible::{Dust, Inspect as InspectFungible, Mutate as MutateFungible, Unbalanced as UnbalancedFungible}, - tokens::{DepositConsequence, Fortitude, Preservation, Provenance, WithdrawConsequence}, - Everything, - }, -}; +use frame_support::{construct_runtime, parameter_types, traits::Everything}; use frame_system::{mocking::MockBlock, EnsureRoot, EnsureSigned}; +use kilt_support::mock::MockCurrency; use pallet_balances::AccountData; use sp_core::{ConstU16, ConstU32, ConstU64, H256}; use sp_runtime::{ traits::{BlakeTwo256, IdentityLookup}, - AccountId32, DispatchError, + AccountId32, }; use xcm::v4::{InteriorLocation, Junctions::Here}; @@ -69,58 +63,6 @@ impl frame_system::Config for MockRuntime { type Version = (); } -// Currency is not used in this XCM component tests, so we mock the entire -// currency system. -pub struct MockCurrency; - -impl MutateFungible for MockCurrency {} - -impl InspectFungible for MockCurrency { - type Balance = u64; - - fn active_issuance() -> Self::Balance { - Self::Balance::default() - } - - fn balance(_who: &AccountId32) -> Self::Balance { - Self::Balance::default() - } - - fn can_deposit(_who: &AccountId32, _amount: Self::Balance, _provenance: Provenance) -> DepositConsequence { - DepositConsequence::Success - } - - fn can_withdraw(_who: &AccountId32, _amount: Self::Balance) -> WithdrawConsequence { - WithdrawConsequence::Success - } - - fn minimum_balance() -> Self::Balance { - Self::Balance::default() - } - - fn reducible_balance(_who: &AccountId32, _preservation: Preservation, _force: Fortitude) -> Self::Balance { - Self::Balance::default() - } - - fn total_balance(_who: &AccountId32) -> Self::Balance { - Self::Balance::default() - } - - fn total_issuance() -> Self::Balance { - Self::Balance::default() - } -} - -impl UnbalancedFungible for MockCurrency { - fn handle_dust(_dust: Dust) {} - - fn write_balance(_who: &AccountId32, _amount: Self::Balance) -> Result, DispatchError> { - Ok(Some(Self::Balance::default())) - } - - fn set_total_issuance(_amount: Self::Balance) {} -} - parameter_types! { pub const UniversalLocation: InteriorLocation = Here; } @@ -129,7 +71,7 @@ impl crate::Config for MockRuntime { type AccountIdConverter = (); type AssetTransactor = (); type FeeOrigin = EnsureRoot; - type LocalCurrency = MockCurrency; + type LocalCurrency = MockCurrency; type PauseOrigin = EnsureRoot; type RuntimeEvent = RuntimeEvent; type SubmitterOrigin = EnsureSigned; diff --git a/pallets/pallet-migration/src/mock.rs b/pallets/pallet-migration/src/mock.rs index 7e658653be..28bad00579 100644 --- a/pallets/pallet-migration/src/mock.rs +++ b/pallets/pallet-migration/src/mock.rs @@ -272,6 +272,7 @@ impl did::Config for Test { type MaxNumberOfTypesPerService = MaxNumberOfTypesPerService; type MaxNumberOfUrlsPerService = MaxNumberOfUrlsPerService; type BalanceMigrationManager = Migration; + type DidLifecycleHooks = (); } parameter_types! { diff --git a/runtimes/common/Cargo.toml b/runtimes/common/Cargo.toml index d31a31f0c3..c3865c2802 100644 --- a/runtimes/common/Cargo.toml +++ b/runtimes/common/Cargo.toml @@ -13,7 +13,9 @@ version = { workspace = true } [dev-dependencies] did = { workspace = true, features = ["mock", "std"] } enum-iterator = { workspace = true } +env_logger = { workspace = true } kilt-dip-primitives = { workspace = true, features = ["std"] } +kilt-support = { workspace = true, features = ["mock", "std"] } sp-io = { workspace = true, features = ["std"] } [dependencies] diff --git a/runtimes/common/src/did/deletion_hooks/mod.rs b/runtimes/common/src/did/deletion_hooks/mod.rs new file mode 100644 index 0000000000..d6ac948c9e --- /dev/null +++ b/runtimes/common/src/did/deletion_hooks/mod.rs @@ -0,0 +1,88 @@ +// 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 + +#[cfg(test)] +mod tests; + +use did::DidIdentifierOf; +use sp_std::marker::PhantomData; + +use sp_weights::Weight; + +/// Implementation of the [`did::traits::DidDeletionHook`] trait that makes sure +/// there is no resource linked to the DID in the provided web3name pallet +/// deployment. +/// +/// The returned worst-case weight is the same weight provided to this type. +pub struct EnsureNoLinkedWeb3NameDeletionHook< + const READ_WEIGHT_TIME: u64, + const READ_WEIGHT_SIZE: u64, + Web3NameDeployment, +>(PhantomData); + +impl did::traits::DidDeletionHook + for EnsureNoLinkedWeb3NameDeletionHook +where + T: did::Config + pallet_web3_names::Config>, + Web3NameDeployment: 'static, +{ + const MAX_WEIGHT: Weight = Weight::from_parts(READ_WEIGHT_TIME, READ_WEIGHT_SIZE); + + fn can_delete(did: &did::DidIdentifierOf) -> Result<(), Weight> { + if !pallet_web3_names::Names::::contains_key(did) { + Ok(()) + } else { + Err(>::MAX_WEIGHT) + } + } +} + +/// Implementation of the [`did::traits::DidDeletionHook`] trait that makes sure +/// there is no resource linked to the DID in the provided did_lookup pallet +/// deployment. +/// +/// The returned worst-case weight is the same weight provided to this type. +pub struct EnsureNoLinkedAccountDeletionHook< + const READ_WEIGHT_TIME: u64, + const READ_WEIGHT_SIZE: u64, + AccountLinkingDeployment, +>(PhantomData); + +impl + did::traits::DidDeletionHook + for EnsureNoLinkedAccountDeletionHook +where + T: did::Config + pallet_did_lookup::Config>, + AccountLinkingDeployment: 'static, +{ + const MAX_WEIGHT: Weight = Weight::from_parts(READ_WEIGHT_TIME, READ_WEIGHT_SIZE); + + fn can_delete(did: &did::DidIdentifierOf) -> Result<(), Weight> { + // We check whether the prefix iterator for the given DID has at least one + // element (`next == Some`). + let is_any_account_linked = + pallet_did_lookup::ConnectedAccounts::::iter_key_prefix(did) + .next() + .is_some(); + if !is_any_account_linked { + Ok(()) + } else { + Err(>::MAX_WEIGHT) + } + } +} diff --git a/runtimes/common/src/did/deletion_hooks/tests/mock.rs b/runtimes/common/src/did/deletion_hooks/tests/mock.rs new file mode 100644 index 0000000000..fa2e63526f --- /dev/null +++ b/runtimes/common/src/did/deletion_hooks/tests/mock.rs @@ -0,0 +1,231 @@ +// 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, traits::deletion::RequireBoth, DidVerificationKeyRelationship}; +use frame_support::{construct_runtime, parameter_types}; +use frame_system::{mocking::MockBlock, EnsureRoot, EnsureSigned, RawOrigin}; +use kilt_support::mock::MockCurrency; +use parity_scale_codec::{Decode, Encode}; +use scale_info::TypeInfo; +use sp_core::{ConstBool, ConstU32, ConstU64, H256}; +use sp_io::TestExternalities; +use sp_runtime::{ + traits::{BlakeTwo256, IdentityLookup}, + AccountId32, +}; + +use crate::{EnsureNoLinkedAccountDeletionHook, EnsureNoLinkedWeb3NameDeletionHook}; + +construct_runtime!( + pub enum TestRuntime + { + System: frame_system, + Did: did, + Web3Names: pallet_web3_names, + LinkedAccounts: pallet_did_lookup, + } +); + +impl frame_system::Config for TestRuntime { + type AccountData = (); + type AccountId = AccountId32; + type BaseCallFilter = (); + type Block = MockBlock; + type BlockHashCount = ConstU64<1>; + type BlockLength = (); + type BlockWeights = (); + type DbWeight = (); + type Hash = H256; + type Hashing = BlakeTwo256; + type Lookup = IdentityLookup; + type MaxConsumers = ConstU32<1>; + type Nonce = u64; + type OnKilledAccount = (); + type OnNewAccount = (); + type OnSetCode = (); + type PalletInfo = PalletInfo; + type RuntimeEvent = (); + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + type RuntimeTask = (); + type SS58Prefix = (); + type SystemWeightInfo = (); + type Version = (); +} + +parameter_types! { + #[derive(TypeInfo, Debug, PartialEq, Eq, Clone, Encode, Decode)] + pub const MaxNewKeyAgreementKeys: u32 = 1; + #[derive(TypeInfo, Debug, PartialEq, Eq, Clone, Encode, Decode)] + pub const MaxTotalKeyAgreementKeys: u32 = 1; +} + +impl did::DeriveDidCallAuthorizationVerificationKeyRelationship for RuntimeCall { + fn derive_verification_key_relationship(&self) -> did::DeriveDidCallKeyRelationshipResult { + Ok(DidVerificationKeyRelationship::Authentication) + } + + #[cfg(feature = "runtime-benchmarks")] + fn get_call_for_did_call_benchmark() -> Self { + Self::System(frame_system::Call::remark { + remark: b"test".to_vec(), + }) + } +} + +pub(super) const DID: AccountId32 = AccountId32::new([100u8; 32]); + +pub struct DidLifecycleHooks; + +impl did::traits::DidLifecycleHooks for DidLifecycleHooks { + type DeletionHook = + RequireBoth, EnsureNoLinkedAccountDeletionHook<1, 1, ()>>; +} + +impl did::Config for TestRuntime { + type BalanceMigrationManager = (); + type BaseDeposit = ConstU64<0>; + type Currency = MockCurrency; + type DidIdentifier = AccountId32; + type DidLifecycleHooks = DidLifecycleHooks; + type EnsureOrigin = EnsureSigned; + type Fee = ConstU64<0>; + type FeeCollector = (); + type KeyDeposit = ConstU64<0>; + type MaxBlocksTxValidity = ConstU64<1>; + type MaxNewKeyAgreementKeys = MaxNewKeyAgreementKeys; + type MaxNumberOfServicesPerDid = ConstU32<1>; + type MaxNumberOfTypesPerService = ConstU32<1>; + type MaxNumberOfUrlsPerService = ConstU32<1>; + type MaxPublicKeysPerDid = ConstU32<1>; + type MaxServiceIdLength = ConstU32<1>; + type MaxServiceTypeLength = ConstU32<1>; + type MaxServiceUrlLength = ConstU32<1>; + type MaxTotalKeyAgreementKeys = MaxTotalKeyAgreementKeys; + type OriginSuccess = AccountId32; + type RuntimeCall = RuntimeCall; + type RuntimeEvent = (); + type RuntimeHoldReason = RuntimeHoldReason; + type RuntimeOrigin = RuntimeOrigin; + type ServiceEndpointDeposit = ConstU64<0>; + type WeightInfo = (); +} + +impl pallet_did_lookup::Config for TestRuntime { + type AssociateOrigin = Self::EnsureOrigin; + type BalanceMigrationManager = (); + type Currency = MockCurrency; + type Deposit = ConstU64<0>; + type DidIdentifier = AccountId32; + type EnsureOrigin = EnsureSigned; + type OriginSuccess = AccountId32; + type RuntimeHoldReason = RuntimeHoldReason; + type RuntimeEvent = (); + type UniqueLinkingEnabled = ConstBool; + type WeightInfo = (); +} + +pub type Web3Name = crate::Web3Name<1, 2>; +impl pallet_web3_names::Config for TestRuntime { + type BalanceMigrationManager = (); + type BanOrigin = EnsureRoot; + type ClaimOrigin = Self::OwnerOrigin; + type Currency = MockCurrency; + type Deposit = ConstU64<0>; + type OriginSuccess = AccountId32; + type MaxNameLength = ConstU32<1>; + type MinNameLength = ConstU32<1>; + type OwnerOrigin = EnsureSigned; + type RuntimeEvent = (); + type RuntimeHoldReason = RuntimeHoldReason; + type Web3Name = Web3Name; + type Web3NameOwner = AccountId32; + type WeightInfo = (); + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = (); +} + +#[derive(Default)] +pub(super) struct ExtBuilder { + dids: Vec<(AccountId32, Option, bool)>, + dangling_dids: Vec<(AccountId32, Option, bool)>, +} + +impl ExtBuilder { + pub(super) fn with_dids(mut self, did_links: Vec<(AccountId32, Option, bool)>) -> Self { + self.dids = did_links; + self + } + + pub(super) fn with_dangling_dids(mut self, dangling_dids: Vec<(AccountId32, Option, bool)>) -> Self { + self.dangling_dids = dangling_dids; + self + } + + pub(super) fn build(self) -> TestExternalities { + let _ = env_logger::try_init(); + let mut ext = TestExternalities::default(); + + ext.execute_with(|| { + for (did, maybe_web3_name, should_link_account) in self.dids { + // Store DID. + Did::create_from_account( + RawOrigin::Signed(did.clone()).into(), + DidVerificationKey::Account(did.clone()), + ) + .expect("Failed to create DID."); + + // If specified, link web3name. + if let Some(web3_name) = maybe_web3_name { + Web3Names::claim( + RawOrigin::Signed(did.clone()).into(), + Vec::::from(web3_name.clone()).try_into().unwrap(), + ) + .expect("Failed to link web3name."); + } + + // If specified, link account. + if should_link_account { + LinkedAccounts::associate_sender(RawOrigin::Signed(did.clone()).into()) + .expect("Failed to link account."); + } + } + + for (did, maybe_web3_name, should_link_account) in self.dangling_dids { + // Cannot write the same DID as both linked and dangling. + assert!(!did::Did::::contains_key(&did)); + if maybe_web3_name.is_none() && !should_link_account { + panic!("One of web3name or linked account must be set."); + } + if let Some(web3_name) = maybe_web3_name { + Web3Names::claim( + RawOrigin::Signed(did.clone()).into(), + Vec::::from(web3_name.clone()).try_into().unwrap(), + ) + .expect("Failed to set dangling web3name."); + } + if should_link_account { + LinkedAccounts::associate_sender(RawOrigin::Signed(did.clone()).into()) + .expect("Failed to set dangling account."); + } + } + }); + + ext + } +} diff --git a/runtimes/common/src/did/deletion_hooks/tests/mod.rs b/runtimes/common/src/did/deletion_hooks/tests/mod.rs new file mode 100644 index 0000000000..99b2b0a033 --- /dev/null +++ b/runtimes/common/src/did/deletion_hooks/tests/mod.rs @@ -0,0 +1,92 @@ +// 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 mock; + +use frame_support::{assert_noop, assert_ok}; +use frame_system::RawOrigin; + +use crate::did::deletion_hooks::tests::mock::{Did, ExtBuilder, TestRuntime, DID}; + +#[test] +fn test_delete_with_no_dangling_resources() { + ExtBuilder::default() + .with_dids(vec![(DID, None, false)]) + .build() + .execute_with(|| { + assert_ok!(Did::delete(RawOrigin::Signed(DID).into(), 0)); + }); +} + +#[test] +fn test_delete_with_dangling_web3_name() { + ExtBuilder::default() + .with_dids(vec![(DID, Some(b"t".to_vec().try_into().unwrap()), false)]) + .build() + .execute_with(|| { + assert_noop!( + Did::delete(RawOrigin::Signed(DID).into(), 0), + did::Error::::CannotDelete + ); + }); +} + +#[test] +fn test_delete_with_dangling_linked_account() { + ExtBuilder::default() + .with_dids(vec![(DID, None, true)]) + .build() + .execute_with(|| { + assert_noop!( + Did::delete(RawOrigin::Signed(DID).into(), 0), + did::Error::::CannotDelete + ); + }); +} + +// If someone tries to re-delete a delete DID with dangling resources, they get +// a `NotFound` error. We are testing that we always check for DID existence +// before we check for linked resources. +#[test] +fn test_delete_with_no_did_and_dangling_web3_name() { + ExtBuilder::default() + .with_dangling_dids(vec![(DID, Some(b"t".to_vec().try_into().unwrap()), false)]) + .build() + .execute_with(|| { + assert_noop!( + Did::delete(RawOrigin::Signed(DID).into(), 0), + did::Error::::NotFound + ); + }); +} + +// If someone tries to re-delete a delete DID with dangling resources, they get +// a `NotFound` error. We are testing that we always check for DID existence +// before we check for linked resources. +#[test] +fn test_delete_with_no_did_and_dangling_linked_account() { + ExtBuilder::default() + .with_dangling_dids(vec![(DID, None, true)]) + .build() + .execute_with(|| { + assert_noop!( + Did::delete(RawOrigin::Signed(DID).into(), 0), + did::Error::::NotFound + ); + }); +} diff --git a/runtimes/common/src/did/mod.rs b/runtimes/common/src/did/mod.rs new file mode 100644 index 0000000000..f51d802733 --- /dev/null +++ b/runtimes/common/src/did/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 deletion_hooks; +pub use deletion_hooks::{EnsureNoLinkedAccountDeletionHook, EnsureNoLinkedWeb3NameDeletionHook}; diff --git a/runtimes/common/src/dip/mock.rs b/runtimes/common/src/dip/mock.rs index 74d9fad942..39e6beb601 100644 --- a/runtimes/common/src/dip/mock.rs +++ b/runtimes/common/src/dip/mock.rs @@ -121,6 +121,7 @@ impl did::Config for TestRuntime { type BaseDeposit = ConstU128; type Currency = Balances; type DidIdentifier = DidIdentifier; + type DidLifecycleHooks = (); type EnsureOrigin = EnsureSigned; type Fee = ConstU128; type FeeCollector = (); diff --git a/runtimes/common/src/lib.rs b/runtimes/common/src/lib.rs index 841cead22b..1056374f64 100644 --- a/runtimes/common/src/lib.rs +++ b/runtimes/common/src/lib.rs @@ -52,6 +52,8 @@ pub mod deposits; pub mod dip; pub mod dot_names; pub use dot_names::DotName; +pub mod did; +pub use did::{EnsureNoLinkedAccountDeletionHook, EnsureNoLinkedWeb3NameDeletionHook}; pub mod errors; pub mod fees; pub mod migrations; diff --git a/runtimes/kestrel/src/lib.rs b/runtimes/kestrel/src/lib.rs index 8c6ec5a26e..3b4e66bda3 100644 --- a/runtimes/kestrel/src/lib.rs +++ b/runtimes/kestrel/src/lib.rs @@ -423,6 +423,8 @@ impl did::Config for Runtime { type MaxNumberOfUrlsPerService = MaxNumberOfUrlsPerService; type WeightInfo = (); type BalanceMigrationManager = (); + // This differs from the implementation of the other runtimes. + type DidLifecycleHooks = (); } impl pallet_did_lookup::Config for Runtime { diff --git a/runtimes/peregrine/src/kilt/did.rs b/runtimes/peregrine/src/kilt/did/mod.rs similarity index 74% rename from runtimes/peregrine/src/kilt/did.rs rename to runtimes/peregrine/src/kilt/did/mod.rs index e81f9b6102..e2aaed4198 100644 --- a/runtimes/peregrine/src/kilt/did.rs +++ b/runtimes/peregrine/src/kilt/did/mod.rs @@ -17,22 +17,27 @@ // If you feel like getting in touch with us, you can do so at info@botlabs.org use did::{ - DeriveDidCallAuthorizationVerificationKeyRelationship, DeriveDidCallKeyRelationshipResult, DidRawOrigin, - DidVerificationKeyRelationship, EnsureDidOrigin, RelationshipDeriveError, + traits::deletion::RequireBoth, DeriveDidCallAuthorizationVerificationKeyRelationship, + DeriveDidCallKeyRelationshipResult, DidRawOrigin, DidVerificationKeyRelationship, EnsureDidOrigin, + RelationshipDeriveError, }; use frame_system::EnsureRoot; use runtime_common::{ constants, dot_names::{AllowedDotNameClaimer, AllowedUniqueLinkingAssociator}, - AccountId, DidIdentifier, SendDustAndFeesToTreasury, + AccountId, DidIdentifier, EnsureNoLinkedAccountDeletionHook, EnsureNoLinkedWeb3NameDeletionHook, + SendDustAndFeesToTreasury, }; use sp_core::ConstBool; use crate::{ - weights, Balances, DotNames, Migration, Runtime, RuntimeCall, RuntimeEvent, RuntimeHoldReason, RuntimeOrigin, - UniqueLinking, + weights::{self, rocksdb_weights::constants::RocksDbWeight}, + Balances, DotNames, Migration, Runtime, RuntimeCall, RuntimeEvent, RuntimeHoldReason, RuntimeOrigin, UniqueLinking, }; +#[cfg(test)] +mod tests; + impl DeriveDidCallAuthorizationVerificationKeyRelationship for RuntimeCall { fn derive_verification_key_relationship(&self) -> DeriveDidCallKeyRelationshipResult { /// ensure that all calls have the same VerificationKeyRelationship @@ -86,6 +91,53 @@ impl DeriveDidCallAuthorizationVerificationKeyRelationship for RuntimeCall { } } +pub struct DidLifecycleHooks; + +impl did::traits::DidLifecycleHooks for DidLifecycleHooks { + type DeletionHook = EnsureNoNamesAndNoLinkedAccountsOnDidDeletion; +} + +// Read size is given by the `MaxEncodedLen`: https://substrate.stackexchange.com/a/11843/1795. +// Since the trait is not `const`, we have unit tests that make sure the +// `max_encoded_len()` function matches this const. +const WORST_CASE_WEB3_NAME_STORAGE_READ_SIZE: u64 = 33; +/// Ensure there is no Web3Name linked to a DID. +type EnsureNoWeb3NameOnDeletion = + EnsureNoLinkedWeb3NameDeletionHook<{ RocksDbWeight::get().read }, WORST_CASE_WEB3_NAME_STORAGE_READ_SIZE, ()>; +// Read size is given by the `MaxEncodedLen`: https://substrate.stackexchange.com/a/11843/1795. +// Since the trait is not `const`, we have unit tests that make sure the +// `max_encoded_len()` function matches this const. +const WORST_CASE_DOT_NAME_STORAGE_READ_SIZE: u64 = 33; +/// Ensure there is no Dotname linked to a DID. +type EnsureNoDotNameOnDeletion = EnsureNoLinkedWeb3NameDeletionHook< + { RocksDbWeight::get().read }, + WORST_CASE_DOT_NAME_STORAGE_READ_SIZE, + DotNamesDeployment, +>; +/// Ensure there is neither a Web3Name nor a Dotname linked to a DID. +type EnsureNoUsernamesOnDeletion = RequireBoth; + +// Read size is given by the `MaxEncodedLen`: https://substrate.stackexchange.com/a/11843/1795. +// Since the trait is not `const`, we have unit tests that make sure the +// `max_encoded_len()` function matches this const. +const WORST_CASE_LINKING_STORAGE_READ_SIZE: u64 = 33; +/// Ensure there is no linked account (for a web3name) to a DID. +type EnsureNoWeb3NameLinkedAccountsOnDeletion = + EnsureNoLinkedAccountDeletionHook<{ RocksDbWeight::get().read }, WORST_CASE_LINKING_STORAGE_READ_SIZE, ()>; +/// Ensure there is no unique linked account (for a dotname) to a DID. +type EnsureNoDotNameLinkedAccountOnDeletion = EnsureNoLinkedWeb3NameDeletionHook< + { RocksDbWeight::get().read }, + WORST_CASE_LINKING_STORAGE_READ_SIZE, + UniqueLinkingDeployment, +>; +/// Ensure there is no account linked for both the DID's Web3Name and DotName. +type EnsureNoLinkedAccountsOnDeletion = + RequireBoth; + +/// Ensure there is no trace of names nor linked accounts for the DID. +pub type EnsureNoNamesAndNoLinkedAccountsOnDidDeletion = + RequireBoth; + impl did::Config for Runtime { type RuntimeEvent = RuntimeEvent; type RuntimeCall = RuntimeCall; @@ -121,6 +173,7 @@ impl did::Config for Runtime { type MaxNumberOfUrlsPerService = constants::did::MaxNumberOfUrlsPerService; type WeightInfo = weights::did::WeightInfo; type BalanceMigrationManager = Migration; + type DidLifecycleHooks = DidLifecycleHooks; } impl pallet_did_lookup::Config for Runtime { diff --git a/runtimes/peregrine/src/kilt/did/tests/did_hooks.rs b/runtimes/peregrine/src/kilt/did/tests/did_hooks.rs new file mode 100644 index 0000000000..cb5bd6f37d --- /dev/null +++ b/runtimes/peregrine/src/kilt/did/tests/did_hooks.rs @@ -0,0 +1,49 @@ +// 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 pallet_did_lookup::linkable_account::LinkableAccountId; +use parity_scale_codec::MaxEncodedLen; + +use crate::{ + kilt::did::{ + WORST_CASE_DOT_NAME_STORAGE_READ_SIZE, WORST_CASE_LINKING_STORAGE_READ_SIZE, + WORST_CASE_WEB3_NAME_STORAGE_READ_SIZE, + }, + DotName, Web3Name, +}; + +#[test] +fn test_worst_case_web3_name_storage_read() { + assert_eq!( + Web3Name::max_encoded_len() as u64, + WORST_CASE_WEB3_NAME_STORAGE_READ_SIZE + ); +} + +#[test] +fn test_worst_case_dot_name_storage_read() { + assert_eq!(DotName::max_encoded_len() as u64, WORST_CASE_DOT_NAME_STORAGE_READ_SIZE); +} + +#[test] +fn test_worst_case_web3_name_linked_account_storage_read() { + assert_eq!( + LinkableAccountId::max_encoded_len() as u64, + WORST_CASE_LINKING_STORAGE_READ_SIZE + ); +} diff --git a/runtimes/peregrine/src/kilt/did/tests/mod.rs b/runtimes/peregrine/src/kilt/did/tests/mod.rs new file mode 100644 index 0000000000..8a23678070 --- /dev/null +++ b/runtimes/peregrine/src/kilt/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 did_hooks; diff --git a/runtimes/spiritnet/src/kilt/did.rs b/runtimes/spiritnet/src/kilt/did/mod.rs similarity index 74% rename from runtimes/spiritnet/src/kilt/did.rs rename to runtimes/spiritnet/src/kilt/did/mod.rs index e81f9b6102..292e3414cc 100644 --- a/runtimes/spiritnet/src/kilt/did.rs +++ b/runtimes/spiritnet/src/kilt/did/mod.rs @@ -17,22 +17,27 @@ // If you feel like getting in touch with us, you can do so at info@botlabs.org use did::{ - DeriveDidCallAuthorizationVerificationKeyRelationship, DeriveDidCallKeyRelationshipResult, DidRawOrigin, - DidVerificationKeyRelationship, EnsureDidOrigin, RelationshipDeriveError, + traits::deletion::RequireBoth, DeriveDidCallAuthorizationVerificationKeyRelationship, + DeriveDidCallKeyRelationshipResult, DidRawOrigin, DidVerificationKeyRelationship, EnsureDidOrigin, + RelationshipDeriveError, }; use frame_system::EnsureRoot; use runtime_common::{ constants, + did::EnsureNoLinkedAccountDeletionHook, dot_names::{AllowedDotNameClaimer, AllowedUniqueLinkingAssociator}, - AccountId, DidIdentifier, SendDustAndFeesToTreasury, + AccountId, DidIdentifier, EnsureNoLinkedWeb3NameDeletionHook, SendDustAndFeesToTreasury, }; use sp_core::ConstBool; use crate::{ - weights, Balances, DotNames, Migration, Runtime, RuntimeCall, RuntimeEvent, RuntimeHoldReason, RuntimeOrigin, - UniqueLinking, + weights::{self, rocksdb_weights::constants::RocksDbWeight}, + Balances, DotNames, Migration, Runtime, RuntimeCall, RuntimeEvent, RuntimeHoldReason, RuntimeOrigin, UniqueLinking, }; +#[cfg(test)] +mod tests; + impl DeriveDidCallAuthorizationVerificationKeyRelationship for RuntimeCall { fn derive_verification_key_relationship(&self) -> DeriveDidCallKeyRelationshipResult { /// ensure that all calls have the same VerificationKeyRelationship @@ -86,6 +91,53 @@ impl DeriveDidCallAuthorizationVerificationKeyRelationship for RuntimeCall { } } +pub struct DidLifecycleHooks; + +impl did::traits::DidLifecycleHooks for DidLifecycleHooks { + type DeletionHook = EnsureNoNamesAndNoLinkedAccountsOnDidDeletion; +} + +// Read size is given by the `MaxEncodedLen`: https://substrate.stackexchange.com/a/11843/1795. +// Since the trait is not `const`, we have unit tests that make sure the +// `max_encoded_len()` function matches this const. +const WORST_CASE_WEB3_NAME_STORAGE_READ_SIZE: u64 = 33; +/// Ensure there is no Web3Name linked to a DID. +type EnsureNoWeb3NameOnDeletion = + EnsureNoLinkedWeb3NameDeletionHook<{ RocksDbWeight::get().read }, WORST_CASE_WEB3_NAME_STORAGE_READ_SIZE, ()>; +// Read size is given by the `MaxEncodedLen`: https://substrate.stackexchange.com/a/11843/1795. +// Since the trait is not `const`, we have unit tests that make sure the +// `max_encoded_len()` function matches this const. +const WORST_CASE_DOT_NAME_STORAGE_READ_SIZE: u64 = 33; +/// Ensure there is no Dotname linked to a DID. +type EnsureNoDotNameOnDeletion = EnsureNoLinkedWeb3NameDeletionHook< + { RocksDbWeight::get().read }, + WORST_CASE_DOT_NAME_STORAGE_READ_SIZE, + DotNamesDeployment, +>; +/// Ensure there is neither a Web3Name nor a Dotname linked to a DID. +type EnsureNoUsernamesOnDeletion = RequireBoth; + +// Read size is given by the `MaxEncodedLen`: https://substrate.stackexchange.com/a/11843/1795. +// Since the trait is not `const`, we have unit tests that make sure the +// `max_encoded_len()` function matches this const. +const WORST_CASE_LINKING_STORAGE_READ_SIZE: u64 = 33; +/// Ensure there is no linked account (for a web3name) to a DID. +type EnsureNoWeb3NameLinkedAccountsOnDeletion = + EnsureNoLinkedAccountDeletionHook<{ RocksDbWeight::get().read }, WORST_CASE_LINKING_STORAGE_READ_SIZE, ()>; +/// Ensure there is no unique linked account (for a dotname) to a DID. +type EnsureNoDotNameLinkedAccountOnDeletion = EnsureNoLinkedWeb3NameDeletionHook< + { RocksDbWeight::get().read }, + WORST_CASE_LINKING_STORAGE_READ_SIZE, + UniqueLinkingDeployment, +>; +/// Ensure there is no account linked for both the DID's Web3Name and DotName. +type EnsureNoLinkedAccountsOnDeletion = + RequireBoth; + +/// Ensure there is no trace of names nor linked accounts for the DID. +pub type EnsureNoNamesAndNoLinkedAccountsOnDidDeletion = + RequireBoth; + impl did::Config for Runtime { type RuntimeEvent = RuntimeEvent; type RuntimeCall = RuntimeCall; @@ -121,6 +173,7 @@ impl did::Config for Runtime { type MaxNumberOfUrlsPerService = constants::did::MaxNumberOfUrlsPerService; type WeightInfo = weights::did::WeightInfo; type BalanceMigrationManager = Migration; + type DidLifecycleHooks = DidLifecycleHooks; } impl pallet_did_lookup::Config for Runtime { diff --git a/runtimes/spiritnet/src/kilt/did/tests/did_hooks.rs b/runtimes/spiritnet/src/kilt/did/tests/did_hooks.rs new file mode 100644 index 0000000000..cb5bd6f37d --- /dev/null +++ b/runtimes/spiritnet/src/kilt/did/tests/did_hooks.rs @@ -0,0 +1,49 @@ +// 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 pallet_did_lookup::linkable_account::LinkableAccountId; +use parity_scale_codec::MaxEncodedLen; + +use crate::{ + kilt::did::{ + WORST_CASE_DOT_NAME_STORAGE_READ_SIZE, WORST_CASE_LINKING_STORAGE_READ_SIZE, + WORST_CASE_WEB3_NAME_STORAGE_READ_SIZE, + }, + DotName, Web3Name, +}; + +#[test] +fn test_worst_case_web3_name_storage_read() { + assert_eq!( + Web3Name::max_encoded_len() as u64, + WORST_CASE_WEB3_NAME_STORAGE_READ_SIZE + ); +} + +#[test] +fn test_worst_case_dot_name_storage_read() { + assert_eq!(DotName::max_encoded_len() as u64, WORST_CASE_DOT_NAME_STORAGE_READ_SIZE); +} + +#[test] +fn test_worst_case_web3_name_linked_account_storage_read() { + assert_eq!( + LinkableAccountId::max_encoded_len() as u64, + WORST_CASE_LINKING_STORAGE_READ_SIZE + ); +} diff --git a/runtimes/spiritnet/src/kilt/did/tests/mod.rs b/runtimes/spiritnet/src/kilt/did/tests/mod.rs new file mode 100644 index 0000000000..8a23678070 --- /dev/null +++ b/runtimes/spiritnet/src/kilt/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 did_hooks; diff --git a/support/src/mock.rs b/support/src/mock.rs index 2f71f31abd..9a2f1977a9 100644 --- a/support/src/mock.rs +++ b/support/src/mock.rs @@ -18,10 +18,15 @@ //! This module contains utilities for testing. +use frame_support::traits::{ + fungible::{Balanced, Dust, Inspect, InspectHold, Mutate, MutateHold, Unbalanced, UnbalancedHold}, + tokens::{Balance as BalanceT, DepositConsequence, Fortitude, Preservation, Provenance, WithdrawConsequence}, +}; use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; use scale_info::TypeInfo; use sp_core::sr25519; -use sp_runtime::AccountId32; +use sp_runtime::{AccountId32, DispatchError, DispatchResult}; +use sp_std::marker::PhantomData; /// This pallet only contains an origin which supports separated sender and /// subject. @@ -171,3 +176,109 @@ impl AsRef<[u8]> for SubjectId { self.0.as_ref() } } + +/// Mock currency that implements all required traits, allowing test runtimes to +/// not include the actual `pallet_balances` pallet. This mock currency is +/// useful for mocks in which a `Currency` is required but not relevant for the +/// goal of the tests. +pub struct MockCurrency(PhantomData<(Balance, RuntimeHoldReason)>); + +impl MutateHold for MockCurrency +where + Balance: BalanceT, + RuntimeHoldReason: Encode + TypeInfo + 'static, +{ +} + +impl UnbalancedHold for MockCurrency +where + Balance: BalanceT, + RuntimeHoldReason: Encode + TypeInfo + 'static, +{ + fn set_balance_on_hold(_reason: &Self::Reason, _who: &AccountId, _amount: Self::Balance) -> DispatchResult { + Ok(()) + } +} + +impl InspectHold for MockCurrency +where + Balance: BalanceT, + RuntimeHoldReason: Encode + TypeInfo + 'static, +{ + type Reason = RuntimeHoldReason; + + fn total_balance_on_hold(_who: &AccountId) -> Self::Balance { + Self::Balance::default() + } + + fn balance_on_hold(_reason: &Self::Reason, _who: &AccountId) -> Self::Balance { + Self::Balance::default() + } +} + +impl Mutate for MockCurrency +where + AccountId: Eq, + Balance: BalanceT, +{ +} + +impl Inspect for MockCurrency +where + Balance: BalanceT, +{ + type Balance = Balance; + + fn active_issuance() -> Self::Balance { + Self::Balance::default() + } + + fn balance(_who: &AccountId) -> Self::Balance { + Self::Balance::default() + } + + fn can_deposit(_who: &AccountId, _amount: Self::Balance, _provenance: Provenance) -> DepositConsequence { + DepositConsequence::Success + } + + fn can_withdraw(_who: &AccountId, _amount: Self::Balance) -> WithdrawConsequence { + WithdrawConsequence::Success + } + + fn minimum_balance() -> Self::Balance { + Self::Balance::default() + } + + fn reducible_balance(_who: &AccountId, _preservation: Preservation, _force: Fortitude) -> Self::Balance { + Self::Balance::default() + } + + fn total_balance(_who: &AccountId) -> Self::Balance { + Self::Balance::default() + } + + fn total_issuance() -> Self::Balance { + Self::Balance::default() + } +} + +impl Unbalanced for MockCurrency +where + Balance: BalanceT, +{ + fn handle_dust(_dust: Dust) {} + + fn write_balance(_who: &AccountId, _amount: Self::Balance) -> Result, DispatchError> { + Ok(Some(Self::Balance::default())) + } + + fn set_total_issuance(_amount: Self::Balance) {} +} + +impl Balanced for MockCurrency +where + Balance: BalanceT, +{ + type OnDropDebt = (); + type OnDropCredit = (); +}