diff --git a/pallets/jobs/src/lib.rs b/pallets/jobs/src/lib.rs index 235711395..1d8f58051 100644 --- a/pallets/jobs/src/lib.rs +++ b/pallets/jobs/src/lib.rs @@ -32,7 +32,7 @@ use sp_std::{prelude::*, vec::Vec}; use tangle_primitives::{ jobs::{JobId, JobInfo, JobKey, PhaseOneResult, ValidatorOffence}, traits::{ - jobs::{JobResultVerifier, JobToFee}, + jobs::{JobToFee, MPCHandler}, roles::RolesHandler, }, }; @@ -71,11 +71,7 @@ pub mod module { type RolesHandler: RolesHandler; /// The job result verifying mechanism - type JobResultVerifier: JobResultVerifier< - Self::AccountId, - BlockNumberFor, - BalanceOf, - >; + type MPCHandler: MPCHandler, BalanceOf>; /// The origin which may set filter. type ForceOrigin: EnsureOrigin; @@ -210,7 +206,7 @@ pub mod module { for participant in participants { ensure!( - T::RolesHandler::is_validator(participant.clone(), job_key.clone().into()), + T::RolesHandler::is_validator(participant.clone(), job_key.clone()), Error::::InvalidValidator ); @@ -238,7 +234,7 @@ pub mod module { // Ensure the phase one participants are still validators for participant in result.participants { ensure!( - T::RolesHandler::is_validator(participant.clone(), job_key.clone().into()), + T::RolesHandler::is_validator(participant.clone(), job_key.clone()), Error::::InvalidValidator ); @@ -341,7 +337,7 @@ pub mod module { }; // Validate the result - T::JobResultVerifier::verify(&job_info, phase1_result.clone(), result.clone())?; + T::MPCHandler::verify(&job_info, phase1_result.clone(), result.clone())?; // If phase 1, store in known result if job_info.job_type.is_phase_one() { @@ -486,11 +482,7 @@ pub mod module { ensure!(participants.contains(&validator), Error::::JobNotFound); // Validate the result - T::JobResultVerifier::verify_validator_report( - validator.clone(), - offence.clone(), - signatures, - )?; + T::MPCHandler::verify_validator_report(validator.clone(), offence.clone(), signatures)?; // Slash the validator T::RolesHandler::slash_validator(validator.clone(), offence)?; diff --git a/pallets/jobs/src/mock.rs b/pallets/jobs/src/mock.rs index 358e1e9c5..64e201e8d 100644 --- a/pallets/jobs/src/mock.rs +++ b/pallets/jobs/src/mock.rs @@ -28,7 +28,7 @@ pub type AccountId = u128; pub type Balance = u128; pub type BlockNumber = u64; -use tangle_primitives::{jobs::*, roles::RoleType}; +use tangle_primitives::jobs::*; impl frame_system::Config for Runtime { type RuntimeOrigin = RuntimeOrigin; @@ -128,7 +128,7 @@ impl JobToFee for MockJobToFeeHandler { pub struct MockRolesHandler; impl RolesHandler for MockRolesHandler { - fn is_validator(address: AccountId, _role_type: RoleType) -> bool { + fn is_validator(address: AccountId, _role_type: JobKey) -> bool { let validators = [1, 2, 3, 4, 5]; validators.contains(&address) } @@ -138,9 +138,9 @@ impl RolesHandler for MockRolesHandler { } } -pub struct MockJobResultVerifier; +pub struct MockMPCHandler; -impl JobResultVerifier for MockJobResultVerifier { +impl MPCHandler for MockMPCHandler { fn verify( job: &JobInfo, phase_one_data: Option>, @@ -161,6 +161,10 @@ impl JobResultVerifier for MockJobResultVerifie ) -> DispatchResult { Ok(()) } + + fn validate_authority_key(_validator: AccountId, _authority_key: Vec) -> DispatchResult { + Ok(()) + } } parameter_types! { @@ -173,7 +177,7 @@ impl Config for Runtime { type Currency = Balances; type JobToFee = MockJobToFeeHandler; type RolesHandler = MockRolesHandler; - type JobResultVerifier = MockJobResultVerifier; + type MPCHandler = MockMPCHandler; type PalletId = JobsPalletId; type WeightInfo = (); } diff --git a/pallets/roles/src/impls.rs b/pallets/roles/src/impls.rs index cb6d23b2f..c64a1bf35 100644 --- a/pallets/roles/src/impls.rs +++ b/pallets/roles/src/impls.rs @@ -15,8 +15,9 @@ // along with Tangle. If not, see . use super::*; -use sp_runtime::{DispatchResult, Percent, Saturating}; -use tangle_primitives::{roles::RoleType, traits::roles::RolesHandler}; +use frame_support::pallet_prelude::DispatchResult; +use sp_runtime::{Percent, Saturating}; +use tangle_primitives::{jobs::JobKey, traits::roles::RolesHandler}; /// Implements RolesHandler for the pallet. impl RolesHandler for Pallet { @@ -24,12 +25,14 @@ impl RolesHandler for Pallet { /// /// # Parameters /// - `address`: The account ID of the validator. - /// - `role`: The key representing the type of job. + /// - `job`: The key representing the type of job. /// /// # Returns /// Returns `true` if the validator is permitted to work with this job type, otherwise `false`. - fn is_validator(address: T::AccountId, role: RoleType) -> bool { - Self::has_role(address, role) + fn is_validator(address: T::AccountId, job_key: JobKey) -> bool { + let assigned_roles = AccountRolesMapping::::get(address); + let job_role = job_key.get_role_type(); + assigned_roles.contains(&job_role) } /// Slash validator stake for the reported offence. The function should be a best effort diff --git a/pallets/roles/src/lib.rs b/pallets/roles/src/lib.rs index d33d35c60..826b4f51d 100644 --- a/pallets/roles/src/lib.rs +++ b/pallets/roles/src/lib.rs @@ -30,7 +30,10 @@ use parity_scale_codec::{Decode, Encode}; use scale_info::TypeInfo; use sp_runtime::{codec, traits::Zero, Saturating}; use sp_std::{convert::TryInto, prelude::*, vec}; -use tangle_primitives::roles::RoleType; +use tangle_primitives::{ + roles::{RoleType, RoleTypeMetadata}, + traits::jobs::JobsHandler, +}; mod impls; #[cfg(test)] pub(crate) mod mock; @@ -66,8 +69,8 @@ pub struct RoleStakingLedger { #[derive(PartialEqNoBound, EqNoBound, Encode, Decode, RuntimeDebugNoBound, TypeInfo, Clone)] #[scale_info(skip_type_params(T))] pub struct RoleStakingRecord { - /// Role type - pub role: RoleType, + /// Metadata associated with the role. + pub metadata: RoleTypeMetadata, /// The total amount of the stash's balance that is re-staked for selected role. #[codec(compact)] pub re_staked: BalanceOf, @@ -94,6 +97,10 @@ pub mod pallet { use super::*; use frame_support::pallet_prelude::*; use frame_system::pallet_prelude::*; + use tangle_primitives::{ + jobs::{JobId, JobKey}, + traits::jobs::MPCHandler, + }; #[pallet::pallet] #[pallet::without_storage_info] @@ -105,9 +112,16 @@ pub mod pallet { /// The overarching event type. type RuntimeEvent: From> + IsType<::RuntimeEvent>; + /// The job manager mechanism. + type JobsHandler: JobsHandler; + + /// Max roles per account. #[pallet::constant] type MaxRolesPerAccount: Get; + /// The config that verifies MPC related functions + type MPCHandler: MPCHandler, BalanceOf>; + type WeightInfo: WeightInfo; } @@ -120,6 +134,8 @@ pub mod pallet { RoleRemoved { account: T::AccountId, role: RoleType }, /// Slashed validator. Slashed { account: T::AccountId, amount: BalanceOf }, + /// Pending jobs,that cannot be opted out at the moment. + PendingJobs { pending_jobs: Vec<(JobKey, JobId)> }, } #[pallet::error] @@ -140,6 +156,8 @@ pub mod pallet { AccountAlreadyPaired, /// Stash controller account not found in Roles Ledger. AccountNotPaired, + /// Role clear request failed due to pending jobs, which can't be opted out at the moment. + RoleClearRequestFailed, } /// Map from all "controller" accounts to the info regarding the staking. @@ -200,10 +218,19 @@ pub mod pallet { // Validate role staking records. for record in records.clone() { - let role = record.role; + let role = record.metadata.get_role_type(); let re_stake_amount = record.re_staked; // Check if role is already assigned. - ensure!(!Self::has_role(stash_account.clone(), role), Error::::HasRoleAssigned); + ensure!( + !Self::has_role(stash_account.clone(), role.clone()), + Error::::HasRoleAssigned + ); + + // validate the metadata + T::MPCHandler::validate_authority_key( + stash_account.clone(), + record.metadata.get_authority_key(), + )?; // Re-staking amount of record should meet min re-staking amount requirement. let min_re_staking_bond = MinReStakingBond::::get(); @@ -219,16 +246,16 @@ pub mod pallet { ); ledger.total = ledger.total.saturating_add(re_stake_amount); - let role_info = RoleStakingRecord { role, re_staked: re_stake_amount }; - ledger.roles.push(role_info); + ledger.roles.push(record); } // Now that records are validated we can add them and update ledger for record in records { - Self::add_role(stash_account.clone(), record.role)?; + let role = record.metadata.get_role_type(); + Self::add_role(stash_account.clone(), role.clone())?; Self::deposit_event(Event::::RoleAssigned { account: stash_account.clone(), - role: record.role, + role, }); } Self::update_ledger(&stash_account, &ledger); @@ -258,14 +285,41 @@ pub mod pallet { ); // check if role is assigned. - ensure!(Self::has_role(stash_account.clone(), role), Error::::NoRoleAssigned); + ensure!( + Self::has_role(stash_account.clone(), role.clone()), + Error::::NoRoleAssigned + ); + + // Get active jobs for the role. + let active_jobs = T::JobsHandler::get_active_jobs(stash_account.clone()); + let mut role_cleared = true; + let mut pending_jobs = Vec::new(); + for job in active_jobs { + let job_key = job.0; + if job_key.get_role_type() == role { + // Submit request to exit from the known set. + let res = T::JobsHandler::exit_from_known_set( + stash_account.clone(), + job_key.clone(), + job.1, + ); + + if res.is_err() { + role_cleared = false; + pending_jobs.push((job_key.clone(), job.1)); + } + } + } - // TODO: Call jobs manager to remove the services. - // On successful removal of services, remove the role from the mapping. - // Issue link for reference : https://github.com/webb-tools/tangle/issues/292 + if !role_cleared { + // Role clear request failed due to pending jobs, which can't be opted out at the + // moment. + Self::deposit_event(Event::::PendingJobs { pending_jobs }); + return Err(Error::::RoleClearRequestFailed.into()) + }; // Remove role from the mapping. - Self::remove_role(stash_account.clone(), role)?; + Self::remove_role(stash_account.clone(), role.clone())?; // Remove stash account related info. Self::kill_stash(&stash_account); diff --git a/pallets/roles/src/mock.rs b/pallets/roles/src/mock.rs index 8ac61a502..760badba1 100644 --- a/pallets/roles/src/mock.rs +++ b/pallets/roles/src/mock.rs @@ -27,10 +27,13 @@ use sp_core::H256; use sp_runtime::{ testing::{Header, UintAuthorityId}, traits::IdentityLookup, - BuildStorage, Perbill, + BuildStorage, DispatchResult, Perbill, }; +use tangle_primitives::{jobs::*, traits::jobs::MPCHandler}; + pub type AccountId = u64; pub type Balance = u128; +pub type BlockNumber = u64; impl frame_system::Config for Runtime { type RuntimeOrigin = RuntimeOrigin; @@ -74,6 +77,30 @@ impl pallet_balances::Config for Runtime { type MaxFreezes = (); } +pub struct MockMPCHandler; + +impl MPCHandler for MockMPCHandler { + fn verify( + _job: &JobInfo, + _phase_one_data: Option>, + _result: Vec, + ) -> DispatchResult { + Ok(()) + } + + fn verify_validator_report( + _validator: AccountId, + _offence: ValidatorOffence, + _report: Vec, + ) -> DispatchResult { + Ok(()) + } + + fn validate_authority_key(_validator: AccountId, _authority_key: Vec) -> DispatchResult { + Ok(()) + } +} + impl pallet_timestamp::Config for Runtime { type Moment = u64; type OnTimestampSet = (); @@ -200,9 +227,27 @@ impl pallet_staking::Config for Runtime { type WeightInfo = (); } +pub struct MockJobsHandler; + +impl JobsHandler for MockJobsHandler { + fn get_active_jobs(_validator: AccountId) -> Vec<(JobKey, JobId)> { + Default::default() + } + + fn exit_from_known_set( + _validator: AccountId, + _job_key: JobKey, + _job_id: JobId, + ) -> sp_runtime::DispatchResult { + Ok(()) + } +} + impl Config for Runtime { type RuntimeEvent = RuntimeEvent; + type JobsHandler = MockJobsHandler; type MaxRolesPerAccount = ConstU32<2>; + type MPCHandler = MockMPCHandler; type WeightInfo = (); } diff --git a/pallets/roles/src/tests.rs b/pallets/roles/src/tests.rs index efdde5155..8dbfe0b2d 100644 --- a/pallets/roles/src/tests.rs +++ b/pallets/roles/src/tests.rs @@ -24,9 +24,12 @@ fn test_assign_roles() { // Initially account if funded with 10000 tokens and we are trying to bond 5000 tokens // Roles user is interested in re-staking. - let role_records = vec![RoleStakingRecord { role: RoleType::Tss, re_staked: 5000 }]; + let role_records = vec![RoleStakingRecord { + metadata: RoleTypeMetadata::Tss(Default::default()), + re_staked: 5000, + }]; - assert_ok!(Roles::assign_roles(RuntimeOrigin::signed(1), role_records)); + assert_ok!(Roles::assign_roles(RuntimeOrigin::signed(1), role_records.clone())); assert_events(vec![RuntimeEvent::Roles(crate::Event::RoleAssigned { account: 1, @@ -38,11 +41,7 @@ fn test_assign_roles() { // Verify ledger mapping assert_eq!( Roles::ledger(1), - Some(RoleStakingLedger { - stash: 1, - total: 5000, - roles: vec![RoleStakingRecord { role: RoleType::Tss, re_staked: 5000 }] - }) + Some(RoleStakingLedger { stash: 1, total: 5000, roles: role_records }) ); }); } @@ -55,11 +54,17 @@ fn test_assign_multiple_roles() { // Roles user is interested in re-staking. let role_records = vec![ - RoleStakingRecord { role: RoleType::Tss, re_staked: 2500 }, - RoleStakingRecord { role: RoleType::ZkSaas, re_staked: 2500 }, + RoleStakingRecord { + metadata: RoleTypeMetadata::Tss(Default::default()), + re_staked: 2500, + }, + RoleStakingRecord { + metadata: RoleTypeMetadata::ZkSaas(Default::default()), + re_staked: 2500, + }, ]; - assert_ok!(Roles::assign_roles(RuntimeOrigin::signed(1), role_records)); + assert_ok!(Roles::assign_roles(RuntimeOrigin::signed(1), role_records.clone())); // Lets verify role assigned to account. assert_eq!(Roles::has_role(1, RoleType::Tss), true); @@ -69,14 +74,7 @@ fn test_assign_multiple_roles() { assert_eq!( Roles::ledger(1), - Some(RoleStakingLedger { - stash: 1, - total: 5000, - roles: vec![ - RoleStakingRecord { role: RoleType::Tss, re_staked: 2500 }, - RoleStakingRecord { role: RoleType::ZkSaas, re_staked: 2500 }, - ] - }) + Some(RoleStakingLedger { stash: 1, total: 5000, roles: role_records }) ); }); } @@ -90,8 +88,14 @@ fn test_assign_roles_should_fail_if_total_re_stake_value_exceeds_max_re_stake_va // Roles user is interested in re-staking. let role_records = vec![ - RoleStakingRecord { role: RoleType::Tss, re_staked: 5000 }, - RoleStakingRecord { role: RoleType::ZkSaas, re_staked: 5000 }, + RoleStakingRecord { + metadata: RoleTypeMetadata::Tss(Default::default()), + re_staked: 5000, + }, + RoleStakingRecord { + metadata: RoleTypeMetadata::ZkSaas(Default::default()), + re_staked: 5000, + }, ]; // Since max re_stake limit is 5000 it should fail with `ExceedsMaxReStakeValue` error. assert_err!( @@ -107,7 +111,10 @@ fn test_clear_role() { // Initially account if funded with 10000 tokens and we are trying to bond 5000 tokens // Roles user is interested in re-staking. - let role_records = vec![RoleStakingRecord { role: RoleType::Tss, re_staked: 5000 }]; + let role_records = vec![RoleStakingRecord { + metadata: RoleTypeMetadata::Tss(Default::default()), + re_staked: 5000, + }]; assert_ok!(Roles::assign_roles(RuntimeOrigin::signed(1), role_records)); @@ -133,7 +140,10 @@ fn test_assign_roles_should_fail_if_not_validator() { // we will use account 5 which is not a validator // Roles user is interested in re-staking. - let role_records = vec![RoleStakingRecord { role: RoleType::Tss, re_staked: 5000 }]; + let role_records = vec![RoleStakingRecord { + metadata: RoleTypeMetadata::Tss(Default::default()), + re_staked: 5000, + }]; assert_err!( Roles::assign_roles(RuntimeOrigin::signed(5), role_records), @@ -149,7 +159,10 @@ fn test_unbound_funds_should_work() { // for providing TSS services. // Roles user is interested in re-staking. - let role_records = vec![RoleStakingRecord { role: RoleType::Tss, re_staked: 5000 }]; + let role_records = vec![RoleStakingRecord { + metadata: RoleTypeMetadata::Tss(Default::default()), + re_staked: 5000, + }]; assert_ok!(Roles::assign_roles(RuntimeOrigin::signed(1), role_records)); @@ -185,7 +198,10 @@ fn test_unbound_funds_should_fail_if_role_assigned() { // for providing TSS services. // Roles user is interested in re-staking. - let role_records = vec![RoleStakingRecord { role: RoleType::Tss, re_staked: 5000 }]; + let role_records = vec![RoleStakingRecord { + metadata: RoleTypeMetadata::Tss(Default::default()), + re_staked: 5000, + }]; assert_ok!(Roles::assign_roles(RuntimeOrigin::signed(1), role_records)); diff --git a/primitives/src/traits/jobs.rs b/primitives/src/traits/jobs.rs index a7a335a03..208409bd9 100644 --- a/primitives/src/traits/jobs.rs +++ b/primitives/src/traits/jobs.rs @@ -38,7 +38,7 @@ pub trait JobToFee { } /// A trait that describes the job result verification. -pub trait JobResultVerifier { +pub trait MPCHandler { /// Verifies the result of a job. /// /// # Parameters @@ -57,11 +57,32 @@ pub trait JobResultVerifier { result: Vec, ) -> DispatchResult; + // Verify a validator report + /// + /// This function is responsible for verifying a report against a specific validator's + /// offence and taking appropriate actions based on the report. + /// + /// # Arguments + /// + /// - `validator`: The account ID of the validator being reported. + /// - `offence`: Details of the offence reported against the validator. + /// - `report`: The report data provided by the reporting entity. fn verify_validator_report( validator: AccountId, offence: ValidatorOffence, report: Vec, ) -> DispatchResult; + + /// Validate the authority key associated with a specific validator. + /// + /// This function is responsible for validating the authority key associated with a given + /// validator. + /// + /// # Arguments + /// + /// - `validator`: The account ID of the validator whose authority key is to be validated. + /// - `authority_key`: The authority key to be validated. + fn validate_authority_key(validator: AccountId, authority_key: Vec) -> DispatchResult; } /// A trait that handles various aspects of jobs for a validator. diff --git a/primitives/src/traits/roles.rs b/primitives/src/traits/roles.rs index 05823416e..77f5dc041 100644 --- a/primitives/src/traits/roles.rs +++ b/primitives/src/traits/roles.rs @@ -14,7 +14,7 @@ // You should have received a copy of the GNU General Public License // along with Tangle. If not, see . -use crate::{jobs::ValidatorOffence, roles::RoleType}; +use crate::jobs::{JobKey, ValidatorOffence}; use sp_runtime::DispatchResult; /// A trait that handles roles associated with job types. @@ -24,12 +24,12 @@ pub trait RolesHandler { /// # Parameters /// /// - `address`: The account ID of the validator. - /// - `role_type`: The type of role + /// - `job_key`: The type of job /// /// # Returns /// /// Returns `true` if the validator is permitted to work with this job type, otherwise `false`. - fn is_validator(address: AccountId, role_type: RoleType) -> bool; + fn is_validator(address: AccountId, job_key: JobKey) -> bool; /// Slash validator stake for the reported offence. The function should be a best effort /// slashing, slash upto max possible by the offence type. diff --git a/primitives/src/types/jobs.rs b/primitives/src/types/jobs.rs index a7af7faae..7e0524fa2 100644 --- a/primitives/src/types/jobs.rs +++ b/primitives/src/types/jobs.rs @@ -17,6 +17,8 @@ use frame_support::{dispatch::Vec, pallet_prelude::*, RuntimeDebug}; #[cfg(feature = "std")] use serde::{Deserialize, Serialize}; +use crate::roles::RoleType; + pub type JobId = u32; /// Represents a job submission with specified `AccountId` and `BlockNumber`. @@ -213,6 +215,18 @@ pub enum JobKey { ZkSaasPhaseTwo, } +impl JobKey { + /// Returns role assigned with the job. + pub fn get_role_type(&self) -> RoleType { + match self { + JobKey::DKG => RoleType::Tss, + JobKey::DKGSignature => RoleType::Tss, + JobKey::ZkSaasPhaseOne => RoleType::ZkSaas, + JobKey::ZkSaasPhaseTwo => RoleType::ZkSaas, + } + } +} + /// Represents a job submission with specified `AccountId` and `BlockNumber`. #[derive(PartialEq, Eq, Encode, Decode, RuntimeDebug, TypeInfo, Clone)] pub struct PhaseOneResult { diff --git a/primitives/src/types/roles.rs b/primitives/src/types/roles.rs index e69212299..f1d935757 100644 --- a/primitives/src/types/roles.rs +++ b/primitives/src/types/roles.rs @@ -13,12 +13,10 @@ // // You should have received a copy of the GNU General Public License // along with Tangle. If not, see . - -use crate::jobs::JobKey; -use frame_support::pallet_prelude::*; +use frame_support::{dispatch::Vec, pallet_prelude::*}; /// Role type to be used in the system. -#[derive(Encode, Decode, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)] +#[derive(Encode, Decode, Clone, Debug, PartialEq, Eq, TypeInfo)] pub enum RoleType { Tss, ZkSaas, @@ -26,25 +24,53 @@ pub enum RoleType { impl RoleType { /// Checks if the role type is a TSS role. - pub fn is_tss(self) -> bool { - self == RoleType::Tss + pub fn is_tss(&self) -> bool { + matches!(self, RoleType::Tss) } /// Checks if the role type is a Zk-Saas role. - pub fn is_zksaas(self) -> bool { - self == RoleType::ZkSaas + pub fn is_zksaas(&self) -> bool { + matches!(self, RoleType::ZkSaas) } } -impl From for RoleType { - fn from(job_key: JobKey) -> Self { - match job_key { - JobKey::DKG => RoleType::Tss, - JobKey::DKGSignature => RoleType::Tss, - JobKey::ZkSaasPhaseOne => RoleType::ZkSaas, - JobKey::ZkSaasPhaseTwo => RoleType::ZkSaas, +/// Metadata associated with a role type. +#[derive(Encode, Decode, Clone, Debug, PartialEq, Eq, TypeInfo)] +pub enum RoleTypeMetadata { + Tss(TssRoleMetadata), + ZkSaas(ZkSaasRoleMetadata), +} + +impl RoleTypeMetadata { + /// Return type of role. + pub fn get_role_type(&self) -> RoleType { + match self { + RoleTypeMetadata::Tss(_) => RoleType::Tss, + RoleTypeMetadata::ZkSaas(_) => RoleType::ZkSaas, } } + + pub fn get_authority_key(&self) -> Vec { + match self { + RoleTypeMetadata::Tss(metadata) => metadata.authority_key.clone(), + RoleTypeMetadata::ZkSaas(metadata) => metadata.authority_key.clone(), + } + } +} + +/// Associated metadata needed for a DKG role +#[derive(Encode, Decode, Clone, Debug, PartialEq, Default, Eq, TypeInfo)] +pub struct TssRoleMetadata { + /// The authority key associated with the role. + authority_key: Vec, +} + +/// Associated metadata needed for a zkSaas role +#[derive(Encode, Decode, Clone, Debug, PartialEq, Default, Eq, TypeInfo)] +pub struct ZkSaasRoleMetadata { + /// The authority key associated with the role. + // TODO : Expand this + authority_key: Vec, } /// Role type to be used in the system.