From 313a9f3da7349d49f979b5b54fdd3c079723f282 Mon Sep 17 00:00:00 2001 From: Fahrrader Date: Thu, 22 Dec 2022 08:48:20 +0000 Subject: [PATCH] feat(collator-selection): licenses + onboarding --- --- a/node/cli/src/chain_spec.rs +++ b/node/cli/src/chain_spec.rs @@ -24,7 +24,7 @@ use serde_json::map::Map; use up_common::types::opaque::*; -use up_common::constants::{GENESIS_CANDIDACY_BOND, SESSION_LENGTH}; +use up_common::constants::{GENESIS_LICENSE_BOND, SESSION_LENGTH}; #[cfg(feature = "unique-runtime")] pub use unique_runtime as default_runtime; @@ -196,7 +196,7 @@ .cloned() .map(|(acc, _)| acc) .collect(), - candidacy_bond: GENESIS_CANDIDACY_BOND, + license_bond: GENESIS_LICENSE_BOND, kick_threshold: SESSION_LENGTH, ..Default::default() }, --- a/pallets/collator-selection/src/benchmarking.rs +++ b/pallets/collator-selection/src/benchmarking.rs @@ -116,12 +116,12 @@ .map(|c| account("candidate", c, SEED)) .collect::>(); assert!( - >::get() > 0u32.into(), + >::get() > 0u32.into(), "Bond cannot be zero!" ); for who in candidates { - T::Currency::make_free_balance_be(&who, >::get() * 2u32.into()); + T::Currency::make_free_balance_be(&who, >::get() * 2u32.into()); >::register_as_candidate(RawOrigin::Signed(who).into()).unwrap(); } } @@ -154,16 +154,16 @@ assert_last_event::(Event::NewDesiredCandidates{desired_candidates: max}.into()); } - set_candidacy_bond { + set_license_bond { let bond_amount: BalanceOf = T::Currency::minimum_balance() * 10u32.into(); let origin = T::UpdateOrigin::successful_origin(); }: { assert_ok!( - >::set_candidacy_bond(origin, bond_amount.clone()) + >::set_license_bond(origin, bond_amount.clone()) ); } verify { - assert_last_event::(Event::NewCandidacyBond{bond_amount}.into()); + assert_last_event::(Event::NewLicenseBond{bond_amount}.into()); } // worse case is when we have all the max-candidate slots filled except one, and we fill that @@ -171,7 +171,7 @@ register_as_candidate { let c in 1 .. T::MaxCandidates::get(); - >::put(T::Currency::minimum_balance()); + >::put(T::Currency::minimum_balance()); >::put(c + 1); register_validators::(c); @@ -195,7 +195,7 @@ // worse case is the last candidate leaving. leave_intent { let c in (T::MinCandidates::get() + 1) .. T::MaxCandidates::get(); - >::put(T::Currency::minimum_balance()); + >::put(T::Currency::minimum_balance()); >::put(c); register_validators::(c); @@ -211,7 +211,7 @@ // worse case is paying a non-existing candidate account. note_author { - >::put(T::Currency::minimum_balance()); + >::put(T::Currency::minimum_balance()); T::Currency::make_free_balance_be( &>::account_id(), T::Currency::minimum_balance() * 4u32.into(), @@ -233,7 +233,7 @@ let r in 1 .. T::MaxCandidates::get(); let c in 1 .. T::MaxCandidates::get(); - >::put(T::Currency::minimum_balance()); + >::put(T::Currency::minimum_balance()); >::put(c); frame_system::Pallet::::set_block_number(0u32.into()); --- a/pallets/collator-selection/src/lib.rs +++ b/pallets/collator-selection/src/lib.rs @@ -181,7 +181,7 @@ #[derive( PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, scale_info::TypeInfo, MaxEncodedLen, )] - pub struct CandidateInfo { + pub struct LicenseInfo { /// Account identifier. pub who: AccountId, /// Reserved deposit. @@ -198,12 +198,18 @@ pub type Invulnerables = StorageValue<_, BoundedVec, ValueQuery>; + /// The (community) collation license holders. + #[pallet::storage] + #[pallet::getter(fn licenses)] + pub type Licenses = + StorageMap<_, Blake2_128Concat, T::AccountId, BalanceOf, ValueQuery>; + /// The (community, limited) collation candidates. #[pallet::storage] #[pallet::getter(fn candidates)] pub type Candidates = StorageValue< _, - BoundedVec>, T::MaxCandidates>, + BoundedVec, //LicenseInfo>, T::MaxCandidates>, // license ID? ValueQuery, >; @@ -231,13 +237,13 @@ /// /// When a collator calls `leave_intent` they immediately receive the deposit back. #[pallet::storage] - #[pallet::getter(fn candidacy_bond)] - pub type CandidacyBond = StorageValue<_, BalanceOf, ValueQuery>; + #[pallet::getter(fn license_bond)] + pub type LicenseBond = StorageValue<_, BalanceOf, ValueQuery>; #[pallet::genesis_config] pub struct GenesisConfig { pub invulnerables: Vec, - pub candidacy_bond: BalanceOf, + pub license_bond: BalanceOf, pub kick_threshold: T::BlockNumber, pub desired_candidates: u32, } @@ -247,7 +253,7 @@ fn default() -> Self { Self { invulnerables: Default::default(), - candidacy_bond: Default::default(), + license_bond: Default::default(), kick_threshold: T::BlockNumber::one(), desired_candidates: Default::default(), } @@ -275,7 +281,7 @@ ); >::put(&self.desired_candidates); - >::put(&self.candidacy_bond); + >::put(&self.license_bond); >::put(&self.kick_threshold); >::put(bounded_invulnerables); } @@ -287,7 +293,7 @@ NewDesiredCandidates { desired_candidates: u32, }, - NewCandidacyBond { + NewLicenseBond { bond_amount: BalanceOf, }, NewKickThreshold { @@ -299,13 +305,19 @@ InvulnerableRemoved { invulnerable: T::AccountId, }, - CandidateAdded { + LicenseObtained { account_id: T::AccountId, deposit: BalanceOf, }, + LicenseForfeited { + account_id: T::AccountId, + deposit_returned: BalanceOf, + }, + CandidateAdded { + account_id: T::AccountId, + }, CandidateRemoved { account_id: T::AccountId, - deposit_returned: BalanceOf, }, } @@ -320,6 +332,10 @@ Unknown, /// Permission issue Permission, + /// User already holds license to collate + AlreadyLicenseHolder, + /// User does not hold a license to collate + NoLicense, /// User is already a candidate AlreadyCandidate, /// User is not a candidate @@ -363,6 +379,9 @@ return Ok(().into()); } + // todo:collator check license holders, release moneys, promotion! + // force_release_license? Error::::lreadyLicenseHolder? + >::try_append(new.clone()) .map_err(|_| Error::::TooManyInvulnerables)?; Self::deposit_event(Event::InvulnerableAdded { invulnerable: new }); @@ -419,20 +438,20 @@ } /// Set the candidacy bond amount. - #[pallet::weight(T::WeightInfo::set_candidacy_bond())] - pub fn set_candidacy_bond( + #[pallet::weight(T::WeightInfo::set_license_bond())] + pub fn set_license_bond( origin: OriginFor, bond: BalanceOf, ) -> DispatchResultWithPostInfo { T::UpdateOrigin::ensure_origin(origin)?; - >::put(&bond); - Self::deposit_event(Event::NewCandidacyBond { bond_amount: bond }); + >::put(&bond); + Self::deposit_event(Event::NewLicenseBond { bond_amount: bond }); Ok(().into()) } /// Set the length of the kick threshold. /// Note that if the length is not a multiple of the session period, it might get inconsistent. - #[pallet::weight(T::WeightInfo::set_candidacy_bond())] // todo:collator weight + #[pallet::weight(T::WeightInfo::set_license_bond())] // todo:collator weight pub fn set_kick_threshold( origin: OriginFor, kick_threshold: T::BlockNumber, @@ -446,21 +465,20 @@ Ok(().into()) } - /// Register this account as a collator candidate. The account must (a) already have - /// registered session keys and (b) be able to reserve the `CandidacyBond`. + /// Purchase a license on block collation for this account. + /// It does not make it a collator candidate, use `onboard` afterward. The account must + /// (a) already have registered session keys and (b) be able to reserve the `LicenseBond`. /// /// This call is not available to `Invulnerable` collators. - #[pallet::weight(T::WeightInfo::register_as_candidate(T::MaxCandidates::get()))] - pub fn register_as_candidate(origin: OriginFor) -> DispatchResultWithPostInfo { + #[pallet::weight(T::WeightInfo::register_as_candidate(T::MaxCandidates::get()))] // todo:collator weight + pub fn get_license(origin: OriginFor) -> DispatchResultWithPostInfo { + // register_as_candidate let who = ensure_signed(origin)?; - // ensure we are below limit. - let length = >::decode_len().unwrap_or_default(); - ensure!( - (length as u32) < Self::desired_candidates(), - Error::::TooManyCandidates - ); - // todo:collator really need it? + if Licenses::::contains_key(&who) { + return Ok(().into()); + } + ensure!( !Self::invulnerables().contains(&who), Error::::AlreadyInvulnerable @@ -473,21 +491,81 @@ Error::::ValidatorNotRegistered ); - let deposit = Self::candidacy_bond(); + let deposit = Self::license_bond(); // First authored block is current block plus kick threshold to handle session delay - let incoming = CandidateInfo { + /*let incoming = LicenseInfo { who: who.clone(), deposit, - }; + };*/ + T::Currency::reserve(&who, deposit)?; + Licenses::::insert(who.clone(), deposit); + + /*let current_count = + >::try_mutate(|licenses| -> Result { + if T::OriginPrivilegeCmp::cmp_privilege(&origin, &scheduled.origin) { + return Err(BadOrigin.into()); + } + if candidates.iter().any(|candidate| *candidate == who) { + Err(Error::::AlreadyLicenseHolder)? + } else { + T::Currency::reserve(&who, deposit)?; + candidates + .try_push(incoming) + .map_err(|_| Error::::TooManyCandidates)?; + >::insert( + who.clone(), + frame_system::Pallet::::block_number() + Self::kick_threshold(), + ); + Ok(candidates.len()) + } + })?;*/ + + Self::deposit_event(Event::LicenseObtained { + account_id: who, + deposit, + }); + Ok(().into()) // Some(T::WeightInfo::register_as_candidate(current_count as u32)).into()) + } + + /// Register this account as a candidate for collators for next sessions. + /// The account must already hold a license, and cannot offboard immediately during a session. + /// + /// This call is not available to `Invulnerable` collators. + #[pallet::weight(T::WeightInfo::register_as_candidate(T::MaxCandidates::get()))] // todo:collator weight + pub fn onboard(origin: OriginFor) -> DispatchResultWithPostInfo { + // register_as_candidate + let who = ensure_signed(origin)?; + + // ensure the user obtained the license. + ensure!(Licenses::::contains_key(&who), Error::::NoLicense); + // ensure we are below limit. + let length = >::decode_len().unwrap_or_default(); + ensure!( + (length as u32) < Self::desired_candidates(), + Error::::TooManyCandidates + ); + // todo:collator really need it? + ensure!( + !Self::invulnerables().contains(&who), + Error::::AlreadyInvulnerable + ); + + let deposit = Self::license_bond(); + // First authored block is current block plus kick threshold to handle session delay + /*let incoming = LicenseInfo { + who: who.clone(), + deposit, + };*/ + let current_count = >::try_mutate(|candidates| -> Result { - if candidates.iter().any(|candidate| candidate.who == who) { + if candidates.iter().any(|candidate| *candidate == who) { Err(Error::::AlreadyCandidate)? } else { T::Currency::reserve(&who, deposit)?; candidates - .try_push(incoming) + .try_push(who.clone()) .map_err(|_| Error::::TooManyCandidates)?; >::insert( who.clone(), @@ -497,31 +575,59 @@ } })?; - Self::deposit_event(Event::CandidateAdded { - account_id: who, - deposit, - }); + Self::deposit_event(Event::CandidateAdded { account_id: who }); Ok(Some(T::WeightInfo::register_as_candidate(current_count as u32)).into()) } /// Deregister `origin` as a collator candidate. Note that the collator can only leave on - /// session change. The `CandidacyBond` will be unreserved immediately. - /// - /// This call will fail if the total number of candidates would drop below `MinCandidates`. + /// session change. The license to `onboard` later at any other time will remain. /// - /// This call is not available to `Invulnerable` collators. - #[pallet::weight(T::WeightInfo::leave_intent(T::MaxCandidates::get()))] - pub fn leave_intent(origin: OriginFor) -> DispatchResultWithPostInfo { + /// This call will fail if the total number of candidates would drop below `MinCandidates`. todo:collator maybe not + #[pallet::weight(T::WeightInfo::leave_intent(T::MaxCandidates::get()))] // todo:collator weight + pub fn offboard(origin: OriginFor) -> DispatchResultWithPostInfo { + // leave_intent let who = ensure_signed(origin)?; // todo:collator invulnerables and candidates should count against min candidates together ensure!( Self::candidates().len() as u32 > T::MinCandidates::get(), Error::::TooFewCandidates ); - let current_count = Self::try_remove_candidate(&who, false)?; + let current_count = Self::try_remove_candidate(&who)?; Ok(Some(T::WeightInfo::leave_intent(current_count as u32)).into()) } + + /// Forfeit `origin`'s own license. The `LicenseBond` will be unreserved immediately. + /// + /// This call is not available to `Invulnerable` collators. + #[pallet::weight(T::WeightInfo::leave_intent(T::MaxCandidates::get()))] // todo:collator weight + pub fn release_license(origin: OriginFor) -> DispatchResultWithPostInfo { + // leave_intent + let who = ensure_signed(origin)?; + // let current_count = Self::try_remove_candidate(&who, false)?; + Self::try_release_license(&who, false)?; + + Ok(().into()) + } + + /// Force deregister `origin` as a collator candidate as a governing authority, and revoke its license. + /// Note that the collator can only leave on session change. + /// The `LicenseBond` will be unreserved and returned immediately. + /// + /// This call is not available to `Invulnerable` collators. + #[pallet::weight(T::WeightInfo::leave_intent(T::MaxCandidates::get()))] // todo:collator weight + pub fn force_release_license( + origin: OriginFor, + who: T::AccountId, + ) -> DispatchResultWithPostInfo { + // leave_intent + T::UpdateOrigin::ensure_origin(origin)?; + + let current_count = Self::try_remove_candidate(&who)?; + Self::try_release_license(&who, false)?; + + Ok(Some(T::WeightInfo::leave_intent(current_count as u32)).into()) // todo:collator weight + } } impl Pallet { @@ -530,21 +636,29 @@ T::PotId::get().into_account_truncating() } - /// Removes a candidate if they exist and sends them back their deposit, optionally slashed. - fn try_remove_candidate( - who: &T::AccountId, - should_slash: bool, - ) -> Result { - let mut deposit_returned = BalanceOf::::default(); + /// Removes a candidate from the collator pool for the next session if they exist. + fn try_remove_candidate(who: &T::AccountId) -> Result { let current_count = >::try_mutate(|candidates| -> Result { let index = candidates .iter() - .position(|candidate| candidate.who == *who) + .position(|candidate| *candidate == *who) .ok_or(Error::::NotCandidate)?; - let candidate = candidates.remove(index); - let deposit = candidate.deposit; + candidates.remove(index); + >::remove(who.clone()); + Ok(candidates.len()) + })?; + Self::deposit_event(Event::CandidateRemoved { + account_id: who.clone(), + }); + Ok(current_count) + } + /// Removes a candidate if they exist and sends them back their deposit, optionally slashed. + fn try_release_license(who: &T::AccountId, should_slash: bool) -> DispatchResult { + let mut deposit_returned = BalanceOf::::default(); + Licenses::::try_mutate_exists(&who, |deposit| -> DispatchResult { + if let Some(deposit) = deposit.take() { if should_slash { let slashed = T::SlashRatio::get() * deposit; let remaining = deposit - slashed; @@ -554,23 +668,22 @@ deposit_returned = remaining; T::Currency::resolve_creating(&T::TreasuryAccountId::get(), imbalance); - - // Self::deposit_event(Event::CandidateSlashed(who.clone())); } else { //T::Currency::unreserve(who, deposit); deposit_returned = deposit; } T::Currency::unreserve(who, deposit_returned); - // candidates.remove(index); - >::remove(who.clone()); - Ok(candidates.len()) - })?; - Self::deposit_event(Event::CandidateRemoved { + Ok(()) + } else { + Err(Error::::NoLicense.into()) + } + })?; + Self::deposit_event(Event::LicenseForfeited { account_id: who.clone(), deposit_returned, }); - Ok(current_count) + Ok(()) } /// Assemble the current set of candidates and invulnerables into the next collator set. @@ -587,24 +700,30 @@ /// Kicks out candidates that did not produce a block in the kick threshold /// and **confiscates** their deposits to the treasury. pub fn kick_stale_candidates( - candidates: BoundedVec>, T::MaxCandidates>, + candidates: BoundedVec, //LicenseInfo> ) -> BoundedVec { let now = frame_system::Pallet::::block_number(); let kick_threshold = Self::kick_threshold(); candidates .into_iter() .filter_map(|c| { - let last_block = >::get(c.who.clone()); + let last_block = >::get(c.clone()); let since_last = now.saturating_sub(last_block); if since_last < kick_threshold || Self::candidates().len() as u32 <= T::MinCandidates::get() { - Some(c.who) + Some(c) } else { - let outcome = Self::try_remove_candidate(&c.who, true); + let outcome = Self::try_remove_candidate(&c); if let Err(why) = outcome { log::warn!("Failed to remove candidate {:?}", why); debug_assert!(false, "failed to remove candidate {:?}", why); + return None; + } + let outcome = Self::try_release_license(&c, true); + if let Err(why) = outcome { + log::warn!("Failed to release license {:?}", why); + debug_assert!(false, "failed to release license {:?}", why); } None } --- a/pallets/collator-selection/src/mock.rs +++ b/pallets/collator-selection/src/mock.rs @@ -264,7 +264,7 @@ .collect::>(); let collator_selection = collator_selection::GenesisConfig:: { desired_candidates: 2, - candidacy_bond: 10, + license_bond: 10, kick_threshold: 1, invulnerables, }; --- a/pallets/collator-selection/src/tests.rs +++ b/pallets/collator-selection/src/tests.rs @@ -31,7 +31,7 @@ // limitations under the License. use crate as collator_selection; -use crate::{mock::*, CandidateInfo, Error}; +use crate::{mock::*, LicenseInfo, Error}; use frame_support::{ assert_noop, assert_ok, traits::{Currency, GenesisBuild, OnInitialize}, @@ -43,7 +43,7 @@ fn basic_setup_works() { new_test_ext().execute_with(|| { assert_eq!(CollatorSelection::desired_candidates(), 2); - assert_eq!(CollatorSelection::candidacy_bond(), 10); + assert_eq!(CollatorSelection::license_bond(), 10); assert!(CollatorSelection::candidates().is_empty()); assert_eq!(CollatorSelection::invulnerables(), vec![1, 2]); @@ -133,21 +133,21 @@ } #[test] -fn set_candidacy_bond() { +fn set_license_bond() { new_test_ext().execute_with(|| { // given - assert_eq!(CollatorSelection::candidacy_bond(), 10); + assert_eq!(CollatorSelection::license_bond(), 10); // can set - assert_ok!(CollatorSelection::set_candidacy_bond( + assert_ok!(CollatorSelection::set_license_bond( RuntimeOrigin::signed(RootAccount::get()), 7 )); - assert_eq!(CollatorSelection::candidacy_bond(), 7); + assert_eq!(CollatorSelection::license_bond(), 7); // rejects bad origin. assert_noop!( - CollatorSelection::set_candidacy_bond(RuntimeOrigin::signed(1), 8), + CollatorSelection::set_license_bond(RuntimeOrigin::signed(1), 8), BadOrigin ); }); @@ -227,7 +227,7 @@ assert_ok!(CollatorSelection::register_as_candidate( RuntimeOrigin::signed(3) )); - let addition = CandidateInfo { + let addition = LicenseInfo { who: 3, deposit: 10, }; @@ -267,7 +267,7 @@ new_test_ext().execute_with(|| { // given assert_eq!(CollatorSelection::desired_candidates(), 2); - assert_eq!(CollatorSelection::candidacy_bond(), 10); + assert_eq!(CollatorSelection::license_bond(), 10); assert_eq!(CollatorSelection::candidates(), Vec::new()); assert_eq!(CollatorSelection::invulnerables(), vec![1, 2]); @@ -331,7 +331,7 @@ // triggers `note_author` Authorship::on_initialize(1); - let collator = CandidateInfo { + let collator = LicenseInfo { who: 4, deposit: 10, }; @@ -361,7 +361,7 @@ // triggers `note_author` Authorship::on_initialize(1); - let collator = CandidateInfo { + let collator = LicenseInfo { who: 4, deposit: 10, }; @@ -432,7 +432,7 @@ assert_eq!(CollatorSelection::candidates().len(), 1); // 3 will be kicked after 1 session delay assert_eq!(SessionHandlerCollators::get(), vec![1, 2, 3, 4]); - let collator = CandidateInfo { + let collator = LicenseInfo { who: 4, deposit: 10, }; @@ -465,7 +465,7 @@ assert_eq!(CollatorSelection::candidates().len(), 1); // 3 will be kicked after 1 session delay assert_eq!(SessionHandlerCollators::get(), vec![1, 2, 3, 5]); - let collator = CandidateInfo { + let collator = LicenseInfo { who: 5, deposit: 10, }; @@ -490,7 +490,7 @@ let collator_selection = collator_selection::GenesisConfig:: { desired_candidates: 2, - candidacy_bond: 10, + license_bond: 10, kick_threshold: 1, invulnerables, }; --- a/pallets/collator-selection/src/weights.rs +++ b/pallets/collator-selection/src/weights.rs @@ -46,7 +46,7 @@ pub trait WeightInfo { fn set_invulnerables(_b: u32) -> Weight; fn set_desired_candidates() -> Weight; - fn set_candidacy_bond() -> Weight; + fn set_license_bond() -> Weight; fn register_as_candidate(_c: u32) -> Weight; fn leave_intent(_c: u32) -> Weight; fn note_author() -> Weight; @@ -65,7 +65,7 @@ fn set_desired_candidates() -> Weight { Weight::from_ref_time(16_363_000 as u64).saturating_add(T::DbWeight::get().writes(1 as u64)) } - fn set_candidacy_bond() -> Weight { + fn set_license_bond() -> Weight { Weight::from_ref_time(16_840_000 as u64).saturating_add(T::DbWeight::get().writes(1 as u64)) } fn register_as_candidate(c: u32) -> Weight { @@ -112,7 +112,7 @@ Weight::from_ref_time(16_363_000 as u64) .saturating_add(RocksDbWeight::get().writes(1 as u64)) } - fn set_candidacy_bond() -> Weight { + fn set_license_bond() -> Weight { Weight::from_ref_time(16_840_000 as u64) .saturating_add(RocksDbWeight::get().writes(1 as u64)) } --- a/primitives/common/src/constants.rs +++ b/primitives/common/src/constants.rs @@ -45,9 +45,9 @@ /// Minimum balance required to create or keep an account open. pub const EXISTENTIAL_DEPOSIT: u128 = 0; /// Amount of Balance reserved for candidate registration. -pub const GENESIS_CANDIDACY_BOND: u128 = EXISTENTIAL_DEPOSIT; +pub const GENESIS_LICENSE_BOND: u128 = EXISTENTIAL_DEPOSIT; /// How long a periodic session lasts in blocks. -pub const SESSION_LENGTH: BlockNumber = HOURS; +pub const SESSION_LENGTH: BlockNumber = MINUTES; // Targeting 0.1 UNQ per transfer pub const WEIGHT_TO_FEE_COEFF: u32 = /**/175_199_920/**/; --- a/runtime/common/mod.rs +++ b/runtime/common/mod.rs @@ -191,7 +191,7 @@ RuntimeAppPublic, }; use pallet_session::SessionManager; - use up_common::constants::GENESIS_CANDIDACY_BOND; + use up_common::constants::GENESIS_LICENSE_BOND; use crate::config::pallets::collator_selection::MaxInvulnerables; let mut weight = ::DbWeight::get().reads(1); @@ -242,7 +242,7 @@ >::put(bounded_invulnerables); >::put(0); - >::put(GENESIS_CANDIDACY_BOND); + >::put(GENESIS_LICENSE_BOND); let keys = invulnerables .into_iter() -- gitstuff