From 12c182523fea44e873923c746a78c237c812b65a Mon Sep 17 00:00:00 2001 From: Drew Stone Date: Mon, 29 Jan 2024 13:33:35 -0700 Subject: [PATCH 1/5] Update and allow switching independent - profile --- pallets/roles/src/impls.rs | 45 ++++++++++++++++++++++++-------------- shared | 0 2 files changed, 29 insertions(+), 16 deletions(-) create mode 100644 shared diff --git a/pallets/roles/src/impls.rs b/pallets/roles/src/impls.rs index 6699c6ed6..08361bbf0 100644 --- a/pallets/roles/src/impls.rs +++ b/pallets/roles/src/impls.rs @@ -101,20 +101,7 @@ impl Pallet { } } }; - // Changing a current independent profile to shared profile is not allowed if there are - // any active jobs for any role that is currently in the profile. The reason we don't - // allow this is because a user who requested the active job might have done so because - // they wanted independent risk for the security of their application. If the validator - // fails to perform the job of a different role, their stake for "this" role won't be - // affected. It won't fall below the minimum and any removal protocol will only be triggered - // on the role that failed to perform the job. If the validator is now shared, then the - // stake for all roles will be affected. - // - // *** Perhaps this is entirely unnecessary, and I am overthinking it. *** - if updated_profile.is_shared() && current_ledger.profile.is_independent() { - ensure!(active_jobs.len() == 0, Error::::HasRoleAssigned); - return Ok(()) - } + // Get all roles for which there are active jobs let roles_with_active_jobs: Vec = active_jobs.iter().map(|job| job.0).fold(Vec::new(), |mut acc, role| { @@ -123,6 +110,32 @@ impl Pallet { } acc }); + + // If there are active jobs, changing a current independent profile to shared profile + // is allowed if and only if the shared restaking amount is at least as much as the + // sum of the restaking amounts of the current profile. This is because we require + // the total amount staked to only increase or remain the same across active roles. + if updated_profile.is_shared() && current_ledger.profile.is_independent() { + ensure!(active_jobs.len() == 0, Error::::HasRoleAssigned); + if active_jobs.len() > 0 { + let mut active_role_restaking_sum = Zero::zero(); + for role in roles_with_active_jobs.iter() { + let current_role_restaking_amount = current_ledger + .profile + .get_records() + .iter() + .find_map(|record| if record.role == *role { record.amount } else { None }) + .unwrap_or_else(|| Zero::zero()); + active_role_restaking_sum += current_role_restaking_amount; + } + + ensure!( + updated_profile.get_total_profile_restake() >= active_role_restaking_sum, + Error::::InsufficientRestakingBond + ); + } + } + // Changing a current shared profile to an independent profile is allowed if there are // active jobs as long as the stake allocated to the active roles is at least as much as // the shared profile restaking amount. This is because the shared restaking profile for an @@ -132,11 +145,11 @@ impl Pallet { if updated_profile.is_independent() && current_ledger.profile.is_shared() { // For each role with an active job, ensure its stake is greater than or equal to the // existing ledger's shared restaking amount. - for role in roles_with_active_jobs { + for role in roles_with_active_jobs.iter() { let updated_role_restaking_amount = updated_profile .get_records() .iter() - .find_map(|record| if record.role == role { record.amount } else { None }) + .find_map(|record| if record.role == *role { record.amount } else { None }) .unwrap_or_else(|| Zero::zero()); ensure!( updated_role_restaking_amount >= diff --git a/shared b/shared new file mode 100644 index 000000000..e69de29bb From f0879eb285ab3d08996fe3288a98b7ab45fbb7d2 Mon Sep 17 00:00:00 2001 From: Drew Stone Date: Mon, 29 Jan 2024 13:39:54 -0700 Subject: [PATCH 2/5] fix: benchmarking.rs errors --- pallets/roles/src/benchmarking.rs | 14 +++++++------- shared | 3 +++ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/pallets/roles/src/benchmarking.rs b/pallets/roles/src/benchmarking.rs index 9330bc92e..6815d3ef5 100644 --- a/pallets/roles/src/benchmarking.rs +++ b/pallets/roles/src/benchmarking.rs @@ -26,7 +26,7 @@ use frame_support::BoundedVec; use frame_system::RawOrigin; use sp_core::sr25519; use sp_runtime::Perbill; -use tangle_primitives::roles::RoleTypeMetadata; +use tangle_primitives::roles::RoleType; fn assert_last_event(generic_event: ::RuntimeEvent) { frame_system::Pallet::::assert_last_event(generic_event.into()); @@ -36,8 +36,8 @@ pub fn shared_profile() -> Profile { let amount: T::CurrencyBalance = 3000_u64.into(); let profile = SharedRestakeProfile { records: BoundedVec::try_from(vec![ - Record { metadata: RoleTypeMetadata::Tss(Default::default()), amount: None }, - Record { metadata: RoleTypeMetadata::ZkSaas(Default::default()), amount: None }, + Record { role: RoleType::Tss(Default::default()), amount: None }, + Record { role: RoleType::ZkSaaS(Default::default()), amount: None }, ]) .unwrap(), amount, @@ -49,8 +49,8 @@ pub fn updated_profile() -> Profile { let amount: T::CurrencyBalance = 5000_u64.into(); let profile = SharedRestakeProfile { records: BoundedVec::try_from(vec![ - Record { metadata: RoleTypeMetadata::Tss(Default::default()), amount: None }, - Record { metadata: RoleTypeMetadata::ZkSaas(Default::default()), amount: None }, + Record { role: RoleType::Tss(Default::default()), amount: None }, + Record { role: RoleType::ZkSaaS(Default::default()), amount: None }, ]) .unwrap(), amount, @@ -112,7 +112,7 @@ mod benchmarks { fn update_profile() { let caller: T::AccountId = create_validator_account::("Alice"); let shared_profile = shared_profile::(); - let ledger = RoleStakingLedger::::new(caller.clone(), shared_profile.clone()); + let ledger = RoleStakingLedger::::new(caller.clone(), shared_profile.clone(), vec![]); Ledger::::insert(caller.clone(), ledger); // Updating shared stake from 3000 to 5000 tokens let updated_profile = updated_profile::(); @@ -129,7 +129,7 @@ mod benchmarks { fn delete_profile() { let caller: T::AccountId = create_validator_account::("Alice"); let shared_profile = shared_profile::(); - let ledger = RoleStakingLedger::::new(caller.clone(), shared_profile.clone()); + let ledger = RoleStakingLedger::::new(caller.clone(), shared_profile.clone(), vec![]); Ledger::::insert(caller.clone(), ledger); #[extrinsic_call] diff --git a/shared b/shared index e69de29bb..c34fca128 100644 --- a/shared +++ b/shared @@ -0,0 +1,3 @@ +[drew/update-role-validation 12c1825] Update and allow switching independent - profile + 2 files changed, 29 insertions(+), 16 deletions(-) + create mode 100644 shared From d388529fe8d9b6d5a3d6642fe4e636961a1ade25 Mon Sep 17 00:00:00 2001 From: Drew Stone Date: Mon, 29 Jan 2024 13:40:43 -0700 Subject: [PATCH 3/5] fix: jobs/benchmarking.rs errors --- pallets/jobs/src/benchmarking.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pallets/jobs/src/benchmarking.rs b/pallets/jobs/src/benchmarking.rs index e8a35e510..3942c8deb 100644 --- a/pallets/jobs/src/benchmarking.rs +++ b/pallets/jobs/src/benchmarking.rs @@ -21,6 +21,7 @@ benchmarks! { let _ = T::Currency::make_free_balance_be(&caller, BalanceOf::::max_value()); let job = JobSubmissionOf:: { expiry: 100u32.into(), + ttl: 100u32.into(), job_type: JobType::DKGTSSPhaseOne(DKGTSSPhaseOneJobType { participants: vec![caller.clone(), caller.clone()], threshold: 1, permitted_caller: None, role_type : ThresholdSignatureRoleType::TssGG20 }), }; @@ -33,6 +34,7 @@ benchmarks! { let _ = T::Currency::make_free_balance_be(&caller, BalanceOf::::max_value()); let job = JobSubmissionOf:: { expiry: 100u32.into(), + ttl: 100u32.into(), job_type: JobType::DKGTSSPhaseOne(DKGTSSPhaseOneJobType { participants: vec![caller.clone(), validator2], threshold: 1, permitted_caller: None, role_type : ThresholdSignatureRoleType::TssGG20 }), }; let _ = Pallet::::submit_job(RawOrigin::Signed(caller.clone()).into(), job); From c3e88f35c155881aafa7f8afced3c7f2635d62cd Mon Sep 17 00:00:00 2001 From: Drew Stone Date: Mon, 29 Jan 2024 13:49:17 -0700 Subject: [PATCH 4/5] fix: jobs/benchmarking.rs errors --- pallets/jobs/src/benchmarking.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/pallets/jobs/src/benchmarking.rs b/pallets/jobs/src/benchmarking.rs index 3942c8deb..73de6190a 100644 --- a/pallets/jobs/src/benchmarking.rs +++ b/pallets/jobs/src/benchmarking.rs @@ -22,7 +22,7 @@ benchmarks! { let job = JobSubmissionOf:: { expiry: 100u32.into(), ttl: 100u32.into(), - job_type: JobType::DKGTSSPhaseOne(DKGTSSPhaseOneJobType { participants: vec![caller.clone(), caller.clone()], threshold: 1, permitted_caller: None, role_type : ThresholdSignatureRoleType::TssGG20 }), + job_type: JobType::DKGTSSPhaseOne(DKGTSSPhaseOneJobType { participants: vec![caller.clone(), caller.clone()], threshold: 1, permitted_caller: None, role_type : Default::default() }), }; }: _(RawOrigin::Signed(caller.clone()), job.clone()) @@ -35,10 +35,10 @@ benchmarks! { let job = JobSubmissionOf:: { expiry: 100u32.into(), ttl: 100u32.into(), - job_type: JobType::DKGTSSPhaseOne(DKGTSSPhaseOneJobType { participants: vec![caller.clone(), validator2], threshold: 1, permitted_caller: None, role_type : ThresholdSignatureRoleType::TssGG20 }), + job_type: JobType::DKGTSSPhaseOne(DKGTSSPhaseOneJobType { participants: vec![caller.clone(), validator2], threshold: 1, permitted_caller: None, role_type : Default::default() }), }; let _ = Pallet::::submit_job(RawOrigin::Signed(caller.clone()).into(), job); - let job_key: RoleType = RoleType::Tss(ThresholdSignatureRoleType::TssGG20); + let job_key: RoleType = RoleType::Tss(Default::default()); let job_id: JobId = 0; let result = JobResult::DKGPhaseOne(DKGTSSKeySubmissionResult { signatures: vec![], @@ -66,10 +66,11 @@ benchmarks! { let _ = T::Currency::make_free_balance_be(&caller, BalanceOf::::max_value()); let job = JobSubmissionOf:: { expiry: 100u32.into(), - job_type: JobType::DKGTSSPhaseOne(DKGTSSPhaseOneJobType { participants: vec![caller.clone(), validator2, validator3], threshold: 2, permitted_caller: None, role_type : ThresholdSignatureRoleType::TssGG20 }), - }; + ttl: 100u32.into(), + job_type: JobType::DKGTSSPhaseOne(DKGTSSPhaseOneJobType { participants: vec![caller.clone(), validator2, validator3], threshold: 2, permitted_caller: None, role_type : Default::default() }), + }; let _ = Pallet::::submit_job(RawOrigin::Signed(caller.clone()).into(), job); - let job_key: RoleType = RoleType::Tss(ThresholdSignatureRoleType::TssGG20); + let job_key: RoleType = RoleType::Tss(Default::default()); let job_id: JobId = 0; }: _(RawOrigin::Signed(caller.clone()), job_key.clone(), job_id.clone(), caller.clone(), ValidatorOffenceType::Inactivity, vec![]) } From 61233fa1d58659d2e697fba05dd52220982d1cd9 Mon Sep 17 00:00:00 2001 From: Drew Stone Date: Mon, 29 Jan 2024 14:19:59 -0700 Subject: [PATCH 5/5] Break tests up to individual cases, add active profile switching tests --- pallets/jobs/src/mock.rs | 4 +- pallets/jobs/src/tests.rs | 436 ++++++++++++++++++++++++- pallets/roles/src/benchmarking.rs | 2 +- pallets/roles/src/impls.rs | 5 +- pallets/roles/src/lib.rs | 20 +- pallets/roles/src/mock.rs | 4 +- pallets/roles/src/tests.rs | 8 +- pallets/roles/src/weights.rs | 6 +- types/src/interfaces/augment-api-tx.ts | 4 +- types/src/interfaces/lookup.ts | 2 +- 10 files changed, 460 insertions(+), 31 deletions(-) diff --git a/pallets/jobs/src/mock.rs b/pallets/jobs/src/mock.rs index d8e1d39f6..d30430a41 100644 --- a/pallets/jobs/src/mock.rs +++ b/pallets/jobs/src/mock.rs @@ -180,10 +180,10 @@ impl pallet_session::historical::Config for Runtime { pub struct BaseFilter; impl Contains for BaseFilter { fn contains(call: &RuntimeCall) -> bool { - let is_stake_unbound_call = + let is_stake_unbond_call = matches!(call, RuntimeCall::Staking(pallet_staking::Call::unbond { .. })); - if is_stake_unbound_call { + if is_stake_unbond_call { // no unbond call return false } diff --git a/pallets/jobs/src/tests.rs b/pallets/jobs/src/tests.rs index d6c2f5fd4..e26fbf31e 100644 --- a/pallets/jobs/src/tests.rs +++ b/pallets/jobs/src/tests.rs @@ -55,6 +55,23 @@ pub fn shared_profile() -> Profile { Profile::Shared(profile) } +pub fn independent_profile() -> Profile { + let profile = IndependentRestakeProfile { + records: BoundedVec::try_from(vec![ + Record { + role: RoleType::Tss(ThresholdSignatureRoleType::ZengoGG20Secp256k1), + amount: Some(500), + }, + Record { + role: RoleType::ZkSaaS(ZeroKnowledgeRoleType::ZkSaaSGroth16), + amount: Some(500), + }, + ]) + .unwrap(), + }; + Profile::Independent(profile) +} + #[test] fn jobs_submission_e2e_works_for_dkg() { new_test_ext(vec![ALICE, BOB, CHARLIE, DAVE, EVE]).execute_with(|| { @@ -711,7 +728,7 @@ fn jobs_submission_e2e_works_for_zksaas() { } #[test] -fn jobs_validator_checks_work() { +fn reduce_active_role_restake_should_fail() { new_test_ext(vec![ALICE, BOB, CHARLIE, DAVE, EVE]).execute_with(|| { Balances::make_free_balance_be(&mock_pub_key(TEN), 100); @@ -765,6 +782,38 @@ fn jobs_validator_checks_work() { pallet_roles::Error::::InsufficientRestakingBond ); } + }); +} + +#[test] +fn delete_profile_with_active_role_should_fail() { + new_test_ext(vec![ALICE, BOB, CHARLIE, DAVE, EVE]).execute_with(|| { + Balances::make_free_balance_be(&mock_pub_key(TEN), 100); + + let participants = vec![ALICE, BOB, CHARLIE, DAVE, EVE]; + + // all validators sign up in roles pallet + let profile = shared_profile(); + for validator in participants.clone() { + assert_ok!(Roles::create_profile( + RuntimeOrigin::signed(mock_pub_key(validator)), + profile.clone() + )); + } + + // submit job with existing validators + let threshold_signature_role_type = ThresholdSignatureRoleType::ZengoGG20Secp256k1; + let submission = JobSubmission { + expiry: 10, + ttl: 200, + job_type: JobType::DKGTSSPhaseOne(DKGTSSPhaseOneJobType { + participants: participants.clone().iter().map(|x| mock_pub_key(*x)).collect(), + threshold: 3, + permitted_caller: Some(mock_pub_key(TEN)), + role_type: threshold_signature_role_type, + }), + }; + assert_ok!(Jobs::submit_job(RuntimeOrigin::signed(mock_pub_key(TEN)), submission)); // ========= active validator cannot delete profile with active job ============= for validator in participants.clone() { @@ -773,6 +822,38 @@ fn jobs_validator_checks_work() { pallet_roles::Error::::ProfileDeleteRequestFailed ); } + }); +} + +#[test] +fn remove_active_role_should_fail() { + new_test_ext(vec![ALICE, BOB, CHARLIE, DAVE, EVE]).execute_with(|| { + Balances::make_free_balance_be(&mock_pub_key(TEN), 100); + + let participants = vec![ALICE, BOB, CHARLIE, DAVE, EVE]; + + // all validators sign up in roles pallet + let profile = shared_profile(); + for validator in participants.clone() { + assert_ok!(Roles::create_profile( + RuntimeOrigin::signed(mock_pub_key(validator)), + profile.clone() + )); + } + + // submit job with existing validators + let threshold_signature_role_type = ThresholdSignatureRoleType::ZengoGG20Secp256k1; + let submission = JobSubmission { + expiry: 10, + ttl: 200, + job_type: JobType::DKGTSSPhaseOne(DKGTSSPhaseOneJobType { + participants: participants.clone().iter().map(|x| mock_pub_key(*x)).collect(), + threshold: 3, + permitted_caller: Some(mock_pub_key(TEN)), + role_type: threshold_signature_role_type, + }), + }; + assert_ok!(Jobs::submit_job(RuntimeOrigin::signed(mock_pub_key(TEN)), submission)); // ========= active validator cannot remove role with active job ============= let reduced_profile = SharedRestakeProfile { @@ -792,6 +873,38 @@ fn jobs_validator_checks_work() { pallet_roles::Error::::RoleCannotBeRemoved ); } + }); +} + +#[test] +fn remove_role_without_active_jobs_should_work() { + new_test_ext(vec![ALICE, BOB, CHARLIE, DAVE, EVE]).execute_with(|| { + Balances::make_free_balance_be(&mock_pub_key(TEN), 100); + + let participants = vec![ALICE, BOB, CHARLIE, DAVE, EVE]; + + // all validators sign up in roles pallet + let profile = shared_profile(); + for validator in participants.clone() { + assert_ok!(Roles::create_profile( + RuntimeOrigin::signed(mock_pub_key(validator)), + profile.clone() + )); + } + + // submit job with existing validators + let threshold_signature_role_type = ThresholdSignatureRoleType::ZengoGG20Secp256k1; + let submission = JobSubmission { + expiry: 10, + ttl: 200, + job_type: JobType::DKGTSSPhaseOne(DKGTSSPhaseOneJobType { + participants: participants.clone().iter().map(|x| mock_pub_key(*x)).collect(), + threshold: 3, + permitted_caller: Some(mock_pub_key(TEN)), + role_type: threshold_signature_role_type, + }), + }; + assert_ok!(Jobs::submit_job(RuntimeOrigin::signed(mock_pub_key(TEN)), submission)); // ========= active validator can remove role without active job ========= let reduced_profile = SharedRestakeProfile { @@ -809,6 +922,38 @@ fn jobs_validator_checks_work() { Profile::Shared(reduced_profile.clone()) )); } + }); +} + +#[test] +fn add_role_to_active_profile_should_work() { + new_test_ext(vec![ALICE, BOB, CHARLIE, DAVE, EVE]).execute_with(|| { + Balances::make_free_balance_be(&mock_pub_key(TEN), 100); + + let participants = vec![ALICE, BOB, CHARLIE, DAVE, EVE]; + + // all validators sign up in roles pallet + let profile = shared_profile(); + for validator in participants.clone() { + assert_ok!(Roles::create_profile( + RuntimeOrigin::signed(mock_pub_key(validator)), + profile.clone() + )); + } + + // submit job with existing validators + let threshold_signature_role_type = ThresholdSignatureRoleType::ZengoGG20Secp256k1; + let submission = JobSubmission { + expiry: 10, + ttl: 200, + job_type: JobType::DKGTSSPhaseOne(DKGTSSPhaseOneJobType { + participants: participants.clone().iter().map(|x| mock_pub_key(*x)).collect(), + threshold: 3, + permitted_caller: Some(mock_pub_key(TEN)), + role_type: threshold_signature_role_type, + }), + }; + assert_ok!(Jobs::submit_job(RuntimeOrigin::signed(mock_pub_key(TEN)), submission)); // ========= active validator can add a new role with current active role ========= let updated_profile = SharedRestakeProfile { @@ -832,6 +977,92 @@ fn jobs_validator_checks_work() { Profile::Shared(updated_profile.clone()) )); } + }); +} + +#[test] +fn reduce_stake_on_non_active_role_should_work() { + new_test_ext(vec![ALICE, BOB, CHARLIE, DAVE, EVE]).execute_with(|| { + Balances::make_free_balance_be(&mock_pub_key(TEN), 100); + + let participants = vec![ALICE, BOB, CHARLIE, DAVE, EVE]; + + // all validators sign up in roles pallet + let profile = shared_profile(); + for validator in participants.clone() { + assert_ok!(Roles::create_profile( + RuntimeOrigin::signed(mock_pub_key(validator)), + profile.clone() + )); + } + + // submit job with existing validators + let threshold_signature_role_type = ThresholdSignatureRoleType::ZengoGG20Secp256k1; + let submission = JobSubmission { + expiry: 10, + ttl: 200, + job_type: JobType::DKGTSSPhaseOne(DKGTSSPhaseOneJobType { + participants: participants.clone().iter().map(|x| mock_pub_key(*x)).collect(), + threshold: 3, + permitted_caller: Some(mock_pub_key(TEN)), + role_type: threshold_signature_role_type, + }), + }; + assert_ok!(Jobs::submit_job(RuntimeOrigin::signed(mock_pub_key(TEN)), submission)); + + // ========= active validator can reduce stake on non active role ========= + let updated_profile = IndependentRestakeProfile { + records: BoundedVec::try_from(vec![ + Record { + role: RoleType::Tss(ThresholdSignatureRoleType::ZengoGG20Secp256k1), + amount: Some(1500), + }, + Record { + role: RoleType::ZkSaaS(ZeroKnowledgeRoleType::ZkSaaSGroth16), + amount: Some(500), // reduced by 3x + }, + ]) + .unwrap(), + }; + + for validator in participants.clone() { + assert_ok!(Roles::update_profile( + RuntimeOrigin::signed(mock_pub_key(validator)), + Profile::Independent(updated_profile.clone()) + )); + } + }); +} + +#[test] +fn increase_stake_on_active_role_should_work() { + new_test_ext(vec![ALICE, BOB, CHARLIE, DAVE, EVE]).execute_with(|| { + Balances::make_free_balance_be(&mock_pub_key(TEN), 100); + + let participants = vec![ALICE, BOB, CHARLIE, DAVE, EVE]; + + // all validators sign up in roles pallet + let profile = shared_profile(); + for validator in participants.clone() { + assert_ok!(Roles::create_profile( + RuntimeOrigin::signed(mock_pub_key(validator)), + profile.clone() + )); + } + + // submit job with existing validators + let threshold_signature_role_type = ThresholdSignatureRoleType::ZengoGG20Secp256k1; + let submission = JobSubmission { + expiry: 10, + ttl: 200, + job_type: JobType::DKGTSSPhaseOne(DKGTSSPhaseOneJobType { + participants: participants.clone().iter().map(|x| mock_pub_key(*x)).collect(), + threshold: 3, + permitted_caller: Some(mock_pub_key(TEN)), + role_type: threshold_signature_role_type, + }), + }; + assert_ok!(Jobs::submit_job(RuntimeOrigin::signed(mock_pub_key(TEN)), submission)); // ========= active validator can increase stake with current active role ========= let updated_profile = SharedRestakeProfile { @@ -855,8 +1086,24 @@ fn jobs_validator_checks_work() { Profile::Shared(updated_profile.clone()) )); } + }); +} - // ========= active validator can reduce stake on non active role ========= +#[test] +fn switch_non_active_profile_should_work() { + new_test_ext(vec![ALICE, BOB, CHARLIE, DAVE, EVE]).execute_with(|| { + let participants = vec![ALICE, BOB, CHARLIE, DAVE, EVE]; + + // all validators sign up in roles pallet + let profile = shared_profile(); + for validator in participants.clone() { + assert_ok!(Roles::create_profile( + RuntimeOrigin::signed(mock_pub_key(validator)), + profile.clone() + )); + } + + // ========= active validator can switch shared to independent profile ========= let updated_profile = IndependentRestakeProfile { records: BoundedVec::try_from(vec![ Record { @@ -865,7 +1112,109 @@ fn jobs_validator_checks_work() { }, Record { role: RoleType::ZkSaaS(ZeroKnowledgeRoleType::ZkSaaSGroth16), - amount: Some(500), // reduced by 3x + amount: Some(500), + }, + ]) + .unwrap(), + }; + + for validator in participants.clone() { + assert_ok!(Roles::update_profile( + RuntimeOrigin::signed(mock_pub_key(validator)), + Profile::Independent(updated_profile.clone()) + )); + } + + // ========= active validator can switch independent to shared profile ========= + let updated_profile = SharedRestakeProfile { + records: BoundedVec::try_from(vec![ + Record { + role: RoleType::Tss(ThresholdSignatureRoleType::ZengoGG20Secp256k1), + amount: None, + }, + Record { + role: RoleType::ZkSaaS(ZeroKnowledgeRoleType::ZkSaaSGroth16), + amount: None, + }, + ]) + .unwrap(), + amount: 1500, + }; + + for validator in participants.clone() { + assert_ok!(Roles::update_profile( + RuntimeOrigin::signed(mock_pub_key(validator)), + Profile::Shared(updated_profile.clone()) + )); + } + }); +} + +#[test] +fn switch_active_shared_profile_to_independent_should_work_if_active_stake_preserved() { + new_test_ext(vec![ALICE, BOB, CHARLIE, DAVE, EVE]).execute_with(|| { + Balances::make_free_balance_be(&mock_pub_key(TEN), 100); + + let participants = vec![ALICE, BOB, CHARLIE, DAVE, EVE]; + + // all validators sign up in roles pallet + let profile = shared_profile(); + for validator in participants.clone() { + assert_ok!(Roles::create_profile( + RuntimeOrigin::signed(mock_pub_key(validator)), + profile.clone() + )); + } + + // submit job with existing validators + let threshold_signature_role_type = ThresholdSignatureRoleType::ZengoGG20Secp256k1; + let submission = JobSubmission { + expiry: 10, + ttl: 200, + job_type: JobType::DKGTSSPhaseOne(DKGTSSPhaseOneJobType { + participants: participants.clone().iter().map(|x| mock_pub_key(*x)).collect(), + threshold: 3, + permitted_caller: Some(mock_pub_key(TEN)), + role_type: threshold_signature_role_type, + }), + }; + assert_ok!(Jobs::submit_job(RuntimeOrigin::signed(mock_pub_key(TEN)), submission)); + + // ========= active validator cannot switch shared to independent profile ========= + let updated_profile = IndependentRestakeProfile { + records: BoundedVec::try_from(vec![ + Record { + role: RoleType::Tss(ThresholdSignatureRoleType::ZengoGG20Secp256k1), + amount: Some(500), // <---------- ACTIVE STAKE NOT PRESERVED + }, + Record { + role: RoleType::ZkSaaS(ZeroKnowledgeRoleType::ZkSaaSGroth16), + amount: Some(500), + }, + ]) + .unwrap(), + }; + + for validator in participants.clone() { + assert_noop!( + Roles::update_profile( + RuntimeOrigin::signed(mock_pub_key(validator)), + Profile::Independent(updated_profile.clone()) + ), + pallet_roles::Error::::InsufficientRestakingBond + ); + } + + // ========= active validator can switch shared to independent profile ========= + let updated_profile = IndependentRestakeProfile { + records: BoundedVec::try_from(vec![ + Record { + role: RoleType::Tss(ThresholdSignatureRoleType::ZengoGG20Secp256k1), + amount: Some(1000), // <---------- ACTIVE STAKE PRESERVED + }, + Record { + role: RoleType::ZkSaaS(ZeroKnowledgeRoleType::ZkSaaSGroth16), + amount: Some(500), }, ]) .unwrap(), @@ -879,3 +1228,84 @@ fn jobs_validator_checks_work() { } }); } + +#[test] +fn switch_active_independent_profile_to_shared_should_work_if_active_restake_sum_preserved() { + new_test_ext(vec![ALICE, BOB, CHARLIE, DAVE, EVE]).execute_with(|| { + Balances::make_free_balance_be(&mock_pub_key(TEN), 100); + + let participants = vec![ALICE, BOB, CHARLIE, DAVE, EVE]; + + // all validators sign up in roles pallet w/ independent profile + let profile = independent_profile(); + for validator in participants.clone() { + assert_ok!(Roles::create_profile( + RuntimeOrigin::signed(mock_pub_key(validator)), + profile.clone() + )); + } + + // submit job with existing validators + let threshold_signature_role_type = ThresholdSignatureRoleType::ZengoGG20Secp256k1; + let submission = JobSubmission { + expiry: 10, + ttl: 200, + job_type: JobType::DKGTSSPhaseOne(DKGTSSPhaseOneJobType { + participants: participants.clone().iter().map(|x| mock_pub_key(*x)).collect(), + threshold: 3, + permitted_caller: Some(mock_pub_key(TEN)), + role_type: threshold_signature_role_type, + }), + }; + assert_ok!(Jobs::submit_job(RuntimeOrigin::signed(mock_pub_key(TEN)), submission)); + + // ========= active validator can not switch independent to shared profile ========= + let updated_profile = SharedRestakeProfile { + records: BoundedVec::try_from(vec![ + Record { + role: RoleType::Tss(ThresholdSignatureRoleType::ZengoGG20Secp256k1), + amount: None, + }, + Record { + role: RoleType::ZkSaaS(ZeroKnowledgeRoleType::ZkSaaSGroth16), + amount: None, + }, + ]) + .unwrap(), + amount: 400, // <---------- ACTIVE RESTAKE SUM NOT PRESERVED + }; + + for validator in participants.clone() { + assert_noop!( + Roles::update_profile( + RuntimeOrigin::signed(mock_pub_key(validator)), + Profile::Shared(updated_profile.clone()) + ), + pallet_roles::Error::::InsufficientRestakingBond + ); + } + + // ========= active validator can switch independent to shared profile ========= + let updated_profile = SharedRestakeProfile { + records: BoundedVec::try_from(vec![ + Record { + role: RoleType::Tss(ThresholdSignatureRoleType::ZengoGG20Secp256k1), + amount: None, + }, + Record { + role: RoleType::ZkSaaS(ZeroKnowledgeRoleType::ZkSaaSGroth16), + amount: None, + }, + ]) + .unwrap(), + amount: 1500, + }; + + for validator in participants.clone() { + assert_ok!(Roles::update_profile( + RuntimeOrigin::signed(mock_pub_key(validator)), + Profile::Shared(updated_profile.clone()) + )); + } + }); +} diff --git a/pallets/roles/src/benchmarking.rs b/pallets/roles/src/benchmarking.rs index 6815d3ef5..802ec1621 100644 --- a/pallets/roles/src/benchmarking.rs +++ b/pallets/roles/src/benchmarking.rs @@ -149,7 +149,7 @@ mod benchmarks { } #[benchmark] - fn unbound_funds() { + fn unbond_funds() { let caller: T::AccountId = create_validator_account::("Alice"); let amount: T::CurrencyBalance = 2000_u64.into(); diff --git a/pallets/roles/src/impls.rs b/pallets/roles/src/impls.rs index 08361bbf0..3c5feeba5 100644 --- a/pallets/roles/src/impls.rs +++ b/pallets/roles/src/impls.rs @@ -116,7 +116,6 @@ impl Pallet { // sum of the restaking amounts of the current profile. This is because we require // the total amount staked to only increase or remain the same across active roles. if updated_profile.is_shared() && current_ledger.profile.is_independent() { - ensure!(active_jobs.len() == 0, Error::::HasRoleAssigned); if active_jobs.len() > 0 { let mut active_role_restaking_sum = Zero::zero(); for role in roles_with_active_jobs.iter() { @@ -190,7 +189,7 @@ impl Pallet { Ok(()) } - /// Check if account can chill, unbound and withdraw funds. + /// Check if account can chill, unbond and withdraw funds. /// /// # Parameters /// - `account`: The account ID of the validator. @@ -200,7 +199,7 @@ impl Pallet { pub(crate) fn can_exit(account: T::AccountId) -> bool { let assigned_roles = AccountRolesMapping::::get(account); if assigned_roles.is_empty() { - // Role is cleared, account can chill, unbound and withdraw funds. + // Role is cleared, account can chill, unbond and withdraw funds. return true } false diff --git a/pallets/roles/src/lib.rs b/pallets/roles/src/lib.rs index 9d331469e..5378c3cda 100644 --- a/pallets/roles/src/lib.rs +++ b/pallets/roles/src/lib.rs @@ -507,31 +507,31 @@ pub mod pallet { pallet_staking::Pallet::::chill(origin) } - /// Unbound funds from the stash account. - /// This will allow user to unbound and later withdraw funds. + /// Unbond funds from the stash account. + /// This will allow user to unbond and later withdraw funds. /// If you have opted for any of the roles, please submit `clear_role` extrinsic to opt out - /// of all the services. Once your role is cleared, you can unbound + /// of all the services. Once your role is cleared, you can unbond /// and withdraw funds. /// /// # Parameters /// /// - `origin`: Origin of the transaction. - /// - `amount`: Amount of funds to unbound. + /// - `amount`: Amount of funds to unbond. /// /// This function will return error if /// - If there is any active role assigned to the user. /// - #[pallet::weight(::WeightInfo::unbound_funds())] + #[pallet::weight(::WeightInfo::unbond_funds())] #[pallet::call_index(4)] - pub fn unbound_funds( + pub fn unbond_funds( origin: OriginFor, #[pallet::compact] amount: BalanceOf, ) -> DispatchResult { let account = ensure_signed(origin.clone())?; - // Ensure no role is assigned to the account and is eligible to unbound. + // Ensure no role is assigned to the account and is eligible to unbond. ensure!(Self::can_exit(account), Error::::HasRoleAssigned); - // Unbound funds. + // Unbond funds. let res = pallet_staking::Pallet::::unbond(origin, amount); match res { Ok(_) => Ok(()), @@ -539,7 +539,7 @@ pub mod pallet { } } - /// Withdraw unbound funds after un-bonding period has passed. + /// Withdraw unbond funds after un-bonding period has passed. /// /// # Parameters /// @@ -554,7 +554,7 @@ pub mod pallet { // Ensure no role is assigned to the account and is eligible to withdraw. ensure!(Self::can_exit(account), Error::::HasRoleAssigned); - // Withdraw unbound funds. + // Withdraw unbond funds. let res = pallet_staking::Pallet::::withdraw_unbonded(origin, 0); match res { Ok(_) => Ok(()), diff --git a/pallets/roles/src/mock.rs b/pallets/roles/src/mock.rs index 22900ce44..7bf412fa3 100644 --- a/pallets/roles/src/mock.rs +++ b/pallets/roles/src/mock.rs @@ -137,10 +137,10 @@ impl pallet_session::historical::Config for Runtime { pub struct BaseFilter; impl Contains for BaseFilter { fn contains(call: &RuntimeCall) -> bool { - let is_stake_unbound_call = + let is_stake_unbond_call = matches!(call, RuntimeCall::Staking(pallet_staking::Call::unbond { .. })); - if is_stake_unbound_call { + if is_stake_unbond_call { // no unbond call return false } diff --git a/pallets/roles/src/tests.rs b/pallets/roles/src/tests.rs index ce073b679..499e82ec1 100644 --- a/pallets/roles/src/tests.rs +++ b/pallets/roles/src/tests.rs @@ -278,7 +278,7 @@ fn test_delete_profile() { } #[test] -fn test_unbound_funds_should_work() { +fn test_unbond_funds_should_work() { new_test_ext(vec![1, 2, 3, 4]).execute_with(|| { // Lets create shared profile. let profile = shared_profile(); @@ -289,8 +289,8 @@ fn test_unbound_funds_should_work() { assert_eq!(Roles::ledger(mock_pub_key(1)), None); - // unbound funds. - assert_ok!(Roles::unbound_funds(RuntimeOrigin::signed(mock_pub_key(1)), 5000)); + // unbond funds. + assert_ok!(Roles::unbond_funds(RuntimeOrigin::signed(mock_pub_key(1)), 5000)); assert_events(vec![RuntimeEvent::Staking(pallet_staking::Event::Unbonded { stash: mock_pub_key(1), @@ -299,7 +299,7 @@ fn test_unbound_funds_should_work() { // Get pallet staking ledger mapping. let staking_ledger = pallet_staking::Ledger::::get(mock_pub_key(1)).unwrap(); - // Since we we have unbounded 5000 tokens, we should have 5000 tokens in staking ledger. + // Since we we have unbonded 5000 tokens, we should have 5000 tokens in staking ledger. assert_eq!(staking_ledger.active, 5000); }); } diff --git a/pallets/roles/src/weights.rs b/pallets/roles/src/weights.rs index 770add9b4..bcc996f0c 100644 --- a/pallets/roles/src/weights.rs +++ b/pallets/roles/src/weights.rs @@ -36,7 +36,7 @@ pub trait WeightInfo { fn update_profile() -> Weight; fn delete_profile() -> Weight; fn chill() -> Weight; - fn unbound_funds() -> Weight; + fn unbond_funds() -> Weight; fn withdraw_unbonded() -> Weight; } @@ -145,7 +145,7 @@ impl WeightInfo for WebbWeight { /// Proof: `BagsList::ListNodes` (`max_values`: None, `max_size`: Some(154), added: 2629, mode: `MaxEncodedLen`) /// Storage: `Staking::Bonded` (r:1 w:0) /// Proof: `Staking::Bonded` (`max_values`: None, `max_size`: Some(72), added: 2547, mode: `MaxEncodedLen`) - fn unbound_funds() -> Weight { + fn unbond_funds() -> Weight { // Proof Size summary in bytes: // Measured: `2379` // Estimated: `5844` @@ -281,7 +281,7 @@ impl WeightInfo for () { /// Proof: `BagsList::ListNodes` (`max_values`: None, `max_size`: Some(154), added: 2629, mode: `MaxEncodedLen`) /// Storage: `Staking::Bonded` (r:1 w:0) /// Proof: `Staking::Bonded` (`max_values`: None, `max_size`: Some(72), added: 2547, mode: `MaxEncodedLen`) - fn unbound_funds() -> Weight { + fn unbond_funds() -> Weight { // Proof Size summary in bytes: // Measured: `2379` // Estimated: `5844` diff --git a/types/src/interfaces/augment-api-tx.ts b/types/src/interfaces/augment-api-tx.ts index 6e3780be4..545b88099 100644 --- a/types/src/interfaces/augment-api-tx.ts +++ b/types/src/interfaces/augment-api-tx.ts @@ -785,9 +785,9 @@ declare module '@polkadot/api-base/types/submittable' { **/ deleteProfile: AugmentedSubmittable<() => SubmittableExtrinsic, []>; /** - * See [`Pallet::unbound_funds`]. + * See [`Pallet::unbond_funds`]. **/ - unboundFunds: AugmentedSubmittable<(amount: Compact | AnyNumber | Uint8Array) => SubmittableExtrinsic, [Compact]>; + unbondFunds: AugmentedSubmittable<(amount: Compact | AnyNumber | Uint8Array) => SubmittableExtrinsic, [Compact]>; /** * See [`Pallet::update_profile`]. **/ diff --git a/types/src/interfaces/lookup.ts b/types/src/interfaces/lookup.ts index 7fc7fe8c1..0063e054e 100644 --- a/types/src/interfaces/lookup.ts +++ b/types/src/interfaces/lookup.ts @@ -3251,7 +3251,7 @@ export default { }, delete_profile: 'Null', chill: 'Null', - unbound_funds: { + unbond_funds: { amount: 'Compact', }, withdraw_unbonded: 'Null'