diff --git a/Cargo.toml b/Cargo.toml index 6fc8de1d..4c4828d3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -151,6 +151,7 @@ pallet-session-benchmarking = { path = 'pallets/session-benchmarking', default-f pallet-assets-runtime-api = { path = "runtimes/common/api/assets", default-features = false } pallet-did-runtime-api = { path = "runtimes/common/api/did", default-features = false } pallet-transaction-weight-runtime-api = { path = "runtimes/common/api/weight", default-features = false } +pallet-dedir = { path = "pallets/dedir", default-features = false } # substrate dependencies frame-benchmarking = { git = "https://github.com/dhiway/substrate-sdk", default-features = false, branch = "release-v1.13.0" } diff --git a/pallets/dedir/Cargo.toml b/pallets/dedir/Cargo.toml new file mode 100644 index 00000000..df430afe --- /dev/null +++ b/pallets/dedir/Cargo.toml @@ -0,0 +1,79 @@ +[package] +name = "pallet-dedir" +description = "Decentralized Directory Management" +authors = [ + "Dhiway Networks ", +] +version.workspace = true +edition.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true + +[lints] +workspace = true + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +codec = { features = ["derive", "max-encoded-len"], workspace = true } +scale-info = { features = ["derive"], workspace = true } +enumflags2 = { workspace = true } +frame-system = { workspace = true } +frame-support = { workspace = true } +sp-runtime = { workspace = true } +sp-std = { workspace = true } +sp-io = { workspace = true } +sp-core = { workspace = true } +identifier = { workspace = true } +cord-utilities = { features = ["mock"], workspace = true } +log = { workspace = true } +serde_json = { workspace = true } +bitflags = { workspace = true } + +frame-benchmarking = { optional = true, workspace = true } + +[dev-dependencies] +sp-core = { workspace = true } +cord-utilities = { workspace = true } +identifier = { workspace = true } +sp-keystore = { workspace = true } +serde_json = { workspace = true } + +[features] +default = ["std"] + +std = [ + "codec/std", + "enumflags2/std", + "frame-benchmarking?/std", + "frame-support/std", + "frame-system/std", + "scale-info/std", + "sp-core/std", + "sp-io/std", + "sp-keystore/std", + "sp-runtime/std", + "sp-std/std", + "identifier/std", + "cord-utilities/std", +] +runtime-benchmarks = [ + "frame-benchmarking/runtime-benchmarks", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", + "cord-utilities/runtime-benchmarks", +] +try-runtime = [ + "frame-support/try-runtime", + "frame-system/try-runtime", + "cord-utilities/try-runtime", + "identifier/try-runtime", + "sp-runtime/try-runtime", +] + +# Disable doctest when running `cargo test` +[lib] +doctest = false diff --git a/pallets/dedir/src/lib.rs b/pallets/dedir/src/lib.rs new file mode 100644 index 00000000..9f3b80f9 --- /dev/null +++ b/pallets/dedir/src/lib.rs @@ -0,0 +1,1009 @@ +// This file is part of CORD – https://cord.network + +// Copyright (C) Dhiway Networks Pvt. Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// CORD 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. + +// CORD 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 CORD. If not, see . +// + +//! # Decentralized Directory Pallet - DeDir +//! +//! +//! ## Overview +//! +//! The Decentralized Directory Pallet (DeDir) aims to implement a decentralized +//! version of a registry. Enabling creation, updation of registries in a distributed +//! decentralized manner. Thereby enabling trust and transperency utilizing CORD blockchain. +//! +//! ## Interface +//! +//! ### Dispatchable Functions +//! +//! * `create_registry` - Create a registry, with blob and digest. +//! * `create_registry_entry` - Create a registry entry for the created registry. +//! * `registry_entry_state_change` - Change the status of the registry entry. +//! * `add_delegate` - Add a account as a delegate with specific permission. +//! * `remove_delegate` - Add a existing account from authorized delegates list. +//! * `update_delegate_permission` - Update the permission of an existing delegate. + +#![cfg_attr(not(feature = "std"), no_std)] + +mod types; + +#[cfg(any(feature = "mock", test))] +pub mod mock; + +#[cfg(test)] +mod tests; + +use frame_support::{ + ensure, + pallet_prelude::DispatchResult, + traits::{Get, StorageVersion}, + BoundedVec, +}; + +pub use pallet::*; +use sp_std::{prelude::*, str}; + +use sp_runtime::traits::Hash; + +pub use frame_system::WeightInfo; +pub use types::{ + DelegateInfo, Delegates, Entry, Permissions, Registry, RegistryEntry, RegistrySupportedStateOf, +}; + +use codec::Encode; + +#[frame_support::pallet] +pub mod pallet { + use super::*; + use frame_support::pallet_prelude::*; + use frame_system::pallet_prelude::*; + pub use identifier::{ + CordIdentifierType, IdentifierCreator, IdentifierTimeline, IdentifierType, Ss58Identifier, + }; + + pub type RegistryHashOf = ::Hash; + pub type RegistryEntryHashOf = ::Hash; + + pub type RegistryIdOf = Ss58Identifier; + pub type RegistryEntryIdOf = Ss58Identifier; + pub type RegistryStateOf = BoundedVec::MaxEncodedInputLength>; + pub type RegistryKeyIdOf = BoundedVec::MaxEncodedInputLength>; + pub type MaxDelegatesOf = ::MaxRegistryDelegates; + + pub type MaxRegistryBlobSizeOf = ::MaxRegistryBlobSize; + + pub type OwnerOf = ::AccountId; + pub type DelegateOf = ::AccountId; + + pub type DelegateEntryOf = + BoundedVec, Permissions>, MaxDelegatesOf>; + + pub type RegistryBlobOf = BoundedVec>; + + #[pallet::config] + pub trait Config: frame_system::Config { + /// The overarching event type. + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + /// The maximum number of bytes in size a Registry Blob can hold. + #[pallet::constant] + type MaxRegistryBlobSize: Get; + + /// The maximum number of Registry Entries supported for a Registry. + #[pallet::constant] + type MaxRegistryDelegates: Get; + + /// The maximum encoded length available for naming. + /// Used by Identifiers, RegistryKeyIds, RegistryStates. + #[pallet::constant] + type MaxEncodedInputLength: Get; + + /// Weight information for extrinsics in this pallet. + type WeightInfo: WeightInfo; + } + + const STORAGE_VERSION: StorageVersion = StorageVersion::new(1); + + #[pallet::pallet] + #[pallet::storage_version(STORAGE_VERSION)] + pub struct Pallet(_); + + /// Single Storage Map to hold Registry inforamtion + /// Holds a bounded vector of Registry Key Ids and corresponding + /// Registry Type of maximum `MaxRegistryEntries`. + #[pallet::storage] + pub type Registries = StorageMap< + _, + Blake2_128Concat, + RegistryIdOf, + Registry, RegistryHashOf, OwnerOf>, + OptionQuery, + >; + + /// Double Storage Map to hold Registry Entry information + /// for a Registry. + #[pallet::storage] + pub type RegistryEntries = StorageDoubleMap< + _, + Blake2_128Concat, + RegistryIdOf, + Blake2_128Concat, + RegistryEntryIdOf, + RegistryEntry< + RegistryBlobOf, + RegistryEntryHashOf, + RegistryIdOf, + RegistrySupportedStateOf, + >, + OptionQuery, + >; + + /// Single Storage Map to hold the details of Delegate + /// information for a Registry. + #[pallet::storage] + pub type DelegatesList = + StorageMap<_, Blake2_128Concat, RegistryIdOf, Delegates>, OptionQuery>; + + #[pallet::error] + pub enum Error { + /// Invalid Identifer Length + InvalidIdentifierLength, + /// Identifier Invalid or Not of DeDir Type + InvalidDeDirIdentifier, + /// Account has no valid authorization + UnauthorizedOperation, + /// Registry Identifier Already Exists + RegistryIdAlreadyExists, + /// Registry Identifier Does Not Exists + RegistryIdDoesNotExist, + /// Registry Entry Identifier Already Exists + RegistryEntryIdAlreadyExists, + /// Registry Entry Identifier Does Not Exists + RegistryEntryIdDoesNotExist, + /// State Not Found In Declared Registry + StateNotSupported, + /// Max Delegates Storage Upper Bound Breached + MaxDelegatesStorageOverflow, + /// Delegates List not found for Registry Id + DelegatesListNotFound, + /// Registry Owner cannot be removed from Delegates List + CannotRemoveRegistryOwnerAsDelegate, + /// Delegate not found in DelegatesList + DelegateNotFound, + /// Invalid Permission + InvalidPermission, + /// Admin is unauthorized from removing another Admin + AdminCannotRemoveAnotherAdmin, + /// Delegate is unauthorized from Delegate Operations. + DelegateCannotRemoveAccounts, + /// Registry Owner Permissions cannot be updated. + CannotUpdateOwnerPermission, + /// Delegator cannot be removed. + DelegatorCannotBeRemoved, + /// Delegator cannot be added. + DelegatorCannotBeAdded, + /// Delegator cannot be updated. + DelegatorCannotBeUpdated, + /// Delegate already exists with same permission. + DelegateAlreadyExistsWithSamePermission, + /// Delegate alreay exists. + DelegateAlreadyAdded, + /// Blob and Digest Does not match. + BlobDoesNotMatchDigest, + } + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// A new registry has been created. + /// \[creator, registry_identifier\] + CreatedRegistry { creator: T::AccountId, registry_id: RegistryIdOf }, + /// A new registry entry has been created. + /// \[creator, registry_identifier, registry_entry_identifier\] + CreatedRegistryEntry { + creator: T::AccountId, + registry_id: RegistryIdOf, + registry_entry_id: RegistryEntryIdOf, + }, + /// State change has been made for existing registry entry. + /// \[who, registry_identifier, registry_entry_identifier, new_state\] + RegistryEntryStateChanged { + who: T::AccountId, + registry_id: RegistryIdOf, + registry_entry_id: RegistryEntryIdOf, + new_state: RegistrySupportedStateOf, + }, + /// A new Delegate has been added to the Registry. + /// \[delegator, registry_id, delegate, permission] + RegistryDelegateAdded { + delegator: T::AccountId, + registry_id: RegistryIdOf, + delegate: DelegateOf, + permission: Permissions, + }, + /// A existing Delegate has been removed from the Registry. + /// \[delegator, registry_id, delegate] + RegistryDelegateRemoved { + delegator: T::AccountId, + registry_id: RegistryIdOf, + delegate: DelegateOf, + }, + /// A existing Registry Delegate permissions have been updated. + /// \[delegator, registry_id, delegate, new_permission] + RegistryDelegatePermissionUpdated { + delegator: T::AccountId, + registry_id: RegistryIdOf, + delegate: DelegateOf, + new_permission: Permissions, + }, + } + + #[pallet::call] + /// DeDir pallet declaration. + impl Pallet { + /// A new Registry has been created. + /// + /// This function allows a user to submit a new create registry request. + /// The Registry is created along with various metadata, including the + /// blob, digest. + /// + /// # Arguments + /// * `origin` - The origin of the call, which should be a signed user in most cases. + /// * `registry_id` - The `registry_id` which is to be of SS58Identifier and must be of + /// Ident type `DeDir`. + /// * `digest` - The digest to be bound to the Registry. + /// * `blob` - (Optional) Bounded Vector of Blob which is derived from same file as digest. + /// + /// # Errors + /// Returns `Error::::RegistryIdAlreadyExists` if the registry identifier + /// exists already. + /// + /// # Events + /// Emits `CreatedRegistry` when a new Registry is created successfully. + /// + /// # Example + /// ``` + /// create_registry(origin, registry_id, digest, blob)?; + /// ``` + #[pallet::call_index(0)] + #[pallet::weight({0})] + pub fn create_registry( + origin: OriginFor, + registry_id: RegistryIdOf, + digest: RegistryHashOf, + blob: Option>, + ) -> DispatchResult { + let creator = ensure_signed(origin)?; + + /* Identifier Management will happen at SDK. + * It is to be constructed as below. + */ + // let id_digest = ::Hashing::hash( + // &[&creator.encode()[..], &digest.encode()[..]].concat()[..], + // ); + // let registry_id = + // Ss58Identifier::create_identifier(&(id_digest).encode()[..], IdentifierType::DeDir) + // .map_err(|_| Error::::InvalidIdentifierLength)?; + + /* Ensure that registry_id is of valid ss58 format, + * and also the type matches to be of `DeDir` + */ + ensure!(Self::is_valid_ss58_format(®istry_id), Error::::InvalidDeDirIdentifier); + + /* Ensure that the registry_id does not already exist */ + ensure!( + !Registries::::contains_key(®istry_id), + Error::::RegistryIdAlreadyExists + ); + + let mut registry = + Registry { blob: BoundedVec::default(), digest, owner: creator.clone() }; + + if let Some(blob) = blob { + /* Check if blob and digest matches */ + ensure!( + Self::does_blob_matches_digest(&blob, &digest), + Error::::BlobDoesNotMatchDigest + ); + + registry.blob = blob; + } + + Registries::::insert(®istry_id, registry); + + let mut delegates = Delegates { entries: BoundedVec::default() }; + + /* Set `delegator` to `None` for owner */ + if delegates + .entries + .try_push(DelegateInfo { + delegate: creator.clone(), + permissions: Permissions::OWNER, + delegator: None, + }) + .is_err() + { + return Err(Error::::MaxDelegatesStorageOverflow.into()); + }; + + DelegatesList::::insert(®istry_id, delegates); + + Self::deposit_event(Event::CreatedRegistry { creator, registry_id }); + + Ok(()) + } + + /// Registry Entry has been created for a Registry. + /// + /// This function allows a user to submit a new create registry entry request + /// for a existing Registry. + /// The Registry Entry is created along with various metadata, including the + /// blob, digest, and the state of the Registry Entry. + /// + /// # Arguments + /// * `origin` - The origin of the call, which should be a signed user in most cases. + /// * `registry_id` - Registry Identifier associated with a existing Registry. + /// * `registry_entry_id` - The `registry_entry_id` which is to be of SS58Identifier and + /// must be of Ident type `DeDir`. + /// * `digest` - The digest to be bound to the Registry Entry. + /// * `blob` - (Optional) Bounded Vector of Blob which is derived from same file as digest. + /// * `state` - (Optional) Valid registry state for the registry entry to be associated + /// with. By default it shall be set to DRAFT. + /// + /// # Errors + /// Returns `Error::::RegistryIdDoesNotExists` if the registry identifier + /// does not exist. + /// Returns `Error::::RegistryEntryIdAlreadyExists` if the registry entry + /// identifier exists already. + /// Attributes Key addition does not have a associated key during Registry Creation. + /// + /// # Events + /// Emits `CreatedRegistryEntry` when a new Registry Entry is created successfully. + /// + /// # Example + /// ``` + /// create_registry_entry(origin, registry_id, + /// registry_entry_id, digest, blob, state)?; + /// ``` + #[pallet::call_index(1)] + #[pallet::weight({0})] + pub fn create_registry_entry( + origin: OriginFor, + registry_id: RegistryIdOf, + registry_entry_id: RegistryEntryIdOf, + digest: RegistryEntryHashOf, + blob: Option>, + state: Option, + ) -> DispatchResult { + let creator = ensure_signed(origin)?; + + /* Ensure RegistryId exists */ + let _registry = + Registries::::get(®istry_id).ok_or(Error::::RegistryIdDoesNotExist)?; + + /* Ensure that registry_entry is created from authorized account */ + let delegates = + DelegatesList::::get(®istry_id).ok_or(Error::::DelegatesListNotFound)?; + + /* Ensure there exists a valid_delegate */ + let permitted_permissions = + Permissions::OWNER | Permissions::ADMIN | Permissions::DELEGATE; + ensure!( + Self::is_valid_delegate(&delegates.entries, &creator, permitted_permissions,), + Error::::UnauthorizedOperation + ); + + /* Identifier Management will happen at SDK. + * It is to be constructed as below. + */ + // let id_digest = ::Hashing::hash( + // &[®istry_id.encode()[..], &digest.encode()[..]].concat()[..], + // ); + // let registry_entry_id = + // Ss58Identifier::create_identifier(&(id_digest).encode()[..], IdentifierType::DeDir) + // .map_err(|_| Error::::InvalidIdentifierLength)?; + + /* Ensure that registry_entry_id is of valid ss58 format, + * and also the type matches to be of `DeDir` + */ + ensure!( + Self::is_valid_ss58_format(®istry_entry_id), + Error::::InvalidDeDirIdentifier + ); + + /* Ensure RegistryEntryId does not already exists for given RegistryId */ + ensure!( + RegistryEntries::::get(®istry_id, ®istry_entry_id).is_none(), + Error::::RegistryEntryIdAlreadyExists + ); + + /* Set default state to `DRAFT` */ + let current_state = if let Some(state) = state { + ensure!(state.is_valid_state(), Error::::StateNotSupported); + state + } else { + RegistrySupportedStateOf::DRAFT + }; + + /* Set known and default values */ + let mut registry_entry = RegistryEntry { + blob: BoundedVec::default(), + registry_id: registry_id.clone(), + digest, + current_state: current_state.clone(), + }; + + if let Some(blob) = blob { + /* Check if blob and digest matches */ + ensure!( + Self::does_blob_matches_digest(&blob, &digest), + Error::::BlobDoesNotMatchDigest + ); + + registry_entry.blob = blob; + } + + RegistryEntries::::insert(®istry_id, ®istry_entry_id, registry_entry); + + Self::deposit_event(Event::CreatedRegistryEntry { + creator, + registry_id, + registry_entry_id, + }); + + Ok(()) + } + + /// Change the state of Registry Entry. + /// + /// This function allows a user to submit a change of state request for an + /// for a existing Registry Entry. + /// The State of Registry Entry is updated with the new-state which is part of + /// existing supported-states. + /// + /// # Arguments + /// * `origin` - The origin of the call, which should be a signed user in most cases. + /// * `registry_id` - Registry Identifier associated with a existing Registry. + /// * `registry_entry_id` - Registry Entry Identifier to be associated with the Registry + /// Entry. + /// * `new_state` - The `new_state` which Registry Entry must be updated to. + /// + /// # Errors + /// Returns `Error::::RegistryIdDoesNotExists` if the registry identifier + /// does not exist. + /// Returns `Error::::RegistryEntryIdDoesNotExists` if the registry entry + /// identifier does not exist. + /// Returns `Error::::StateNotSupported` State not found in declared Registry. + /// + /// # Events + /// Emits `RegistryEntryStateChanged` when a new Registry Entry State has been updated. + /// + /// # Example + /// ``` + /// registry_entry_state_change(origin, registry_id, registry_entry_id, + /// new_state)?; + /// ``` + #[pallet::call_index(2)] + #[pallet::weight({0})] + pub fn registry_entry_state_change( + origin: OriginFor, + registry_id: RegistryIdOf, + registry_entry_id: RegistryEntryIdOf, + new_state: RegistrySupportedStateOf, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + + /* Ensure RegistryId exists */ + let _registry = + Registries::::get(®istry_id).ok_or(Error::::RegistryIdDoesNotExist)?; + + /* Ensure RegistryEntryId exists for the given RegistryId */ + let mut registry_entry = RegistryEntries::::get(®istry_id, ®istry_entry_id) + .ok_or(Error::::RegistryEntryIdDoesNotExist)?; + + /* Ensure that registry state updation happens from authorized account */ + let delegates = + DelegatesList::::get(®istry_id).ok_or(Error::::DelegatesListNotFound)?; + + let permitted_permissions = + Permissions::OWNER | Permissions::ADMIN | Permissions::DELEGATE; + ensure!( + Self::is_valid_delegate(&delegates.entries, &who, permitted_permissions,), + Error::::UnauthorizedOperation + ); + + /* Ensure given `new_state` is part of supported states enum */ + ensure!(new_state.is_valid_state(), Error::::StateNotSupported); + + registry_entry.current_state = new_state.clone(); + RegistryEntries::::insert(®istry_id, ®istry_entry_id, registry_entry); + + Self::deposit_event(Event::RegistryEntryStateChanged { + who, + registry_id, + registry_entry_id, + new_state, + }); + + Ok(()) + } + + /// Add a Delegate to a Registry. + /// + /// This function allows a user to add a delegate with a specified permission to an + /// existing registry. The function ensures that the addition complies with the rules + /// governing delegate permissions and authorization. + /// + /// # Rules for Adding a Delegate: + /// + /// - **Registry Existence:** The registry identified by `registry_id` must exist. If the + /// registry does not exist, the function will return an error. + /// + /// - **Unique Delegator and Delegate:** The delegate being added cannot be the same as the + /// delegator initiating the operation. This ensures that a user cannot delegate + /// permissions to themselves. + /// + /// - **Owner Permission Constraint:** The `OWNER` permission cannot be assigned to any + /// delegate. This prevents multiple delegates from having `OWNER` permissions within the + /// same registry. + /// + /// - **Authorization Requirements:** Only delegates with `OWNER` or `ADMIN` permissions can + /// add new delegates. Delegates with `DELEGATE` permissions are not authorized to add + /// other delegates. + /// + /// - **Existing Delegate Check:** The function checks whether the delegate already exists + /// in the registry. If the delegate is already listed with same permission, the operation + /// will return an error. If the delegate is same but with different permission, the + /// permission bits shall be overlapped as union. + /// + /// - **Storage Overflow Handling:** If the addition of the new delegate exceeds the maximum + /// allowed storage, an error will be returned. + /// + /// # Arguments + /// * `origin` - The origin of the call, which should be a signed user in most cases. + /// * `registry_id` - The registry identifier associated with an existing registry. + /// * `delegate` - The account to be added as a delegate. + /// * `permission` - The permission to be assigned to the delegate. Valid permissions are + /// `ADMIN` and `DELEGATE`. + /// + /// # Errors + /// Returns `Error::::RegistryIdDoesNotExist` if the registry identifier does not exist. + /// Returns `Error::::DelegatorCannotBeAdded` if the delegate and delegator are the same. + /// Returns `Error::::InvalidPermission` if an attempt is made to assign the `OWNER` + /// permission. Returns `Error::::DelegatesListNotFound` if the list of delegates for + /// the registry cannot be found. Returns `Error::::UnauthorizedOperation` if the + /// origin does not have sufficient permissions to add a delegate. + /// Returns `Error::::DelegateAlreadyAdded` if the delegate already exists in the + /// registry. Returns `Error::::MaxDelegatesStorageOverflow` if the addition of the new + /// delegate exceeds the maximum allowed storage. + /// + /// # Events + /// Emits `RegistryDelegateAdded` when a new delegate is successfully added to the registry. + /// + /// # Example + /// ``` + /// add_delegate(origin, registry_id, delegate, permission)?; + /// ``` + #[pallet::call_index(3)] + #[pallet::weight({0})] + pub fn add_delegate( + origin: OriginFor, + registry_id: RegistryIdOf, + delegate: DelegateOf, + permission: Permissions, + ) -> DispatchResult { + let delegator = ensure_signed(origin)?; + + /* Ensure RegistryId exists */ + let _registry = + Registries::::get(®istry_id).ok_or(Error::::RegistryIdDoesNotExist)?; + + /* Ensure delegator and delegate are not same */ + ensure!(delegate != delegator, Error::::DelegatorCannotBeAdded); + + /* Ensure OWNER permission is not assigned */ + ensure!(!permission.contains(Permissions::OWNER), Error::::InvalidPermission); + + /* Ensure that registry_entry is created from authorized account */ + let mut delegates = + DelegatesList::::get(®istry_id).ok_or(Error::::DelegatesListNotFound)?; + + /* Ensure there exists a valid_delegate with + * OWNER or ADMIN permissions to add a delegate. + */ + ensure!( + Self::is_valid_delegate( + &delegates.entries, + &delegator, + Permissions::OWNER | Permissions::ADMIN + ), + Error::::UnauthorizedOperation + ); + + /* Handle avoidance of mulitple entries with same `delegate`. + * Overlap the `Permissions` bits if there exists a same `delegate` entry + */ + if let Some(existing_entry) = + delegates.entries.iter_mut().find(|d| d.delegate == delegate) + { + // TODO: + // Revisit should there be a strict check or mild check. + // Example scenario: + // Already there exists a delegate with `DELEGATE` permission. + // If the same delegate is being added again with `ADMIN | DELEGATE` permission, + // then should we allow the permissions to added or throw error that `DELEGATE` + // permission already exists. + if existing_entry.permissions.intersects(permission) { + return Err(Error::::DelegateAlreadyExistsWithSamePermission.into()); + } + existing_entry.permissions |= permission; + } else { + delegates + .entries + .try_push(DelegateInfo { + delegate: delegate.clone(), + permissions: permission, + delegator: Some(delegator.clone()), + }) + .map_err(|_| Error::::MaxDelegatesStorageOverflow)?; + } + + DelegatesList::::insert(®istry_id, delegates); + + Self::deposit_event(Event::RegistryDelegateAdded { + delegator: delegator.clone(), + registry_id, + delegate, + permission, + }); + + Ok(()) + } + + /// Remove a Delegate from the Registry. + /// + /// This function allows an authorized user to remove a delegate from a registry. + /// The operation is subject to specific rules based on the permission level of the + /// user initiating the request and the delegate being removed. + /// + /// # Rules for Removing a Delegate: + /// + /// **Ownership Constraints:** + /// - **OWNER Cannot Be Removed:** The OWNER of a registry cannot be removed as a delegate. + /// This ensures the integrity and stability of the registry by maintaining a consistent + /// owner. + /// + /// **Authorization Requirements:** + /// - **Authorized Users:** Only users with OWNER or ADMIN permissions are allowed to + /// perform the removal operation. DELEGATE-level users do not have the authority to + /// remove any accounts. + /// - **Admin Removal Constraints:** An ADMIN cannot remove another ADMIN. This restriction + /// ensures that ADMIN users cannot interfere with each other's permissions. + /// + /// **Delegator-Delegate Relationship:** + /// - **Self-Removal Restriction:** The delegator (user initiating the removal) cannot be + /// the same as the delegate being removed. This prevents users from accidentally or + /// maliciously removing themselves. + /// + /// **Permission Enforcement:** + /// - **OWNER Authority:** The OWNER has the authority to remove any other permissioned + /// accounts (i.e., ADMINs or DELEGATEs). + /// - **ADMIN Restrictions:** ADMINs are restricted from removing other ADMINs, ensuring a + /// balanced distribution of power within the registry. + /// - **DELEGATE Limitations:** DELEGATE-level users cannot remove any accounts, regardless + /// of their permissions. + /// + /// # Arguments + /// * `origin` - The origin of the call, which should be a signed user. + /// * `registry_id` - The identifier of the registry from which the delegate should be + /// removed. + /// * `delegate` - The account identifier of the delegate to be removed. + /// + /// # Errors + /// Returns `Error::::RegistryIdDoesNotExist` if the registry identifier does not exist. + /// Returns `Error::::DelegatorCannotBeRemoved` if the delegator attempts to remove + /// themselves. Returns `Error::::DelegatesListNotFound` if the delegate list for the + /// given registry does not exist. + /// Returns `Error::::CannotRemoveRegistryOwnerAsDelegate` if an attempt is made to + /// remove the OWNER. Returns `Error::::AdminCannotRemoveAnotherAdmin` if an ADMIN + /// attempts to remove another ADMIN. Returns `Error::::DelegateCannotRemoveAccounts` + /// if a DELEGATE attempts to remove any account. Returns `Error::::DelegateNotFound` + /// if the delegate to be removed is not found in the registry. + /// + /// # Events + /// Emits `RegistryDelegateRemoved` when a delegate is successfully removed from the + /// registry. + /// + /// # Example + /// ``` + /// remove_delegate(origin, registry_id, delegate)?; + /// ``` + #[pallet::call_index(4)] + #[pallet::weight({0})] + pub fn remove_delegate( + origin: OriginFor, + registry_id: RegistryIdOf, + delegate: DelegateOf, + ) -> DispatchResult { + let delegator = ensure_signed(origin)?; + + /* Ensure RegistryId exists */ + let _registry = + Registries::::get(®istry_id).ok_or(Error::::RegistryIdDoesNotExist)?; + + ensure!(delegator != delegate, Error::::DelegatorCannotBeRemoved); + + /* Ensure that registry_entry is created from authorized account */ + let mut delegates = + DelegatesList::::get(®istry_id).ok_or(Error::::DelegatesListNotFound)?; + + let mut delegator_permission = None; + let mut delegate_index = None; + + for (i, entry) in delegates.entries.iter().enumerate() { + if entry.delegate == delegator { + delegator_permission = Some(entry.permissions); + } + if entry.delegate == delegate { + delegate_index = Some(i); + } + } + + /* Ensure the delegator has the required permissions of being either OWNER/ADMIN */ + ensure!( + delegator_permission.map_or(false, |perm| { + perm.intersects(Permissions::OWNER | Permissions::ADMIN) + }), + Error::::UnauthorizedOperation + ); + + /* Ensure that the delegate to be removed is found */ + if let Some(index) = delegate_index { + /* Ensure OWNER cannot be removed, assuming there exists one owner per registry */ + if delegates.entries[index].delegator.is_none() { + return Err(Error::::CannotRemoveRegistryOwnerAsDelegate.into()); + } + + /* Ensure that permissions are correctly enforced: + * OWNER can remove any other permissioned accounts. + * ADMIN cannot remove another ADMIN. + * DELEGATE cannot remove any accounts. + */ + match delegator_permission.unwrap() { + // OWNER can remove any accounts, no additional checks required + permission if permission.contains(Permissions::OWNER) => {}, + + permission if permission.contains(Permissions::ADMIN) => { + ensure!( + !delegates.entries[index].permissions.contains(Permissions::ADMIN), + Error::::AdminCannotRemoveAnotherAdmin + ); + }, + permission if permission.contains(Permissions::DELEGATE) => { + return Err(Error::::DelegateCannotRemoveAccounts.into()); + }, + _ => { + return Err(Error::::InvalidPermission.into()); + }, + } + + delegates.entries.remove(index); + DelegatesList::::insert(®istry_id, delegates); + + Self::deposit_event(Event::RegistryDelegateRemoved { + delegator: delegator.clone(), + registry_id, + delegate, + }); + } else { + return Err(Error::::DelegateNotFound.into()); + } + + Ok(()) + } + + /// Update the permissions of an existing Delegate. + /// + /// This function allows a user to submit a change of permission request for an + /// existing Delegate. The permission of the Registry is updated with the `new_permission` + /// which is part of the existing supported Permissions List. + /// + /// # Rules for Updating Permissions of a Delegate: + /// + /// **Existence of Delegate:** + /// - The delegate whose permission is to be updated must already exist in the registry. + /// + /// **Permission Constraints:** + /// - **No Ownership Updates:** Permissions of type OWNER cannot be assigned to any + /// delegate. This ensures that only one OWNER exists for a given registry. + /// - **Valid Permission Levels:** The new permission must be either ADMIN or DELEGATE. + /// Assigning OWNER is not permitted. + /// + /// **Authorization Requirements:** + /// - **Authorized Users:** The update operation can only be performed by users with OWNER + /// or ADMIN permissions. DELEGATE-level users are not authorized to perform this + /// operation. + /// - **Admin Downgrades:** If the new permission is DELEGATE, only an OWNER can downgrade + /// an ADMIN to DELEGATE. An ADMIN cannot perform this downgrade. + /// + /// **Delegator Restrictions:** + /// - **Same User Update:** The delegator cannot be the same as the delegate whose + /// permission is being updated. + /// - **Permission Upgrade:** ADMIN users are allowed to upgrade a delegate to ADMIN or + /// DELEGATE, but cannot downgrade an ADMIN to DELEGATE unless performed by an OWNER. + /// + /// # Arguments + /// * `origin` - The origin of the call, which should be a signed user. + /// * `registry_id` - The Registry Identifier associated with an existing Registry. + /// * `delegate` - The account for which the permission update should take place. + /// * `new_permission` - The `new_permission` to which the `delegate` should be updated. The + /// valid permissions are `ADMIN` and `DELEGATE`. + /// + /// # Errors + /// Returns `Error::::RegistryIdDoesNotExist` if the registry identifier does not exist. + /// Returns `Error::::DelegatorCannotBeUpdated` if the delegate and delegator are the + /// same. Returns `Error::::InvalidPermission` if the `new_permission` is not a valid + /// permission. Returns `Error::::DelegatesListNotFound` if the DelegateList does not + /// exist for the given RegistryId. + /// Returns `Error::::DelegateNotFound` if the given `delegate` does not exist. + /// Returns `Error::::UnauthorizedOperation` if the given operation is not valid. + /// Returns `Error::::CannotUpdateOwnerPermission` if the owner's permission is attempted + /// to be changed. + /// Returns `Error::::DelegateAlreadyExistsWithSamePermission` if the given `delegate` + /// already exists with the same `new_permission`. + /// + /// # Events + /// Emits `RegistryDelegatePermissionUpdated` when a Registry Delegate Permission is + /// updated. + /// + /// # Example + /// ``` + /// update_delegate_permission(origin, registry_id, delegate, new_permission)?; + /// ``` + #[pallet::call_index(5)] + #[pallet::weight({0})] + pub fn update_delegate_permission( + origin: OriginFor, + registry_id: RegistryIdOf, + delegate: DelegateOf, + new_permission: Permissions, + ) -> DispatchResult { + let delegator = ensure_signed(origin)?; + + /* Ensure RegistryId exists */ + let _registry = + Registries::::get(®istry_id).ok_or(Error::::RegistryIdDoesNotExist)?; + + ensure!(delegator != delegate, Error::::DelegatorCannotBeUpdated); + + /* Ensure that new_permission is not OWNER */ + ensure!( + matches!(new_permission, Permissions::ADMIN | Permissions::DELEGATE), + Error::::InvalidPermission + ); + + /* Ensure that registry_entry is created from authorized account */ + let mut delegates = + DelegatesList::::get(®istry_id).ok_or(Error::::DelegatesListNotFound)?; + + /* Ensure that the delegate already exists */ + let delegate_index = delegates + .entries + .iter() + .position(|d| d.delegate == delegate) + .ok_or(Error::::DelegateNotFound)?; + + /* Ensure the delegator is either an OWNER or an ADMIN */ + let delegator_permission = delegates + .entries + .iter() + .find(|d| d.delegate == delegator) + .map(|d| d.permissions) + .ok_or(Error::::UnauthorizedOperation)?; + + ensure!( + matches!(delegator_permission, Permissions::OWNER | Permissions::ADMIN), + Error::::UnauthorizedOperation + ); + + let delegate_entry = &delegates.entries[delegate_index]; + + /* Ensure that the delegate is not the OWNER */ + ensure!(!new_permission.contains(Permissions::OWNER), Error::::InvalidPermission); + + /* Ensure Delegate with same permission does not already exist */ + ensure!( + !delegate_entry.permissions.contains(new_permission), + Error::::DelegateAlreadyExistsWithSamePermission + ); + + /* Ensure only an OWNER can downgrade an ADMIN to DELEGATE */ + if new_permission.contains(Permissions::DELEGATE) { + if delegate_entry.permissions.contains(Permissions::ADMIN) && + !delegator_permission.contains(Permissions::OWNER) + { + return Err(Error::::UnauthorizedOperation.into()); + } + } + + delegates.entries[delegate_index].permissions = new_permission; + + DelegatesList::::insert(®istry_id, delegates); + + Self::deposit_event(Event::RegistryDelegatePermissionUpdated { + delegator: delegator.clone(), + registry_id, + delegate, + new_permission, + }); + + Ok(()) + } + } +} + +impl Pallet { + /// Method to check if the input identifier calculated from sdk + /// is actually a valid SS58 Identifier Format and of valid type `DeDir`. + pub fn is_valid_ss58_format(identifier: &Ss58Identifier) -> bool { + match identifier.get_type() { + Ok(id_type) => + if id_type == IdentifierType::DeDir { + log::debug!("The SS58 identifier is of type DeDir."); + true + } else { + log::debug!("The SS58 identifier is not of type DeDir."); + false + }, + Err(e) => { + log::debug!("Invalid SS58 identifier. Error: {:?}", e); + false + }, + } + } + + /// Method to check if there exists a valid delegate + pub fn is_valid_delegate( + delegates: &DelegateEntryOf, + creator: &OwnerOf, + required_permissions: Permissions, + ) -> bool { + for entry in delegates.iter() { + if entry.delegate == *creator { + log::debug!( + "Delegate: {:?}, Entry Permissions: {:?}, Required Permissions: {:?}", + entry.delegate, + entry.permissions.bits(), + required_permissions.bits() + ); + if required_permissions.contains(entry.permissions) { + return true; + } + } + } + false + } + + /// Method to check if blob matches the given digest + pub fn does_blob_matches_digest(blob: &RegistryBlobOf, digest: &RegistryHashOf) -> bool { + let blob_digest = ::Hashing::hash(&blob.encode()[..]); + + log::debug!("digest: {:?}, blob_digest: {:?}", *digest, blob_digest); + + blob_digest == *digest + } +} diff --git a/pallets/dedir/src/mock.rs b/pallets/dedir/src/mock.rs new file mode 100644 index 00000000..44a179bb --- /dev/null +++ b/pallets/dedir/src/mock.rs @@ -0,0 +1,101 @@ +// This file is part of CORD – https://cord.network + +// Copyright (C) Dhiway Networks Pvt. Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// CORD 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. + +// CORD 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 CORD. If not, see . + +use super::*; +use crate as pallet_dedir; +use cord_utilities::mock::{mock_origin, SubjectId}; +use frame_support::{derive_impl, parameter_types}; + +//use frame_system::EnsureRoot; +use sp_runtime::{ + traits::{IdentifyAccount, IdentityLookup, Verify}, + BuildStorage, MultiSignature, +}; + +type Signature = MultiSignature; +type AccountPublic = ::Signer; +pub type AccountId = ::AccountId; +pub(crate) type Block = frame_system::mocking::MockBlock; + +frame_support::construct_runtime!( + pub enum Test { + System: frame_system, + Identifier: identifier, + MockOrigin: mock_origin, + DeDir: pallet_dedir, + } +); + +parameter_types! { + pub const SS58Prefix: u8 = 29; +} + +#[derive_impl(frame_system::config_preludes::TestDefaultConfig)] +impl frame_system::Config for Test { + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + type Block = Block; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type SS58Prefix = SS58Prefix; +} + +impl mock_origin::Config for Test { + type RuntimeOrigin = RuntimeOrigin; + type AccountId = AccountId; + type SubjectId = SubjectId; +} + +parameter_types! { + pub const MaxRegistryDelegates: u32 = 25; + pub const MaxEncodedInputLength: u32 = 32; + pub const MaxRegistryBlobSize: u32 = 16 * 1024; // 16KB in bytes +} + +impl pallet_dedir::Config for Test { + type RuntimeEvent = RuntimeEvent; + type MaxRegistryBlobSize = MaxRegistryBlobSize; + type MaxRegistryDelegates = MaxRegistryDelegates; + type MaxEncodedInputLength = MaxEncodedInputLength; + type WeightInfo = (); +} + +parameter_types! { + pub const MaxEventsHistory: u32 = 6u32; +} + +impl identifier::Config for Test { + type MaxEventsHistory = MaxEventsHistory; +} + +parameter_types! { + storage SpaceEvents: u32 = 0; +} + +#[allow(dead_code)] +pub(crate) fn new_test_ext() -> sp_io::TestExternalities { + let t: sp_runtime::Storage = + frame_system::GenesisConfig::::default().build_storage().unwrap(); + let mut ext = sp_io::TestExternalities::new(t); + #[cfg(feature = "runtime-benchmarks")] + let keystore = sp_keystore::testing::MemoryKeystore::new(); + #[cfg(feature = "runtime-benchmarks")] + ext.register_extension(sp_keystore::KeystoreExt(sp_std::sync::Arc::new(keystore))); + ext.execute_with(|| System::set_block_number(1)); + ext +} diff --git a/pallets/dedir/src/tests.rs b/pallets/dedir/src/tests.rs new file mode 100644 index 00000000..faea93f7 --- /dev/null +++ b/pallets/dedir/src/tests.rs @@ -0,0 +1,649 @@ +// This file is part of CORD – https://cord.network + +// Copyright (C) Dhiway Networks Pvt. Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// CORD 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. + +// CORD 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 CORD. If not, see . + +use super::*; +use crate::mock::*; +use codec::Encode; +//use cord_utilities::mock::{mock_origin::DoubleOrigin, SubjectId}; +use frame_support::{assert_ok, BoundedVec}; +//use frame_system::RawOrigin; +use sp_runtime::traits::Hash; +use sp_std::prelude::*; +//use cord_utilities::mock::mock_origin::Origin; +use serde_json::json; + +/// Generates a Registry ID +pub fn generate_registry_id(id_digest: &RegistryHashOf) -> RegistryIdOf { + let registry_id: RegistryIdOf = + Ss58Identifier::create_identifier(&(id_digest).encode()[..], IdentifierType::DeDir) + .expect("Registry Identifier creation failed."); + + registry_id +} + +/// Generates a Registry Entry ID +pub fn generate_registry_entry_id(id_digest: &RegistryHashOf) -> RegistryEntryIdOf { + let registry_entry_id: RegistryEntryIdOf = + Ss58Identifier::create_identifier(&(id_digest).encode()[..], IdentifierType::DeDir) + .expect("Registry Entry Identifier creation failed"); + + registry_entry_id +} + +pub(crate) const ACCOUNT_00: AccountId = AccountId::new([1u8; 32]); +pub(crate) const ACCOUNT_01: AccountId = AccountId::new([2u8; 32]); + +#[test] +fn create_registry_should_work() { + new_test_ext().execute_with(|| { + let creator = ACCOUNT_00; + + let json_object = json!({ + "name": "String", + "age": "Number", + "email": "String", + "isActive": "Boolean", + "address": { + "street": "String", + "city": "String", + "zipcode": "Number" + }, + "phoneNumbers": [ + "Number", + "Number" + ], + }); + + let json_string = serde_json::to_string(&json_object).expect("Failed to serialize JSON"); + + let raw_bytes = json_string.as_bytes().to_vec(); + + let blob: RegistryBlobOf = BoundedVec::try_from(raw_bytes.clone()).expect( + "Test Blob should fit into the expected input length of BLOB for the test runtime.", + ); + + let digest: RegistryHashOf = + ::Hashing::hash(&raw_bytes.encode()[..]); + + let id_digest = ::Hashing::hash( + &[&creator.encode()[..], &digest.encode()[..]].concat()[..], + ); + + let registry_id: RegistryIdOf = generate_registry_id::(&id_digest); + + /* Test creation of a Registry */ + assert_ok!(DeDir::create_registry( + frame_system::RawOrigin::Signed(creator.clone()).into(), + registry_id.clone(), + digest, + Some(blob.clone()), + )); + + /* Check if the Registry was created */ + assert!(Registries::::contains_key(registry_id.clone())); + let registry = Registries::::get(registry_id.clone()).unwrap(); + + /* Check for values stored are correct */ + assert_eq!(registry.digest, digest); + assert_eq!(registry.blob, blob.clone()); + assert_eq!(registry.owner, creator); + + /* Check for Delegates */ + let delegates = DelegatesList::::get(registry_id.clone()); + assert!(delegates + .unwrap() + .entries + .iter() + .any(|d| d.delegate == creator && d.permissions == Permissions::OWNER)); + + /* Check for successfull event emission of CreatedRegistry */ + System::assert_last_event( + Event::CreatedRegistry { creator: creator.clone(), registry_id: registry_id.clone() } + .into(), + ); + }); +} + +#[test] +fn create_registry_entry_should_work() { + let creator = ACCOUNT_00; + + /* Assumed Json for Registry (schema) */ + let registry_json_object = json!({ + "name": "String", + "age": "Number", + "email": "String", + "isActive": "Boolean", + "address": { + "street": "String", + "city": "String", + "zipcode": "Number" + }, + "phoneNumbers": [ + "Number", + "Number" + ], + }); + + let registry_json_string = + serde_json::to_string(®istry_json_object).expect("Failed to serialize JSON"); + + let registry_raw_bytes = registry_json_string.as_bytes().to_vec(); + + let registry_blob: RegistryBlobOf = BoundedVec::try_from(registry_raw_bytes.clone()) + .expect( + "Test Blob should fit into the expected input length of BLOB for the test runtime.", + ); + + let registry_digest: RegistryHashOf = + ::Hashing::hash(®istry_raw_bytes.encode()[..]); + + let registry_id_digest = ::Hashing::hash( + &[&creator.encode()[..], ®istry_digest.encode()[..]].concat()[..], + ); + + let registry_id: RegistryIdOf = generate_registry_id::(®istry_id_digest); + + /* Assumed JSON for Registry Entry (record) */ + let registry_entry_json_object = json!({ + "name": "Alice", + "age": 25, + "email": "alice@dhiway.com", + "isActive": true, + "address": { + "street": "M.G ROAD", + "city": "Bengaluru", + "zipcode": "560001" + }, + "phoneNumbers": [ + "+91-234787324", + "+91-283746823" + ] + }); + + let registry_entry_json_string = + serde_json::to_string(®istry_entry_json_object).expect("Failed to serialize JSON"); + + let registry_entry_raw_bytes = registry_entry_json_string.as_bytes().to_vec(); + + let registry_entry_blob: RegistryBlobOf = BoundedVec::try_from( + registry_entry_raw_bytes.clone(), + ) + .expect("Test Blob should fit into the expected input length of BLOB for the test runtime."); + + let registry_entry_digest: RegistryHashOf = + ::Hashing::hash(®istry_entry_raw_bytes.encode()[..]); + + let registry_entry_id_digest = ::Hashing::hash( + &[&creator.encode()[..], ®istry_entry_digest.encode()[..]].concat()[..], + ); + + let registry_entry_id: RegistryIdOf = + generate_registry_entry_id::(®istry_entry_id_digest); + + new_test_ext().execute_with(|| { + /* Test creation of a Registry */ + assert_ok!(DeDir::create_registry( + frame_system::RawOrigin::Signed(creator.clone()).into(), + registry_id.clone(), + registry_digest, + Some(registry_blob.clone()), + )); + + /* Test creation of a Registry Entry */ + assert_ok!(DeDir::create_registry_entry( + frame_system::RawOrigin::Signed(creator.clone()).into(), + registry_id.clone(), + registry_entry_id.clone(), + registry_entry_digest, + Some(registry_entry_blob.clone()), + None, + )); + + /* Check if the registry entry was created */ + let registry_entry = + RegistryEntries::::get(registry_id.clone(), registry_entry_id.clone()); + assert!(registry_entry.is_some()); + + /* Check the stored values in TEST chain-state */ + let registry_entry = registry_entry.unwrap(); + assert_eq!(registry_entry.digest, registry_entry_digest); + assert_eq!(registry_entry.blob, registry_entry_blob); + assert_eq!(registry_entry.current_state, RegistrySupportedStateOf::DRAFT); + + /* Check for successfull event emission of CreatedRegistryEntry */ + System::assert_last_event( + Event::CreatedRegistryEntry { + creator: creator.clone(), + registry_id: registry_id.clone(), + registry_entry_id: registry_entry_id.clone(), + } + .into(), + ); + }); +} + +#[test] +fn registry_entry_state_updation_should_work() { + let creator = ACCOUNT_00; + + /* Assumed Json for Registry (schema) */ + let registry_json_object = json!({ + "name": "String", + "age": "Number", + "email": "String", + "isActive": "Boolean", + "address": { + "street": "String", + "city": "String", + "zipcode": "Number" + }, + "phoneNumbers": [ + "Number", + "Number" + ], + }); + + let registry_json_string = + serde_json::to_string(®istry_json_object).expect("Failed to serialize JSON"); + + let registry_raw_bytes = registry_json_string.as_bytes().to_vec(); + + let registry_blob: RegistryBlobOf = BoundedVec::try_from(registry_raw_bytes.clone()) + .expect( + "Test Blob should fit into the expected input length of BLOB for the test runtime.", + ); + + let registry_digest: RegistryHashOf = + ::Hashing::hash(®istry_raw_bytes.encode()[..]); + + let registry_id_digest = ::Hashing::hash( + &[&creator.encode()[..], ®istry_digest.encode()[..]].concat()[..], + ); + + let registry_id: RegistryIdOf = generate_registry_id::(®istry_id_digest); + + /* Assumed JSON for Registry Entry (record) */ + let registry_entry_json_object = json!({ + "name": "Alice", + "age": 25, + "email": "alice@dhiway.com", + "isActive": true, + "address": { + "street": "M.G ROAD", + "city": "Bengaluru", + "zipcode": "560001" + }, + "phoneNumbers": [ + "+91-234787324", + "+91-283746823" + ] + }); + + let registry_entry_json_string = + serde_json::to_string(®istry_entry_json_object).expect("Failed to serialize JSON"); + + let registry_entry_raw_bytes = registry_entry_json_string.as_bytes().to_vec(); + + let registry_entry_blob: RegistryBlobOf = BoundedVec::try_from( + registry_entry_raw_bytes.clone(), + ) + .expect("Test Blob should fit into the expected input length of BLOB for the test runtime."); + + let registry_entry_digest: RegistryHashOf = + ::Hashing::hash(®istry_entry_raw_bytes.encode()[..]); + + let registry_entry_id_digest = ::Hashing::hash( + &[&creator.encode()[..], ®istry_entry_digest.encode()[..]].concat()[..], + ); + + let registry_entry_id: RegistryIdOf = + generate_registry_entry_id::(®istry_entry_id_digest); + + let new_state = RegistrySupportedStateOf::ACTIVE; + + new_test_ext().execute_with(|| { + /* Test creation of a Registry */ + assert_ok!(DeDir::create_registry( + frame_system::RawOrigin::Signed(creator.clone()).into(), + registry_id.clone(), + registry_digest, + Some(registry_blob.clone()), + )); + + /* Test creation of a Registry Entry */ + assert_ok!(DeDir::create_registry_entry( + frame_system::RawOrigin::Signed(creator.clone()).into(), + registry_id.clone(), + registry_entry_id.clone(), + registry_entry_digest, + Some(registry_entry_blob.clone()), + None, + )); + + /* Check if the registry entry was created */ + let registry_entry = + RegistryEntries::::get(registry_id.clone(), registry_entry_id.clone()); + assert!(registry_entry.is_some()); + + /* Check the stored values in TEST chain-state */ + let registry_entry = registry_entry.unwrap(); + assert_eq!(registry_entry.digest, registry_entry_digest); + assert_eq!(registry_entry.blob, registry_entry_blob); + assert_eq!(registry_entry.current_state, RegistrySupportedStateOf::DRAFT); + + /* Test change of a Registry Entry State from DRAFT to ACTIVE */ + assert_ok!(DeDir::registry_entry_state_change( + frame_system::RawOrigin::Signed(creator.clone()).into(), + registry_id.clone(), + registry_entry_id.clone(), + new_state.clone(), + )); + + /* Test if state of Registry Entry has been updated to ACTIVE */ + let updated_registry_entry = + RegistryEntries::::get(registry_id.clone(), registry_entry_id.clone()); + assert!(updated_registry_entry.is_some()); + + let updated_registry_entry = updated_registry_entry.unwrap(); + assert_eq!(updated_registry_entry.current_state, new_state); + + /* Check for successfull event emission of CreatedRegistryEntry */ + System::assert_last_event( + Event::RegistryEntryStateChanged { + who: creator.clone(), + registry_id: registry_id.clone(), + registry_entry_id: registry_entry_id.clone(), + new_state: new_state.clone(), + } + .into(), + ); + }); +} + +#[test] +fn add_delegate_should_work() { + let creator = ACCOUNT_00; + + /* Assumed Json for Registry (schema) */ + let registry_json_object = json!({ + "name": "String", + "age": "Number", + "email": "String", + "isActive": "Boolean", + "address": { + "street": "String", + "city": "String", + "zipcode": "Number" + }, + "phoneNumbers": [ + "Number", + "Number" + ], + }); + + let registry_json_string = + serde_json::to_string(®istry_json_object).expect("Failed to serialize JSON"); + + let registry_raw_bytes = registry_json_string.as_bytes().to_vec(); + + let registry_blob: RegistryBlobOf = BoundedVec::try_from(registry_raw_bytes.clone()) + .expect( + "Test Blob should fit into the expected input length of BLOB for the test runtime.", + ); + + let registry_digest: RegistryHashOf = + ::Hashing::hash(®istry_raw_bytes.encode()[..]); + + let registry_id_digest = ::Hashing::hash( + &[&creator.encode()[..], ®istry_digest.encode()[..]].concat()[..], + ); + + let registry_id: RegistryIdOf = generate_registry_id::(®istry_id_digest); + + let delegator = creator.clone(); + + let delegate = ACCOUNT_01; + + let permission = Permissions::ADMIN; + + new_test_ext().execute_with(|| { + /* Test creation of a Registry */ + assert_ok!(DeDir::create_registry( + frame_system::RawOrigin::Signed(creator.clone()).into(), + registry_id.clone(), + registry_digest, + Some(registry_blob.clone()), + )); + + /* Test for addition of a delegate */ + assert_ok!(DeDir::add_delegate( + frame_system::RawOrigin::Signed(delegator.clone()).into(), + registry_id.clone(), + delegate.clone(), + permission, + )); + + /* Check if the delegate was added successfully */ + let delegates = DelegatesList::::get(registry_id.clone()); + assert!(delegates.is_some()); + + let delegates = delegates.unwrap(); + assert!(delegates + .entries + .iter() + .any(|d| d.delegate == delegate && d.permissions == permission)); + + /* Check for successfull event emission of RegistryDelegateAdded */ + System::assert_last_event( + Event::RegistryDelegateAdded { + delegator: delegator.clone(), + registry_id: registry_id.clone(), + delegate: delegate.clone(), + permission, + } + .into(), + ); + }); +} + +#[test] +fn remove_delegate_should_work() { + let creator = ACCOUNT_00; + + /* Assumed Json for Registry (schema) */ + let registry_json_object = json!({ + "name": "String", + "age": "Number", + "email": "String", + "isActive": "Boolean", + "address": { + "street": "String", + "city": "String", + "zipcode": "Number" + }, + "phoneNumbers": [ + "Number", + "Number" + ], + }); + + let registry_json_string = + serde_json::to_string(®istry_json_object).expect("Failed to serialize JSON"); + + let registry_raw_bytes = registry_json_string.as_bytes().to_vec(); + + let registry_blob: RegistryBlobOf = BoundedVec::try_from(registry_raw_bytes.clone()) + .expect( + "Test Blob should fit into the expected input length of BLOB for the test runtime.", + ); + + let registry_digest: RegistryHashOf = + ::Hashing::hash(®istry_raw_bytes.encode()[..]); + + let registry_id_digest = ::Hashing::hash( + &[&creator.encode()[..], ®istry_digest.encode()[..]].concat()[..], + ); + + let registry_id: RegistryIdOf = generate_registry_id::(®istry_id_digest); + + let delegator = creator.clone(); + + let delegate = ACCOUNT_01; + + let permission = Permissions::ADMIN; + + new_test_ext().execute_with(|| { + /* Test creation of a Registry */ + assert_ok!(DeDir::create_registry( + frame_system::RawOrigin::Signed(creator.clone()).into(), + registry_id.clone(), + registry_digest, + Some(registry_blob.clone()), + )); + + /* Test for addition of a delegate */ + assert_ok!(DeDir::add_delegate( + frame_system::RawOrigin::Signed(delegator.clone()).into(), + registry_id.clone(), + delegate.clone(), + permission, + )); + + /* Test for removal of delegate */ + assert_ok!(DeDir::remove_delegate( + frame_system::RawOrigin::Signed(delegator.clone()).into(), + registry_id.clone(), + delegate.clone(), + )); + + /* Check if the delegate was removed successfully */ + let delegates = DelegatesList::::get(registry_id.clone()); + assert!(delegates.is_some()); + let delegates = delegates.unwrap(); + + assert!(!delegates.entries.iter().any(|d| d.delegate == delegate)); + + /* Check for successfull event emission of RegistryDelegateRemoved */ + System::assert_last_event( + Event::RegistryDelegateRemoved { + delegator: delegator.clone(), + registry_id: registry_id.clone(), + delegate: delegate.clone(), + } + .into(), + ); + }); +} + +#[test] +fn update_delegate_permission_should_work() { + let creator = ACCOUNT_00; + + /* Assumed Json for Registry (schema) */ + let registry_json_object = json!({ + "name": "String", + "age": "Number", + "email": "String", + "isActive": "Boolean", + "address": { + "street": "String", + "city": "String", + "zipcode": "Number" + }, + "phoneNumbers": [ + "Number", + "Number" + ], + }); + + let registry_json_string = + serde_json::to_string(®istry_json_object).expect("Failed to serialize JSON"); + + let registry_raw_bytes = registry_json_string.as_bytes().to_vec(); + + let registry_blob: RegistryBlobOf = BoundedVec::try_from(registry_raw_bytes.clone()) + .expect( + "Test Blob should fit into the expected input length of BLOB for the test runtime.", + ); + + let registry_digest: RegistryHashOf = + ::Hashing::hash(®istry_raw_bytes.encode()[..]); + + let registry_id_digest = ::Hashing::hash( + &[&creator.encode()[..], ®istry_digest.encode()[..]].concat()[..], + ); + + let registry_id: RegistryIdOf = generate_registry_id::(®istry_id_digest); + + let delegator = creator.clone(); + + let delegate = ACCOUNT_01; + + let permission = Permissions::DELEGATE; + + let new_permission = Permissions::ADMIN; + + new_test_ext().execute_with(|| { + /* Test creation of a Registry */ + assert_ok!(DeDir::create_registry( + frame_system::RawOrigin::Signed(creator.clone()).into(), + registry_id.clone(), + registry_digest, + Some(registry_blob.clone()), + )); + + /* Test for addition of a delegate */ + assert_ok!(DeDir::add_delegate( + frame_system::RawOrigin::Signed(delegator.clone()).into(), + registry_id.clone(), + delegate.clone(), + permission, + )); + + /* Test for addition of a delegate */ + assert_ok!(DeDir::update_delegate_permission( + frame_system::RawOrigin::Signed(delegator.clone()).into(), + registry_id.clone(), + delegate.clone(), + new_permission, + )); + + /* Check if the delegate was added successfully */ + let delegates = DelegatesList::::get(registry_id.clone()); + assert!(delegates.is_some()); + + /* Check if the permission of the delegate was updated to ADMIN */ + let delegates = delegates.unwrap(); + assert!(delegates + .entries + .iter() + .any(|d| d.delegate == delegate && d.permissions == new_permission)); + + /* Check for successfull event emission of RegistryDelegateAdded */ + System::assert_last_event( + Event::RegistryDelegatePermissionUpdated { + delegator: delegator.clone(), + registry_id: registry_id.clone(), + delegate: delegate.clone(), + new_permission, + } + .into(), + ); + }); +} diff --git a/pallets/dedir/src/types.rs b/pallets/dedir/src/types.rs new file mode 100644 index 00000000..89b98bbd --- /dev/null +++ b/pallets/dedir/src/types.rs @@ -0,0 +1,104 @@ +// This file is part of CORD – https://cord.network + +// Copyright (C) Parity Technologies (UK) Ltd. +// Copyright (C) Dhiway Networks Pvt. Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later +// Adapted to meet the requirements of the CORD project. + +// CORD 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. + +// CORD 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 CORD. If not, see . + +//! DeDir pallet types. + +use codec::{Decode, Encode, MaxEncodedLen}; +use scale_info::TypeInfo; + +use sp_runtime::RuntimeDebug; +use sp_std::prelude::*; + +use bitflags::bitflags; + +#[derive(Encode, Decode, MaxEncodedLen, Clone, RuntimeDebug, PartialEq, Eq, TypeInfo)] +pub enum RegistrySupportedStateOf { + DRAFT, + ACTIVE, + REVOKED, +} + +impl RegistrySupportedStateOf { + pub fn is_valid_state(&self) -> bool { + matches!(self, Self::DRAFT | Self::ACTIVE | Self::REVOKED) + } +} + +#[derive(Encode, Decode, Clone, Default, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)] +pub struct Entry { + /// Type of Registry Key + pub registry_key: RegistryKeyIdOf, + /// Type of Registry Key Type + pub registry_key_type: RegistrySupportedTypeOf, +} + +#[derive(Encode, Decode, Clone, Default, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)] +pub struct Registry { + pub blob: RegistryBlobOf, + pub owner: OwnerOf, + pub digest: RegistryHashOf, +} + +#[derive(Encode, Decode, Clone, Default, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)] +pub struct RegistryEntry< + RegistryBlobOf, + RegistryEntryHashOf, + RegistryIdOf, + RegistrySupportedStateOf, +> { + /// Type of Entries + pub blob: RegistryBlobOf, + /// Type of Digest + pub digest: RegistryEntryHashOf, + /// Type of Registry Identifier + pub registry_id: RegistryIdOf, + /// Type of Current State of Registry Entry + pub current_state: RegistrySupportedStateOf, +} + +/* The `Permissions` enum defines the levels of access control available for an account within a + * registry. + * + * - `DELEGATE`: Grants permission to manage registry entries. + * - `ADMIN`: Extends `DELEGATE` permissions, allowing the management of delegates in addition to + * managing registry entries. + * - `OWNER`: The creator or owner of the registry. This permission level encompasses the full + * range of management capabilities, including the permissions of both `DELEGATE` and `ADMIN`. + */ +bitflags! { + #[derive(Encode, Decode, TypeInfo, MaxEncodedLen)] + pub struct Permissions: u32 { + const DELEGATE = 0b0000_0001; + const ADMIN = 0b0000_0010; + const OWNER = 0b0000_0100; + } +} + +#[derive(Encode, Decode, Clone, Default, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)] +pub struct DelegateInfo { + pub delegate: DelegateOf, + pub permissions: Permissions, + pub delegator: Option, +} + +#[derive(Encode, Decode, Clone, Default, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)] +pub struct Delegates { + pub entries: DelegateEntryOf, +} diff --git a/primitives/identifier/src/curi.rs b/primitives/identifier/src/curi.rs index 6d61daac..27f8c901 100644 --- a/primitives/identifier/src/curi.rs +++ b/primitives/identifier/src/curi.rs @@ -43,6 +43,7 @@ pub enum IdentifierType { Asset, AssetInstance, Rating, + DeDir, } impl IdentifierType { @@ -55,6 +56,7 @@ impl IdentifierType { const IDENT_ASSET: u16 = 2348; const IDENT_RATING: u16 = 6077; const IDENT_ASSET_INSTANCE: u16 = 11380; + const IDENT_DEDIR: u16 = 9274; fn ident_value(&self) -> u16 { match self { @@ -67,6 +69,7 @@ impl IdentifierType { IdentifierType::Asset => Self::IDENT_ASSET, IdentifierType::AssetInstance => Self::IDENT_ASSET_INSTANCE, IdentifierType::Rating => Self::IDENT_RATING, + IdentifierType::DeDir => Self::IDENT_DEDIR, } } fn from_u16(value: u16) -> Option { @@ -80,6 +83,7 @@ impl IdentifierType { 2348 => Some(IdentifierType::Asset), 6077 => Some(IdentifierType::AssetInstance), 11380 => Some(IdentifierType::Rating), + 9274 => Some(IdentifierType::DeDir), _ => None, } } diff --git a/runtimes/braid/Cargo.toml b/runtimes/braid/Cargo.toml index 7ad2d94f..bf8222eb 100644 --- a/runtimes/braid/Cargo.toml +++ b/runtimes/braid/Cargo.toml @@ -51,6 +51,7 @@ pallet-offences = { workspace = true } pallet-node-authorization = { workspace = true } pallet-network-score = { workspace = true } pallet-session-benchmarking = { workspace = true } +pallet-dedir = { workspace = true } # Internal runtime API (with default disabled) pallet-did-runtime-api = { workspace = true } @@ -165,6 +166,7 @@ std = [ "pallet-schema/std", "pallet-chain-space/std", "pallet-statement/std", + "pallet-dedir/std", "pallet-network-score/std", "pallet-network-membership/std", "pallet-runtime-upgrade/std", @@ -263,6 +265,7 @@ try-runtime = [ "pallet-network-membership/try-runtime", "pallet-runtime-upgrade/try-runtime", "pallet-remark/try-runtime", + "pallet-dedir/try-runtime", "cord-runtime-common/try-runtime", "authority-membership/try-runtime", "identifier/try-runtime", diff --git a/runtimes/braid/src/lib.rs b/runtimes/braid/src/lib.rs index 48a60153..8bfd3308 100644 --- a/runtimes/braid/src/lib.rs +++ b/runtimes/braid/src/lib.rs @@ -468,6 +468,20 @@ impl pallet_identity::Config for Runtime { type WeightInfo = pallet_identity::weights::SubstrateWeight; } +parameter_types! { + pub const MaxRegistryDelegates: u32 = 25; + pub const MaxEncodedInputLength: u32 = 32; + pub const MaxRegistryBlobSize: u32 = 16 * 1024; // 16KB in bytes +} + +impl pallet_dedir::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type MaxRegistryBlobSize = MaxRegistryBlobSize; + type MaxRegistryDelegates = MaxRegistryDelegates; + type MaxEncodedInputLength = MaxEncodedInputLength; + type WeightInfo = (); +} + impl pallet_offences::Config for Runtime { type RuntimeEvent = RuntimeEvent; type IdentificationTuple = pallet_session::historical::IdentificationTuple; @@ -957,6 +971,9 @@ mod runtime { #[runtime::pallet_index(60)] pub type NetworkParameters = pallet_config; + #[runtime::pallet_index(61)] + pub type DeDir = pallet_dedir; + #[runtime::pallet_index(255)] pub type Sudo = pallet_sudo; } diff --git a/runtimes/loom/Cargo.toml b/runtimes/loom/Cargo.toml index 9a6a4141..4845d9bd 100644 --- a/runtimes/loom/Cargo.toml +++ b/runtimes/loom/Cargo.toml @@ -52,6 +52,7 @@ pallet-offences = { workspace = true } pallet-node-authorization = { workspace = true } pallet-network-score = { workspace = true } pallet-session-benchmarking = { workspace = true } +pallet-dedir = { workspace = true } # Internal runtime API (with default disabled) pallet-did-runtime-api = { workspace = true } @@ -180,6 +181,7 @@ std = [ "pallet-assets-runtime-api/std", "pallet-did-runtime-api/std", "pallet-node-authorization/std", + "pallet-dedir/std", "pallet-transaction-weight-runtime-api/std", "sp-runtime/std", "sp-staking/std", @@ -280,6 +282,7 @@ try-runtime = [ "pallet-network-membership/try-runtime", "pallet-runtime-upgrade/try-runtime", "pallet-remark/try-runtime", + "pallet-dedir/try-runtime", "cord-runtime-common/try-runtime", "authority-membership/try-runtime", "identifier/try-runtime", diff --git a/runtimes/loom/src/lib.rs b/runtimes/loom/src/lib.rs index 8493b4b5..8a76ca18 100644 --- a/runtimes/loom/src/lib.rs +++ b/runtimes/loom/src/lib.rs @@ -504,6 +504,20 @@ impl pallet_identity::Config for Runtime { type WeightInfo = pallet_identity::weights::SubstrateWeight; } +parameter_types! { + pub const MaxRegistryDelegates: u32 = 25; + pub const MaxEncodedInputLength: u32 = 32; + pub const MaxRegistryBlobSize: u32 = 16 * 1024; // 16KB in bytes +} + +impl pallet_dedir::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type MaxRegistryBlobSize = MaxRegistryBlobSize; + type MaxRegistryDelegates = MaxRegistryDelegates; + type MaxEncodedInputLength = MaxEncodedInputLength; + type WeightInfo = (); +} + parameter_types! { pub MotionDuration: BlockNumber = prod_or_fast!(3 * DAYS, 2 * MINUTES, "CORD_MOTION_DURATION"); pub const MaxProposals: u32 = 100; @@ -1121,6 +1135,9 @@ mod runtime { #[runtime::pallet_index(60)] pub type NetworkParameters = pallet_config; + #[runtime::pallet_index(61)] + pub type DeDir = pallet_dedir; + #[runtime::pallet_index(254)] pub type RootTesting = pallet_root_testing; diff --git a/runtimes/weave/Cargo.toml b/runtimes/weave/Cargo.toml index 09414223..bfb6f0a2 100644 --- a/runtimes/weave/Cargo.toml +++ b/runtimes/weave/Cargo.toml @@ -52,6 +52,7 @@ pallet-offences = { workspace = true } pallet-node-authorization = { workspace = true } pallet-network-score = { workspace = true } pallet-session-benchmarking = { workspace = true } +pallet-dedir = { workspace = true } # Internal runtime API (with default disabled) pallet-did-runtime-api = { workspace = true } @@ -196,6 +197,7 @@ std = [ "frame-try-runtime?/std", "primitive-types/std", "sp-io/std", + "pallet-dedir/std", ] runtime-benchmarks = [ "frame-benchmarking/runtime-benchmarks", @@ -285,6 +287,7 @@ try-runtime = [ "frame-try-runtime/try-runtime", "pallet-config/try-runtime", "sp-runtime/try-runtime", + "pallet-dedir/try-runtime", ] # Set timing constants (e.g. session period) to faster versions to speed up testing. diff --git a/runtimes/weave/src/lib.rs b/runtimes/weave/src/lib.rs index 82dde1a8..77da76c8 100644 --- a/runtimes/weave/src/lib.rs +++ b/runtimes/weave/src/lib.rs @@ -783,6 +783,20 @@ impl pallet_network_membership::Config for Runtime { type WeightInfo = weights::pallet_network_membership::WeightInfo; } +parameter_types! { + pub const MaxRegistryDelegates: u32 = 25; + pub const MaxEncodedInputLength: u32 = 32; + pub const MaxRegistryBlobSize: u32 = 16 * 1024; // 16KB in bytes +} + +impl pallet_dedir::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type MaxRegistryBlobSize = MaxRegistryBlobSize; + type MaxRegistryDelegates = MaxRegistryDelegates; + type MaxEncodedInputLength = MaxEncodedInputLength; + type WeightInfo = (); +} + impl identifier::Config for Runtime { type MaxEventsHistory = MaxEventsHistory; } @@ -1118,6 +1132,9 @@ mod runtime { #[runtime::pallet_index(60)] pub type NetworkParameters = pallet_config; + #[runtime::pallet_index(61)] + pub type DeDir = pallet_dedir; + #[runtime::pallet_index(255)] pub type Sudo = pallet_sudo; }