From dd85da39b3042b41213d2dbbf38b28d513425643 Mon Sep 17 00:00:00 2001 From: 1xstj <106580853+1xstj@users.noreply.github.com> Date: Fri, 24 Nov 2023 12:08:44 +0000 Subject: [PATCH 1/7] first pass : simple distribution --- Cargo.lock | 1 + pallets/roles/src/impls.rs | 79 ++++++++++++++++++++++++++++++++++- pallets/roles/src/lib.rs | 14 ++++++- primitives/Cargo.toml | 1 + primitives/src/types/roles.rs | 26 ++++++++++++ 5 files changed, 119 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 706b22215..ecb58ac71 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15423,6 +15423,7 @@ dependencies = [ "sp-arithmetic 16.0.0 (git+https://github.com/paritytech/substrate?branch=polkadot-v1.0.0)", "sp-core 21.0.0 (git+https://github.com/paritytech/substrate?branch=polkadot-v1.0.0)", "sp-runtime 24.0.0 (git+https://github.com/paritytech/substrate?branch=polkadot-v1.0.0)", + "sp-std 8.0.0 (git+https://github.com/paritytech/substrate?branch=polkadot-v1.0.0)", ] [[package]] diff --git a/pallets/roles/src/impls.rs b/pallets/roles/src/impls.rs index c64a1bf35..5ac9f175e 100644 --- a/pallets/roles/src/impls.rs +++ b/pallets/roles/src/impls.rs @@ -15,8 +15,13 @@ // along with Tangle. If not, see . use super::*; -use frame_support::pallet_prelude::DispatchResult; +use frame_support::{ + log, + pallet_prelude::DispatchResult, + traits::{Currency, ExistenceRequirement::KeepAlive, OneSessionHandler}, +}; use sp_runtime::{Percent, Saturating}; +use sp_std::ops::Div; use tangle_primitives::{jobs::JobKey, traits::roles::RolesHandler}; /// Implements RolesHandler for the pallet. @@ -180,4 +185,76 @@ impl Pallet { pub(crate) fn kill_stash(stash: &T::AccountId) { >::remove(&stash); } + + pub fn distribute_rewards() -> DispatchResult { + let total_rewards = T::InflationRewardPerEra::get(); + + let mut tss_validators: Vec = Default::default(); + let mut zksaas_validators: Vec = Default::default(); + + for (acc, role_types) in AccountRolesMapping::::iter() { + if role_types.contains(&RoleType::Tss) { + tss_validators.push(acc.clone()) + } + + if role_types.contains(&RoleType::ZkSaas) { + zksaas_validators.push(acc) + } + } + + log::debug!("Found {:?} tss validators", tss_validators.len()); + log::debug!("Found {:?} zksaas validators", zksaas_validators.len()); + + let reward_distribution = T::ValidatorRewardDistribution::get(); + + let dist = reward_distribution.get_reward_distribution(); + let tss_validator_reward = + dist.0.mul_floor(total_rewards).div((tss_validators.len() as u32).into()); + let zksaas_validators_reward = + dist.1.mul_floor(total_rewards).div((zksaas_validators.len() as u32).into()); + + log::debug!("Reward for tss validator : {:?}", tss_validator_reward); + log::debug!("Reward for zksaas validator : {:?}", zksaas_validators_reward); + + for validator in tss_validators { + T::Currency::deposit_creating(&validator, tss_validator_reward); + } + + for validator in zksaas_validators { + T::Currency::deposit_creating(&validator, zksaas_validators_reward); + } + + Ok(()) + } +} + +impl sp_runtime::BoundToRuntimeAppPublic for Pallet { + type Public = T::AuthorityId; +} + +impl OneSessionHandler for Pallet { + type Key = T::AuthorityId; + + fn on_genesis_session<'a, I: 'a>(_validators: I) + where + I: Iterator, + { + // nothing to be done + } + + fn on_new_session<'a, I: 'a>(_changed: bool, _validators: I, _queued_validators: I) + where + I: Iterator, + { + // nothing to be done + } + + fn on_disabled(_i: u32) { + // ignore + } + + // Distribute the inflation rewards + fn on_before_session_ending() { + let _ = Self::distribute_rewards(); + } } diff --git a/pallets/roles/src/lib.rs b/pallets/roles/src/lib.rs index 826b4f51d..4c48749a5 100644 --- a/pallets/roles/src/lib.rs +++ b/pallets/roles/src/lib.rs @@ -24,6 +24,7 @@ use frame_support::{ traits::{Currency, Get}, CloneNoBound, EqNoBound, PartialEqNoBound, RuntimeDebugNoBound, }; +use tangle_primitives::roles::ValidatorRewardDistribution; pub use pallet::*; use parity_scale_codec::{Decode, Encode}; @@ -42,6 +43,8 @@ mod tests; mod weights; pub use weights::WeightInfo; +use sp_runtime::RuntimeAppPublic; + /// The ledger of a (bonded) stash. #[derive( PartialEqNoBound, @@ -122,6 +125,15 @@ pub mod pallet { /// The config that verifies MPC related functions type MPCHandler: MPCHandler, BalanceOf>; + /// The inflation reward to distribute per era + type InflationRewardPerEra: Get>; + + /// The inflation distribution based on validator type + type ValidatorRewardDistribution: Get; + + /// The type used to identify an authority + type AuthorityId: RuntimeAppPublic + Decode; + type WeightInfo: WeightInfo; } @@ -171,7 +183,7 @@ pub mod pallet { /// Mapping of resource to bridge index pub type AccountRolesMapping = StorageMap< _, - Blake2_256, + Blake2_128Concat, T::AccountId, BoundedVec, ValueQuery, diff --git a/primitives/Cargo.toml b/primitives/Cargo.toml index b26338a84..f5d9626a2 100644 --- a/primitives/Cargo.toml +++ b/primitives/Cargo.toml @@ -16,6 +16,7 @@ serde = { workspace = true } smallvec = { workspace = true } sp-arithmetic = { workspace = true } sp-core = { workspace = true } +sp-std = { workspace = true } sp-runtime = { workspace = true } [features] diff --git a/primitives/src/types/roles.rs b/primitives/src/types/roles.rs index f1d935757..a21438e7a 100644 --- a/primitives/src/types/roles.rs +++ b/primitives/src/types/roles.rs @@ -14,6 +14,8 @@ // You should have received a copy of the GNU General Public License // along with Tangle. If not, see . use frame_support::{dispatch::Vec, pallet_prelude::*}; +use sp_arithmetic::Percent; +use sp_std::ops::Add; /// Role type to be used in the system. #[derive(Encode, Decode, Clone, Debug, PartialEq, Eq, TypeInfo)] @@ -81,3 +83,27 @@ pub enum ReStakingOption { // Re-stake only the given amount of funds for selected role. Custom(u64), } + +/// Represents the reward distribution percentages for validators in a key generation process. +pub struct ValidatorRewardDistribution { + /// The percentage share of the reward allocated for TSS + tss_share : Percent, + /// The percentage share of the reward allocated for the ZK-SaaS + zksaas_share : Percent +} + +impl ValidatorRewardDistribution { + pub fn try_new(tss_share : Percent, zksaas_share: Percent) -> Result { + if !tss_share.add(zksaas_share).is_one() { + return Err("Shares must add to One".to_string()) + } + + Ok(Self { tss_share , zksaas_share}) + } + + pub fn get_reward_distribution(self) -> (Percent, Percent) { + (self.tss_share, self.zksaas_share) + } +} + + From cd6dce50bdbda814254c55d55e732c9a5ff604cd Mon Sep 17 00:00:00 2001 From: 1xstj <106580853+1xstj@users.noreply.github.com> Date: Fri, 24 Nov 2023 12:27:11 +0000 Subject: [PATCH 2/7] add tests --- pallets/roles/src/impls.rs | 21 +++++++++----- pallets/roles/src/mock.rs | 13 +++++++-- pallets/roles/src/tests.rs | 57 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+), 10 deletions(-) diff --git a/pallets/roles/src/impls.rs b/pallets/roles/src/impls.rs index 5ac9f175e..6c3e51b7b 100644 --- a/pallets/roles/src/impls.rs +++ b/pallets/roles/src/impls.rs @@ -18,10 +18,10 @@ use super::*; use frame_support::{ log, pallet_prelude::DispatchResult, - traits::{Currency, ExistenceRequirement::KeepAlive, OneSessionHandler}, + traits::{Currency, OneSessionHandler}, }; -use sp_runtime::{Percent, Saturating}; -use sp_std::ops::Div; +use sp_runtime::{traits::CheckedDiv, Percent, Saturating}; + use tangle_primitives::{jobs::JobKey, traits::roles::RolesHandler}; /// Implements RolesHandler for the pallet. @@ -208,10 +208,17 @@ impl Pallet { let reward_distribution = T::ValidatorRewardDistribution::get(); let dist = reward_distribution.get_reward_distribution(); - let tss_validator_reward = - dist.0.mul_floor(total_rewards).div((tss_validators.len() as u32).into()); - let zksaas_validators_reward = - dist.1.mul_floor(total_rewards).div((zksaas_validators.len() as u32).into()); + + let tss_validator_reward = dist + .0 + .mul_floor(total_rewards) + .checked_div(&(tss_validators.len() as u32).into()) + .unwrap_or(Zero::zero()); + let zksaas_validators_reward = dist + .1 + .mul_floor(total_rewards) + .checked_div(&(zksaas_validators.len() as u32).into()) + .unwrap_or(Zero::zero()); log::debug!("Reward for tss validator : {:?}", tss_validator_reward); log::debug!("Reward for zksaas validator : {:?}", zksaas_validators_reward); diff --git a/pallets/roles/src/mock.rs b/pallets/roles/src/mock.rs index 760badba1..b785a83da 100644 --- a/pallets/roles/src/mock.rs +++ b/pallets/roles/src/mock.rs @@ -27,10 +27,9 @@ use sp_core::H256; use sp_runtime::{ testing::{Header, UintAuthorityId}, traits::IdentityLookup, - BuildStorage, DispatchResult, Perbill, + BuildStorage, DispatchResult, Perbill, Percent, }; -use tangle_primitives::{jobs::*, traits::jobs::MPCHandler}; - +use tangle_primitives::{jobs::*, roles::ValidatorRewardDistribution, traits::jobs::MPCHandler}; pub type AccountId = u64; pub type Balance = u128; pub type BlockNumber = u64; @@ -243,11 +242,19 @@ impl JobsHandler for MockJobsHandler { } } +parameter_types! { + pub InflationRewardPerEra: Balance = 10_000; + pub Reward : ValidatorRewardDistribution = ValidatorRewardDistribution::try_new(Percent::from_rational(1_u32,2_u32), Percent::from_rational(1_u32,2_u32)).unwrap(); +} + impl Config for Runtime { type RuntimeEvent = RuntimeEvent; type JobsHandler = MockJobsHandler; type MaxRolesPerAccount = ConstU32<2>; type MPCHandler = MockMPCHandler; + type InflationRewardPerEra = InflationRewardPerEra; + type AuthorityId = UintAuthorityId; + type ValidatorRewardDistribution = Reward; type WeightInfo = (); } diff --git a/pallets/roles/src/tests.rs b/pallets/roles/src/tests.rs index 8dbfe0b2d..302c88305 100644 --- a/pallets/roles/src/tests.rs +++ b/pallets/roles/src/tests.rs @@ -231,3 +231,60 @@ fn test_unbound_funds_should_work_if_no_role_assigned() { })]); }); } + +#[test] +fn test_reward_dist_works_as_expected_with_one_validator() { + new_test_ext_raw_authorities(vec![1, 2, 3, 4]).execute_with(|| { + assert_eq!(Balances::free_balance(1), 20_000); + + // Roles user is interested in re-staking. + let role_records = vec![ + 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)); + + // The reward is 100, we have 5 authorities + assert_ok!(Roles::distribute_rewards()); + + // ensure the distribution is correct + assert_eq!(Balances::free_balance(1), 20_000 + 10_000); + }); +} + +#[test] +fn test_reward_dist_works_as_expected_with_multiple_validator() { + new_test_ext_raw_authorities(vec![1, 2, 3, 4]).execute_with(|| { + let _reward_amount = 10_000; + assert_eq!(Balances::free_balance(1), 20_000); + + // Roles user is interested in re-staking. + let role_records = vec![ + 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.clone())); + assert_ok!(Roles::assign_roles(RuntimeOrigin::signed(2), role_records)); + + // The reward is 100, we have 5 authorities + assert_ok!(Roles::distribute_rewards()); + + // ensure the distribution is correct + assert_eq!(Balances::free_balance(1), 20_000 + 5000); + assert_eq!(Balances::free_balance(2), 20_000 + 5000); + }); +} From 52a9ce9382d91d7c98e1309692cf865a780be66a Mon Sep 17 00:00:00 2001 From: 1xstj <106580853+1xstj@users.noreply.github.com> Date: Fri, 24 Nov 2023 12:29:40 +0000 Subject: [PATCH 3/7] add tests --- primitives/src/types/roles.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/primitives/src/types/roles.rs b/primitives/src/types/roles.rs index a21438e7a..f5c66f53f 100644 --- a/primitives/src/types/roles.rs +++ b/primitives/src/types/roles.rs @@ -87,23 +87,21 @@ pub enum ReStakingOption { /// Represents the reward distribution percentages for validators in a key generation process. pub struct ValidatorRewardDistribution { /// The percentage share of the reward allocated for TSS - tss_share : Percent, + tss_share: Percent, /// The percentage share of the reward allocated for the ZK-SaaS - zksaas_share : Percent + zksaas_share: Percent, } impl ValidatorRewardDistribution { - pub fn try_new(tss_share : Percent, zksaas_share: Percent) -> Result { + pub fn try_new(tss_share: Percent, zksaas_share: Percent) -> Result { if !tss_share.add(zksaas_share).is_one() { return Err("Shares must add to One".to_string()) } - Ok(Self { tss_share , zksaas_share}) + Ok(Self { tss_share, zksaas_share }) } pub fn get_reward_distribution(self) -> (Percent, Percent) { (self.tss_share, self.zksaas_share) } } - - From f95fc45432709a706587cb107658d28db141a708 Mon Sep 17 00:00:00 2001 From: 1xstj <106580853+1xstj@users.noreply.github.com> Date: Fri, 24 Nov 2023 12:57:57 +0000 Subject: [PATCH 4/7] clippy fix --- primitives/src/types/roles.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/primitives/src/types/roles.rs b/primitives/src/types/roles.rs index f5c66f53f..7c750b8f5 100644 --- a/primitives/src/types/roles.rs +++ b/primitives/src/types/roles.rs @@ -14,6 +14,7 @@ // You should have received a copy of the GNU General Public License // along with Tangle. If not, see . use frame_support::{dispatch::Vec, pallet_prelude::*}; +use scale_info::prelude::string::String; use sp_arithmetic::Percent; use sp_std::ops::Add; From a57dc1371821ba4c4c192f10e5ce2f0ca51cc866 Mon Sep 17 00:00:00 2001 From: 1xstj <106580853+1xstj@users.noreply.github.com> Date: Fri, 24 Nov 2023 13:24:28 +0000 Subject: [PATCH 5/7] clippy fix --- primitives/src/types/roles.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/primitives/src/types/roles.rs b/primitives/src/types/roles.rs index 7c750b8f5..042cdd9bd 100644 --- a/primitives/src/types/roles.rs +++ b/primitives/src/types/roles.rs @@ -17,6 +17,7 @@ use frame_support::{dispatch::Vec, pallet_prelude::*}; use scale_info::prelude::string::String; use sp_arithmetic::Percent; use sp_std::ops::Add; +use parity_scale_codec::alloc::string::ToString; /// Role type to be used in the system. #[derive(Encode, Decode, Clone, Debug, PartialEq, Eq, TypeInfo)] From 0cb0a90fc8d101b1f9f9592cfb0249addd7662fa Mon Sep 17 00:00:00 2001 From: 1xstj <106580853+1xstj@users.noreply.github.com> Date: Fri, 24 Nov 2023 13:26:37 +0000 Subject: [PATCH 6/7] clippy fix --- primitives/src/types/roles.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/primitives/src/types/roles.rs b/primitives/src/types/roles.rs index 042cdd9bd..c000602d5 100644 --- a/primitives/src/types/roles.rs +++ b/primitives/src/types/roles.rs @@ -14,10 +14,10 @@ // You should have received a copy of the GNU General Public License // along with Tangle. If not, see . use frame_support::{dispatch::Vec, pallet_prelude::*}; +use parity_scale_codec::alloc::string::ToString; use scale_info::prelude::string::String; use sp_arithmetic::Percent; use sp_std::ops::Add; -use parity_scale_codec::alloc::string::ToString; /// Role type to be used in the system. #[derive(Encode, Decode, Clone, Debug, PartialEq, Eq, TypeInfo)] From 50e27aef61570cac46f6cf6629b1fcbc0e1e9e54 Mon Sep 17 00:00:00 2001 From: 1xstj <106580853+1xstj@users.noreply.github.com> Date: Mon, 27 Nov 2023 16:59:35 +0000 Subject: [PATCH 7/7] rename era to session --- pallets/roles/src/impls.rs | 2 +- pallets/roles/src/lib.rs | 2 +- pallets/roles/src/mock.rs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pallets/roles/src/impls.rs b/pallets/roles/src/impls.rs index 6c3e51b7b..e5d79030e 100644 --- a/pallets/roles/src/impls.rs +++ b/pallets/roles/src/impls.rs @@ -187,7 +187,7 @@ impl Pallet { } pub fn distribute_rewards() -> DispatchResult { - let total_rewards = T::InflationRewardPerEra::get(); + let total_rewards = T::InflationRewardPerSession::get(); let mut tss_validators: Vec = Default::default(); let mut zksaas_validators: Vec = Default::default(); diff --git a/pallets/roles/src/lib.rs b/pallets/roles/src/lib.rs index 4c48749a5..db532431a 100644 --- a/pallets/roles/src/lib.rs +++ b/pallets/roles/src/lib.rs @@ -126,7 +126,7 @@ pub mod pallet { type MPCHandler: MPCHandler, BalanceOf>; /// The inflation reward to distribute per era - type InflationRewardPerEra: Get>; + type InflationRewardPerSession: Get>; /// The inflation distribution based on validator type type ValidatorRewardDistribution: Get; diff --git a/pallets/roles/src/mock.rs b/pallets/roles/src/mock.rs index b785a83da..73710fe6d 100644 --- a/pallets/roles/src/mock.rs +++ b/pallets/roles/src/mock.rs @@ -243,7 +243,7 @@ impl JobsHandler for MockJobsHandler { } parameter_types! { - pub InflationRewardPerEra: Balance = 10_000; + pub InflationRewardPerSession: Balance = 10_000; pub Reward : ValidatorRewardDistribution = ValidatorRewardDistribution::try_new(Percent::from_rational(1_u32,2_u32), Percent::from_rational(1_u32,2_u32)).unwrap(); } @@ -252,7 +252,7 @@ impl Config for Runtime { type JobsHandler = MockJobsHandler; type MaxRolesPerAccount = ConstU32<2>; type MPCHandler = MockMPCHandler; - type InflationRewardPerEra = InflationRewardPerEra; + type InflationRewardPerSession = InflationRewardPerSession; type AuthorityId = UintAuthorityId; type ValidatorRewardDistribution = Reward; type WeightInfo = ();