From 1614b9c49c5605ecd24495e18947b884de6da4e0 Mon Sep 17 00:00:00 2001 From: 4meta5 Date: Mon, 18 Jan 2021 13:02:23 -0500 Subject: [PATCH 01/23] rename CandidateState to Validator and redefine to match Exposure --- pallets/stake/src/lib.rs | 58 ++++++++++++++++++++-------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/pallets/stake/src/lib.rs b/pallets/stake/src/lib.rs index de2e8372ca..882322fa07 100644 --- a/pallets/stake/src/lib.rs +++ b/pallets/stake/src/lib.rs @@ -127,9 +127,10 @@ impl Default for ValidatorStatus { } #[derive(Encode, Decode, RuntimeDebug)] -pub struct CandidateState { - pub validator: AccountId, +pub struct Validator { + pub account: AccountId, pub fee: Perbill, + pub bond: Balance, pub nominators: OrderedSet>, pub total: Balance, pub state: ValidatorStatus, @@ -138,19 +139,15 @@ pub struct CandidateState { impl< A: Ord + Clone, B: AtLeast32BitUnsigned + Ord + Copy + sp_std::ops::AddAssign + sp_std::ops::SubAssign, - C: Ord + Copy, - > CandidateState + > Validator { - pub fn new(validator: A, fee: Perbill, bond: B) -> Self { - let nominators = OrderedSet::from(vec![Bond { - owner: validator.clone(), - amount: bond, - }]); + pub fn new(account: A, fee: Perbill, bond: B) -> Self { let total = bond; - CandidateState { - validator, + Validator { + account, fee, - nominators, + bond, + nominators: OrderedSet::new(), total, state: ValidatorStatus::default(), // default active } @@ -171,26 +168,17 @@ impl< pub fn go_online(&mut self) { self.state = ValidatorStatus::Active; } - pub fn leave_candidates(&mut self, block: C) { + pub fn leave_candidates(&mut self, block: RoundIndex) { self.state = ValidatorStatus::Leaving(block); } } -impl Into> for CandidateState { +impl Into> for Validator { fn into(self) -> Exposure { - let mut others = Vec::>::new(); - let mut own = Zero::zero(); - for Bond { owner, amount } in self.nominators.0 { - if owner == self.validator { - own = amount; - } else { - others.push(Bond { owner, amount }.into()); - } - } Exposure { total: self.total, - own, - others, + own: self.bond, + others: self.nominators.0.into_iter().map(|x| x.into()).collect(), } } } @@ -198,7 +186,7 @@ impl Into> for CandidateSt type RoundIndex = u32; type RewardPoint = u32; type BalanceOf = <::Currency as Currency<::AccountId>>::Balance; -type Candidate = CandidateState<::AccountId, BalanceOf, RoundIndex>; +type Candidate = Validator<::AccountId, BalanceOf>; pub trait Config: System { /// The overarching event type @@ -353,7 +341,7 @@ decl_module! { Error::::CandidateExists ); T::Currency::reserve(&acc,bond)?; - let candidate: Candidate = CandidateState::new(acc.clone(),fee,bond); + let candidate: Candidate = Validator::new(acc.clone(),fee,bond); let new_total = >::get() + bond; >::put(new_total); >::insert(&acc,candidate); @@ -532,7 +520,7 @@ impl Module { continue; } if let Some(state) = >::get(&val) { - if state.nominators.0.len() == 1usize { + if state.nominators.0.len() == 0usize { // solo validator with no nominators if let Some(imb) = T::Currency::deposit_into_existing(&val, amt_due).ok() { Self::deposit_event(RawEvent::Rewarded(val.clone(), imb.peek())); @@ -551,6 +539,16 @@ impl Module { Self::deposit_event(RawEvent::Rewarded(owner.clone(), imb.peek())); } } + let pct = Perbill::from_rational_approximation(state.bond, state.total); + let due = pct * amt_due; + if let Some(imb) = + T::Currency::deposit_into_existing(&state.account, due).ok() + { + Self::deposit_event(RawEvent::Rewarded( + state.account.clone(), + imb.peek(), + )); + } } } } @@ -566,9 +564,11 @@ impl Module { } else { if let Some(state) = >::get(&x.owner) { for bond in state.nominators.0 { - // return funds to nominator + // return stake to nominator T::Currency::unreserve(&bond.owner, bond.amount); } + // return stake to validator + T::Currency::unreserve(&state.account, state.bond); let new_total = >::get() - state.total; >::put(new_total); >::remove(&x.owner); From c439d7f1937ca1a1cfefb9d387dfc7468d3e4db0 Mon Sep 17 00:00:00 2001 From: 4meta5 Date: Mon, 18 Jan 2021 19:14:09 -0500 Subject: [PATCH 02/23] feat multiple nominations --- pallets/stake/src/lib.rs | 242 ++++++++++++++++++++++++++++++-------- pallets/stake/src/mock.rs | 3 + runtime/src/lib.rs | 4 + 3 files changed, 197 insertions(+), 52 deletions(-) diff --git a/pallets/stake/src/lib.rs b/pallets/stake/src/lib.rs index 882322fa07..151d790dc8 100644 --- a/pallets/stake/src/lib.rs +++ b/pallets/stake/src/lib.rs @@ -183,6 +183,59 @@ impl Into> for Validator { + pub nominations: OrderedSet>, + pub total: Balance, +} + +impl + Nominator +{ + pub fn new(validator: AccountId, nomination: Balance) -> Self { + Nominator { + nominations: OrderedSet::from(vec![Bond { + owner: validator.clone(), + amount: nomination, + }]), + total: nomination, + } + } + pub fn add_nomination(&mut self, bond: Bond) -> bool { + let amt = bond.amount; + if self.nominations.insert(bond) { + self.total += amt; + true + } else { + false + } + } + // returns remaining balance, must be more than MinNominatorStk + pub fn sub_nomination(&mut self, validator: AccountId) -> Option { + let mut amt: Option = None; + let nominations = self + .nominations + .0 + .iter() + .filter_map(|x| { + if x.owner == validator { + amt = Some(x.amount); + None + } else { + Some(x.clone()) + } + }) + .collect(); + if let Some(balance) = amt { + self.nominations = OrderedSet::from(nominations); + self.total -= balance; + Some(self.total) + } else { + None + } + } +} + type RoundIndex = u32; type RewardPoint = u32; type BalanceOf = <::Currency as Currency<::AccountId>>::Balance; @@ -201,12 +254,16 @@ pub trait Config: System { type MaxValidators: Get; /// Maximum nominators per validator type MaxNominatorsPerValidator: Get; + /// Maximum validators per nominator + type MaxValidatorsPerNominator: Get; /// Balance issued as rewards per round (constant issuance) type IssuancePerRound: Get>; /// Maximum fee for any validator type MaxFee: Get; /// Minimum stake for any registered on-chain account to become a validator type MinValidatorStk: Get>; + /// Minimum stake for any registered on-chain account to nominate + type MinNomination: Get>; /// Minimum stake for any registered on-chain account to become a nominator type MinNominatorStk: Get>; } @@ -230,10 +287,14 @@ decl_event!( ValidatorScheduledExit(RoundIndex, AccountId, RoundIndex), /// Account, Amount Unlocked, New Total Amt Locked ValidatorLeft(AccountId, Balance, Balance), - /// Nominator, Validator, Amount Unstaked, New Total Amt Staked for Validator - NominatorLeft(AccountId, AccountId, Balance, Balance), + /// Nominator, Amount Staked + NominatorJoined(AccountId, Balance), + /// Nominator, Amount Unstaked + NominatorLeft(AccountId, Balance), /// Nominator, Amount Locked, Validator, New Total Amt Locked ValidatorNominated(AccountId, Balance, AccountId, Balance), + /// Nominator, Validator, Amount Unstaked, New Total Amt Staked for Validator + NominatorLeftValidator(AccountId, AccountId, Balance, Balance), Rewarded(AccountId, Balance), } ); @@ -250,11 +311,16 @@ decl_error! { FeeOverMax, ValBondBelowMin, NomBondBelowMin, + NominationBelowMin, AlreadyOffline, AlreadyActive, AlreadyLeaving, TooManyNominators, CannotActivateIfLeaving, + ExceedMaxValidatorsPerNom, + AlreadyNominatedValidator, + MustNominateAtLeastOne, + NominationDNE, } } @@ -263,7 +329,8 @@ decl_storage! { /// Current round, incremented every `BlocksPerRound` in `fn on_finalize` Round: RoundIndex; /// Current nominators with their validator - Nominators: map hasher(blake2_128_concat) T::AccountId => Option; + Nominators: map + hasher(blake2_128_concat) T::AccountId => Option>>; /// Current candidates with associated state Candidates: map hasher(blake2_128_concat) T::AccountId => Option>; /// Current validator set @@ -296,7 +363,7 @@ decl_storage! { "Stash does not have enough balance to bond." ); let _ = if let Some(nominated_val) = opt_val { - >::nominate( + >::join_nominators( T::Origin::from(Some(actor.clone()).into()), nominated_val.clone(), balance, @@ -403,68 +470,68 @@ decl_module! { Ok(()) } #[weight = 0] - fn nominate( + fn join_nominators( origin, validator: T::AccountId, amount: BalanceOf, ) -> DispatchResult { let acc = ensure_signed(origin)?; + ensure!(amount >= T::MinNominatorStk::get(), Error::::NomBondBelowMin); ensure!(!Self::is_nominator(&acc),Error::::NominatorExists); ensure!(!Self::is_candidate(&acc),Error::::CandidateExists); - let mut state = >::get(&validator).ok_or(Error::::CandidateDNE)?; - ensure!(amount >= T::MinNominatorStk::get(), Error::::NomBondBelowMin); - let nomination = Bond { - owner: acc.clone(), - amount, - }; - ensure!(state.nominators.insert(nomination),Error::::NominatorExists); + Self::nominator_joins_validator(acc.clone(), amount, validator.clone())?; + >::insert(&acc, Nominator::new(validator.clone(),amount)); + Self::deposit_event(RawEvent::NominatorJoined(acc,amount)); + Ok(()) + } + #[weight = 0] + fn nominate( + origin, + validator: T::AccountId, + amount: BalanceOf, + ) -> DispatchResult { + let acc = ensure_signed(origin)?; + ensure!(amount >= T::MinNomination::get(), Error::::NominationBelowMin); + let mut nominator = >::get(&acc).ok_or(Error::::NominatorDNE)?; ensure!( - state.nominators.0.len() <= T::MaxNominatorsPerValidator::get(), - Error::::TooManyNominators + nominator.nominations.0.len() < T::MaxValidatorsPerNominator::get(), + Error::::ExceedMaxValidatorsPerNom ); - T::Currency::reserve(&acc,amount)?; - let new_total = state.total + amount; - if state.is_active() { - Self::update_active_candidate(validator.clone(),new_total); - } - let new_total_locked = >::get() + amount; - >::put(new_total_locked); - >::insert(&acc,validator.clone()); - state.total = new_total; - >::insert(&validator,state); - Self::deposit_event(RawEvent::ValidatorNominated(acc,amount,validator,new_total)); + ensure!( + nominator.add_nomination(Bond{owner:validator.clone(), amount}), + Error::::AlreadyNominatedValidator + ); + Self::nominator_joins_validator(acc.clone(), amount, validator.clone())?; + >::insert(&acc, nominator); + Ok(()) + } + #[weight = 0] + fn revoke_nominate(origin, validator: T::AccountId) -> DispatchResult { + let acc = ensure_signed(origin)?; + let mut nominator = >::get(&acc).ok_or(Error::::NominatorDNE)?; + ensure!( + nominator.nominations.0.len() > 1usize, + Error::::MustNominateAtLeastOne + ); + let remaining = nominator.sub_nomination(validator.clone()) + .ok_or(Error::::NominationDNE)?; + ensure!( + remaining >= T::MinNominatorStk::get(), + Error::::NomBondBelowMin + ); + Self::nominator_leaves_validator(acc.clone(), validator.clone())?; + >::insert(&acc, nominator); Ok(()) } #[weight = 0] fn leave_nominators(origin) -> DispatchResult { - let nominator = ensure_signed(origin)?; - let validator = >::get(&nominator).ok_or(Error::::NominatorDNE)?; - let mut state = >::get(&validator).ok_or(Error::::CandidateDNE)?; - let mut exists: Option> = None; - let noms = state.nominators.0.into_iter().filter_map(|nom| { - if nom.owner != nominator { - Some(nom) - } else { - exists = Some(nom.amount); - None - } - }).collect(); - let nominators = OrderedSet::from(noms); - let nominator_stake = exists.ok_or(Error::::NominatorDNE)?; - T::Currency::unreserve(&nominator,nominator_stake); - state.nominators = nominators; - let new_total = state.total - nominator_stake; - if state.is_active() { - Self::update_active_candidate(validator.clone(),new_total); + let acc = ensure_signed(origin)?; + let nominator = >::get(&acc).ok_or(Error::::NominatorDNE)?; + for bond in nominator.nominations.0 { + Self::nominator_leaves_validator(acc.clone(), bond.owner.clone())?; } - state.total = new_total; - let new_total_locked = >::get() - nominator_stake; - >::put(new_total_locked); - >::insert(&validator,state); - >::remove(&nominator); - Self::deposit_event( - RawEvent::NominatorLeft(nominator,validator,nominator_stake,new_total) - ); + >::remove(&acc); + Self::deposit_event(RawEvent::NominatorLeft(acc.clone(), nominator.total)); Ok(()) } fn on_finalize(n: T::BlockNumber) { @@ -504,6 +571,77 @@ impl Module { }); >::put(candidates); } + fn nominator_joins_validator( + nominator: T::AccountId, + amount: BalanceOf, + validator: T::AccountId, + ) -> DispatchResult { + let mut state = >::get(&validator).ok_or(Error::::CandidateDNE)?; + let nomination = Bond { + owner: nominator.clone(), + amount, + }; + ensure!( + state.nominators.insert(nomination), + Error::::NominatorExists + ); + ensure!( + state.nominators.0.len() <= T::MaxNominatorsPerValidator::get(), + Error::::TooManyNominators + ); + T::Currency::reserve(&nominator, amount)?; + let new_total = state.total + amount; + if state.is_active() { + Self::update_active_candidate(validator.clone(), new_total); + } + let new_total_locked = >::get() + amount; + >::put(new_total_locked); + state.total = new_total; + >::insert(&validator, state); + Self::deposit_event(RawEvent::ValidatorNominated( + nominator, amount, validator, new_total, + )); + Ok(()) + } + fn nominator_leaves_validator( + nominator: T::AccountId, + validator: T::AccountId, + ) -> DispatchResult { + let mut state = >::get(&validator).ok_or(Error::::CandidateDNE)?; + let mut exists: Option> = None; + let noms = state + .nominators + .0 + .into_iter() + .filter_map(|nom| { + if nom.owner != nominator { + Some(nom) + } else { + exists = Some(nom.amount); + None + } + }) + .collect(); + let nominators = OrderedSet::from(noms); + let nominator_stake = exists.ok_or(Error::::NominatorDNE)?; + T::Currency::unreserve(&nominator, nominator_stake); + state.nominators = nominators; + let new_total = state.total - nominator_stake; + if state.is_active() { + Self::update_active_candidate(validator.clone(), new_total); + } + state.total = new_total; + let new_total_locked = >::get() - nominator_stake; + >::put(new_total_locked); + >::insert(&validator, state); + Self::deposit_event(RawEvent::NominatorLeftValidator( + nominator, + validator, + nominator_stake, + new_total, + )); + Ok(()) + } fn pay_stakers(next: RoundIndex) { let duration = T::BondDuration::get(); if next > duration { diff --git a/pallets/stake/src/mock.rs b/pallets/stake/src/mock.rs index 1650549a75..799e882827 100644 --- a/pallets/stake/src/mock.rs +++ b/pallets/stake/src/mock.rs @@ -99,6 +99,7 @@ parameter_types! { pub const BondDuration: u32 = 2; pub const MaxValidators: u32 = 5; pub const MaxNominatorsPerValidator: usize = 10; + pub const MaxValidatorsPerNominator: usize = 5; pub const IssuancePerRound: u128 = 10; pub const MaxFee: Perbill = Perbill::from_percent(50); pub const MinValidatorStk: u128 = 10; @@ -111,9 +112,11 @@ impl Config for Test { type BondDuration = BondDuration; type MaxValidators = MaxValidators; type MaxNominatorsPerValidator = MaxNominatorsPerValidator; + type MaxValidatorsPerNominator = MaxValidatorsPerNominator; type IssuancePerRound = IssuancePerRound; type MaxFee = MaxFee; type MinValidatorStk = MinValidatorStk; + type MinNomination = MinNominatorStk; type MinNominatorStk = MinNominatorStk; } pub type Balances = pallet_balances::Module; diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 35b91ca563..b28615bc4d 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -324,6 +324,8 @@ parameter_types! { pub const MaxValidators: u32 = 8; /// Maximum 10 nominators per validator pub const MaxNominatorsPerValidator: usize = 10; + /// Maximum 8 validators per nominator (same as MaxValidators) + pub const MaxValidatorsPerNominator: usize = 8; /// Issue 49 new tokens as rewards to validators every 2 minutes (round) pub const IssuancePerRound: u128 = 49 * GLMR; /// The maximum percent a validator can take off the top of its rewards is 50% @@ -340,9 +342,11 @@ impl stake::Config for Runtime { type BondDuration = BondDuration; type MaxValidators = MaxValidators; type MaxNominatorsPerValidator = MaxNominatorsPerValidator; + type MaxValidatorsPerNominator = MaxValidatorsPerNominator; type IssuancePerRound = IssuancePerRound; type MaxFee = MaxFee; type MinValidatorStk = MinValidatorStk; + type MinNomination = MinNominatorStk; type MinNominatorStk = MinNominatorStk; } impl author_inherent::Config for Runtime { From 36036589e54c64b3d4d2776cb76d10ce60e77b2d Mon Sep 17 00:00:00 2001 From: 4meta5 Date: Tue, 19 Jan 2021 01:07:27 -0500 Subject: [PATCH 03/23] clean --- pallets/stake/src/lib.rs | 47 ++++++++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/pallets/stake/src/lib.rs b/pallets/stake/src/lib.rs index 151d790dc8..b5ca4f9c52 100644 --- a/pallets/stake/src/lib.rs +++ b/pallets/stake/src/lib.rs @@ -38,9 +38,9 @@ //! stored in the `ExitQueue` and processed `BondDuration` rounds later to unstake the validator //! and all of its nominators. //! -//! To join the set of nominators, an account must not be a validator candidate nor an existing -//! nominator. To join the set of nominators, an account must call `join_nominators` with -//! stake >= `MinNominatorStk`. +//! To join the set of nominators, an account must call `join_nominators` with +//! stake >= `MinNominatorStk`. There are also runtime methods for nominating additional validators +//! and revoking nominations. #![recursion_limit = "256"] #![cfg_attr(not(feature = "std"), no_std)] @@ -506,22 +506,8 @@ decl_module! { Ok(()) } #[weight = 0] - fn revoke_nominate(origin, validator: T::AccountId) -> DispatchResult { - let acc = ensure_signed(origin)?; - let mut nominator = >::get(&acc).ok_or(Error::::NominatorDNE)?; - ensure!( - nominator.nominations.0.len() > 1usize, - Error::::MustNominateAtLeastOne - ); - let remaining = nominator.sub_nomination(validator.clone()) - .ok_or(Error::::NominationDNE)?; - ensure!( - remaining >= T::MinNominatorStk::get(), - Error::::NomBondBelowMin - ); - Self::nominator_leaves_validator(acc.clone(), validator.clone())?; - >::insert(&acc, nominator); - Ok(()) + fn revoke_nomination(origin, validator: T::AccountId) -> DispatchResult { + Self::nominator_revokes_validator(ensure_signed(origin)?, validator.clone()) } #[weight = 0] fn leave_nominators(origin) -> DispatchResult { @@ -603,6 +589,19 @@ impl Module { )); Ok(()) } + fn nominator_revokes_validator(acc: T::AccountId, validator: T::AccountId) -> DispatchResult { + let mut nominator = >::get(&acc).ok_or(Error::::NominatorDNE)?; + let remaining = nominator + .sub_nomination(validator.clone()) + .ok_or(Error::::NominationDNE)?; + ensure!( + remaining >= T::MinNominatorStk::get(), + Error::::NomBondBelowMin + ); + Self::nominator_leaves_validator(acc.clone(), validator.clone())?; + >::insert(&acc, nominator); + Ok(()) + } fn nominator_leaves_validator( nominator: T::AccountId, validator: T::AccountId, @@ -704,6 +703,16 @@ impl Module { for bond in state.nominators.0 { // return stake to nominator T::Currency::unreserve(&bond.owner, bond.amount); + // remove nomination from nominator state + if let Some(mut nominator) = >::get(&bond.owner) { + if let Some(remaining) = nominator.sub_nomination(x.owner.clone()) { + if remaining.is_zero() { + >::remove(&bond.owner); + } else { + >::insert(&bond.owner, nominator); + } + } + } } // return stake to validator T::Currency::unreserve(&state.account, state.bond); From 11da16c619195cf0aa90224349b55c43e4834aeb Mon Sep 17 00:00:00 2001 From: 4meta5 Date: Tue, 19 Jan 2021 11:29:53 -0500 Subject: [PATCH 04/23] unit test multiple nominations --- pallets/stake/src/mock.rs | 58 ++++++++- pallets/stake/src/tests.rs | 256 ++++++++++++++++++------------------- 2 files changed, 180 insertions(+), 134 deletions(-) diff --git a/pallets/stake/src/mock.rs b/pallets/stake/src/mock.rs index 799e882827..121fef3436 100644 --- a/pallets/stake/src/mock.rs +++ b/pallets/stake/src/mock.rs @@ -99,7 +99,7 @@ parameter_types! { pub const BondDuration: u32 = 2; pub const MaxValidators: u32 = 5; pub const MaxNominatorsPerValidator: usize = 10; - pub const MaxValidatorsPerNominator: usize = 5; + pub const MaxValidatorsPerNominator: usize = 4; pub const IssuancePerRound: u128 = 10; pub const MaxFee: Perbill = Perbill::from_percent(50); pub const MinValidatorStk: u128 = 10; @@ -196,6 +196,48 @@ pub fn genesis2() -> sp_io::TestExternalities { ext } +pub fn genesis3() -> sp_io::TestExternalities { + let mut storage = frame_system::GenesisConfig::default() + .build_storage::() + .unwrap(); + let genesis = pallet_balances::GenesisConfig:: { + balances: vec![ + (1, 100), + (2, 100), + (3, 100), + (4, 100), + (5, 100), + (6, 100), + (7, 100), + (8, 100), + (9, 100), + (10, 100), + ], + }; + genesis.assimilate_storage(&mut storage).unwrap(); + GenesisConfig:: { + stakers: vec![ + // validators + (1, None, 20), + (2, None, 20), + (3, None, 20), + (4, None, 20), + (5, None, 10), + // nominators + (6, Some(1), 10), + (7, Some(1), 10), + (8, Some(2), 10), + (9, Some(2), 10), + (10, Some(1), 10), + ], + } + .assimilate_storage(&mut storage) + .unwrap(); + let mut ext = sp_io::TestExternalities::from(storage); + ext.execute_with(|| Sys::set_block_number(1)); + ext +} + pub fn roll_to(n: u64) { while Sys::block_number() < n { Stake::on_finalize(Sys::block_number()); @@ -211,3 +253,17 @@ pub fn roll_to(n: u64) { pub fn last_event() -> MetaEvent { Sys::events().pop().expect("Event expected").event } + +pub fn events() -> Vec> { + Sys::events() + .into_iter() + .map(|r| r.event) + .filter_map(|e| { + if let MetaEvent::stake(inner) = e { + Some(inner) + } else { + None + } + }) + .collect::>() +} diff --git a/pallets/stake/src/tests.rs b/pallets/stake/src/tests.rs index 931e351809..2353c41482 100644 --- a/pallets/stake/src/tests.rs +++ b/pallets/stake/src/tests.rs @@ -21,7 +21,7 @@ use mock::*; use sp_runtime::DispatchError; #[test] -fn genesis_config_works() { +fn genesis_works() { genesis().execute_with(|| { assert!(Sys::events().is_empty()); // validators @@ -50,6 +50,28 @@ fn genesis_config_works() { }); } +#[test] +fn genesis3_works() { + genesis3().execute_with(|| { + assert!(Sys::events().is_empty()); + // validators + for x in 1..5 { + assert!(Stake::is_candidate(&x)); + assert_eq!(Balances::free_balance(&x), 80); + assert_eq!(Balances::reserved_balance(&x), 20); + } + assert!(Stake::is_candidate(&5)); + assert_eq!(Balances::free_balance(&5), 90); + assert_eq!(Balances::reserved_balance(&5), 10); + // nominators + for x in 6..11 { + assert!(Stake::is_nominator(&x)); + assert_eq!(Balances::free_balance(&x), 90); + assert_eq!(Balances::reserved_balance(&x), 10); + } + }); +} + #[test] fn online_offline_behaves() { genesis().execute_with(|| { @@ -73,17 +95,6 @@ fn online_offline_behaves() { MetaEvent::stake(RawEvent::ValidatorWentOffline(3, 2)) ); roll_to(21); - let events = Sys::events() - .into_iter() - .map(|r| r.event) - .filter_map(|e| { - if let MetaEvent::stake(inner) = e { - Some(inner) - } else { - None - } - }) - .collect::>(); let mut expected = vec![ RawEvent::ValidatorChosen(2, 1, 700), RawEvent::ValidatorChosen(2, 2, 400), @@ -97,7 +108,7 @@ fn online_offline_behaves() { RawEvent::ValidatorChosen(5, 1, 700), RawEvent::NewRound(20, 5, 1, 700), ]; - assert_eq!(events, expected); + assert_eq!(events(), expected); assert_noop!( Stake::go_offline(Origin::signed(2)), Error::::AlreadyOffline @@ -112,18 +123,7 @@ fn online_offline_behaves() { expected.push(RawEvent::ValidatorChosen(6, 1, 700)); expected.push(RawEvent::ValidatorChosen(6, 2, 400)); expected.push(RawEvent::NewRound(25, 6, 2, 1100)); - let events = Sys::events() - .into_iter() - .map(|r| r.event) - .filter_map(|e| { - if let MetaEvent::stake(inner) = e { - Some(inner) - } else { - None - } - }) - .collect::>(); - assert_eq!(events, expected); + assert_eq!(events(), expected); }); } @@ -184,17 +184,6 @@ fn validator_exit_executes_after_delay() { let info = ::Candidates::get(&2).unwrap(); assert_eq!(info.state, ValidatorStatus::Leaving(5)); roll_to(21); - let events = Sys::events() - .into_iter() - .map(|r| r.event) - .filter_map(|e| { - if let MetaEvent::stake(inner) = e { - Some(inner) - } else { - None - } - }) - .collect::>(); // we must exclude leaving validators from rewards while // holding them retroactively accountable for previous faults // (within the last T::SlashingWindow blocks) @@ -212,7 +201,7 @@ fn validator_exit_executes_after_delay() { RawEvent::ValidatorChosen(5, 1, 700), RawEvent::NewRound(20, 5, 1, 700), ]; - assert_eq!(events, expected); + assert_eq!(events(), expected); }); } @@ -221,17 +210,6 @@ fn validator_selection_chooses_top_candidates() { genesis2().execute_with(|| { roll_to(4); roll_to(8); - let events = Sys::events() - .into_iter() - .map(|r| r.event) - .filter_map(|e| { - if let MetaEvent::stake(inner) = e { - Some(inner) - } else { - None - } - }) - .collect::>(); // should choose top MaxValidators (5), in order let expected = vec![ RawEvent::ValidatorChosen(2, 1, 100), @@ -241,7 +219,7 @@ fn validator_selection_chooses_top_candidates() { RawEvent::ValidatorChosen(2, 5, 60), RawEvent::NewRound(5, 2, 5, 400), ]; - assert_eq!(events, expected); + assert_eq!(events(), expected); assert_ok!(Stake::leave_candidates(Origin::signed(6))); assert_eq!( last_event(), @@ -258,17 +236,6 @@ fn validator_selection_chooses_top_candidates() { MetaEvent::stake(RawEvent::JoinedValidatorCandidates(6, 69u128, 469u128)) ); roll_to(27); - let events = Sys::events() - .into_iter() - .map(|r| r.event) - .filter_map(|e| { - if let MetaEvent::stake(inner) = e { - Some(inner) - } else { - None - } - }) - .collect::>(); // should choose top MaxValidators (5), in order let expected = vec![ RawEvent::ValidatorChosen(2, 1, 100), @@ -305,7 +272,7 @@ fn validator_selection_chooses_top_candidates() { RawEvent::ValidatorChosen(6, 6, 69), RawEvent::NewRound(25, 6, 5, 409), ]; - assert_eq!(events, expected); + assert_eq!(events(), expected); }); } @@ -314,17 +281,6 @@ fn exit_queue_works() { genesis2().execute_with(|| { roll_to(4); roll_to(8); - let events = Sys::events() - .into_iter() - .map(|r| r.event) - .filter_map(|e| { - if let MetaEvent::stake(inner) = e { - Some(inner) - } else { - None - } - }) - .collect::>(); // should choose top MaxValidators (5), in order let mut expected = vec![ RawEvent::ValidatorChosen(2, 1, 100), @@ -334,7 +290,7 @@ fn exit_queue_works() { RawEvent::ValidatorChosen(2, 5, 60), RawEvent::NewRound(5, 2, 5, 400), ]; - assert_eq!(events, expected); + assert_eq!(events(), expected); assert_ok!(Stake::leave_candidates(Origin::signed(6))); assert_eq!( last_event(), @@ -353,17 +309,6 @@ fn exit_queue_works() { MetaEvent::stake(RawEvent::ValidatorScheduledExit(4, 4, 6)) ); roll_to(21); - let events = Sys::events() - .into_iter() - .map(|r| r.event) - .filter_map(|e| { - if let MetaEvent::stake(inner) = e { - Some(inner) - } else { - None - } - }) - .collect::>(); let mut new_events = vec![ RawEvent::ValidatorScheduledExit(2, 6, 4), RawEvent::ValidatorChosen(3, 1, 100), @@ -387,7 +332,7 @@ fn exit_queue_works() { RawEvent::NewRound(20, 5, 3, 270), ]; expected.append(&mut new_events); - assert_eq!(events, expected); + assert_eq!(events(), expected); }); } @@ -401,17 +346,6 @@ fn payout_distribution_works() { } roll_to(4); roll_to(8); - let events = Sys::events() - .into_iter() - .map(|r| r.event) - .filter_map(|e| { - if let MetaEvent::stake(inner) = e { - Some(inner) - } else { - None - } - }) - .collect::>(); // should choose top MaxValidators (5), in order let mut expected = vec![ RawEvent::ValidatorChosen(2, 1, 100), @@ -421,21 +355,10 @@ fn payout_distribution_works() { RawEvent::ValidatorChosen(2, 5, 60), RawEvent::NewRound(5, 2, 5, 400), ]; - assert_eq!(events, expected); + assert_eq!(events(), expected); // ~ set block author as 1 for all blocks this round set_pts(2, 1, 100); roll_to(16); - let events = Sys::events() - .into_iter() - .map(|r| r.event) - .filter_map(|e| { - if let MetaEvent::stake(inner) = e { - Some(inner) - } else { - None - } - }) - .collect::>(); // pay total issuance (=10) to 1 let mut new = vec![ RawEvent::ValidatorChosen(3, 1, 100), @@ -453,23 +376,12 @@ fn payout_distribution_works() { RawEvent::NewRound(15, 4, 5, 400), ]; expected.append(&mut new); - assert_eq!(events, expected); + assert_eq!(events(), expected); // ~ set block author as 1 for 3 blocks this round set_pts(4, 1, 60); // ~ set block author as 2 for 2 blocks this round set_pts(4, 2, 40); roll_to(26); - let events = Sys::events() - .into_iter() - .map(|r| r.event) - .filter_map(|e| { - if let MetaEvent::stake(inner) = e { - Some(inner) - } else { - None - } - }) - .collect::>(); // pay 60% total issuance to 1 and 40% total issuance to 2 let mut new1 = vec![ RawEvent::ValidatorChosen(5, 1, 100), @@ -488,7 +400,7 @@ fn payout_distribution_works() { RawEvent::NewRound(25, 6, 5, 400), ]; expected.append(&mut new1); - assert_eq!(events, expected); + assert_eq!(events(), expected); // ~ each validator produces 1 block this round set_pts(6, 1, 20); set_pts(6, 2, 20); @@ -496,17 +408,6 @@ fn payout_distribution_works() { set_pts(6, 4, 20); set_pts(6, 5, 20); roll_to(36); - let events = Sys::events() - .into_iter() - .map(|r| r.event) - .filter_map(|e| { - if let MetaEvent::stake(inner) = e { - Some(inner) - } else { - None - } - }) - .collect::>(); // pay 20% issuance for all validators let mut new2 = vec![ RawEvent::ValidatorChosen(7, 1, 100), @@ -528,7 +429,7 @@ fn payout_distribution_works() { RawEvent::NewRound(35, 8, 5, 400), ]; expected.append(&mut new2); - assert_eq!(events, expected); + assert_eq!(events(), expected); // check that distributing rewards clears awarded pts assert!(::AwardedPts::get(1, 1).is_zero()); assert!(::AwardedPts::get(4, 1).is_zero()); @@ -540,3 +441,92 @@ fn payout_distribution_works() { assert!(::AwardedPts::get(6, 5).is_zero()); }); } + +#[test] +fn multiple_nomination_works() { + genesis3().execute_with(|| { + roll_to(4); + roll_to(8); + // chooses top MaxValidators (5), in order + let mut expected = vec![ + RawEvent::ValidatorChosen(2, 1, 50), + RawEvent::ValidatorChosen(2, 2, 40), + RawEvent::ValidatorChosen(2, 4, 20), + RawEvent::ValidatorChosen(2, 3, 20), + RawEvent::ValidatorChosen(2, 5, 10), + RawEvent::NewRound(5, 2, 5, 140), + ]; + assert_eq!(events(), expected); + assert_noop!( + Stake::nominate(Origin::signed(5), 2, 10), + Error::::NominatorDNE, + ); + assert_noop!( + Stake::nominate(Origin::signed(11), 1, 10), + Error::::NominatorDNE, + ); + assert_noop!( + Stake::nominate(Origin::signed(6), 1, 10), + Error::::AlreadyNominatedValidator, + ); + assert_noop!( + Stake::nominate(Origin::signed(6), 2, 4), + Error::::NominationBelowMin, + ); + assert_ok!(Stake::nominate(Origin::signed(6), 2, 10)); + assert_ok!(Stake::nominate(Origin::signed(6), 3, 10)); + assert_ok!(Stake::nominate(Origin::signed(6), 4, 10)); + assert_noop!( + Stake::nominate(Origin::signed(6), 5, 10), + Error::::ExceedMaxValidatorsPerNom, + ); + roll_to(16); + let mut new = vec![ + RawEvent::ValidatorNominated(6, 10, 2, 50), + RawEvent::ValidatorNominated(6, 10, 3, 30), + RawEvent::ValidatorNominated(6, 10, 4, 30), + RawEvent::ValidatorChosen(3, 2, 50), + RawEvent::ValidatorChosen(3, 1, 50), + RawEvent::ValidatorChosen(3, 4, 30), + RawEvent::ValidatorChosen(3, 3, 30), + RawEvent::ValidatorChosen(3, 5, 10), + RawEvent::NewRound(10, 3, 5, 170), + RawEvent::ValidatorChosen(4, 2, 50), + RawEvent::ValidatorChosen(4, 1, 50), + RawEvent::ValidatorChosen(4, 4, 30), + RawEvent::ValidatorChosen(4, 3, 30), + RawEvent::ValidatorChosen(4, 5, 10), + RawEvent::NewRound(15, 4, 5, 170), + ]; + expected.append(&mut new); + assert_eq!(events(), expected); + roll_to(21); + assert_ok!(Stake::nominate(Origin::signed(7), 2, 80)); + assert_noop!( + Stake::nominate(Origin::signed(7), 3, 11), + DispatchError::Module { + index: 0, + error: 3, + message: Some("InsufficientBalance") + }, + ); + roll_to(26); + let mut new2 = vec![ + RawEvent::ValidatorChosen(5, 2, 50), + RawEvent::ValidatorChosen(5, 1, 50), + RawEvent::ValidatorChosen(5, 4, 30), + RawEvent::ValidatorChosen(5, 3, 30), + RawEvent::ValidatorChosen(5, 5, 10), + RawEvent::NewRound(20, 5, 5, 170), + RawEvent::ValidatorNominated(7, 80, 2, 130), + RawEvent::ValidatorChosen(6, 2, 130), + RawEvent::ValidatorChosen(6, 1, 50), + RawEvent::ValidatorChosen(6, 4, 30), + RawEvent::ValidatorChosen(6, 3, 30), + RawEvent::ValidatorChosen(6, 5, 10), + RawEvent::NewRound(25, 6, 5, 250), + ]; + expected.append(&mut new2); + assert_eq!(events(), expected); + }); +} From 0cba05e3d819b81f809e323e8b048a3459746928 Mon Sep 17 00:00:00 2001 From: 4meta5 Date: Tue, 19 Jan 2021 18:13:42 -0500 Subject: [PATCH 05/23] add stakers as separate field to chainspec with fmt that matches stake genesis config --- node/parachain/src/chain_spec.rs | 19 +++---- node/standalone/src/chain_spec.rs | 26 ++++++---- node/standalone/src/service.rs | 82 ++++++++++++++++++------------- pallets/stake/src/tests.rs | 20 ++++++++ 4 files changed, 95 insertions(+), 52 deletions(-) diff --git a/node/parachain/src/chain_spec.rs b/node/parachain/src/chain_spec.rs index 4abe43bb9b..a7ecefbd7a 100644 --- a/node/parachain/src/chain_spec.rs +++ b/node/parachain/src/chain_spec.rs @@ -16,8 +16,8 @@ use cumulus_primitives::ParaId; use moonbeam_runtime::{ - AccountId, BalancesConfig, EVMConfig, EthereumChainIdConfig, EthereumConfig, GenesisConfig, - ParachainInfoConfig, StakeConfig, SudoConfig, SystemConfig, GLMR, WASM_BINARY, + AccountId, Balance, BalancesConfig, EVMConfig, EthereumChainIdConfig, EthereumConfig, + GenesisConfig, ParachainInfoConfig, StakeConfig, SudoConfig, SystemConfig, GLMR, WASM_BINARY, }; use sc_chain_spec::{ChainSpecExtension, ChainSpecGroup}; use sc_service::ChainType; @@ -54,6 +54,12 @@ pub fn get_chain_spec(para_id: ParaId) -> ChainSpec { move || { testnet_genesis( AccountId::from_str("6Be02d1d3665660d22FF9624b7BE0551ee1Ac91b").unwrap(), + // Validator + vec![( + AccountId::from_str("6Be02d1d3665660d22FF9624b7BE0551ee1Ac91b").unwrap(), + None, + 100_000 * GLMR, + )], vec![AccountId::from_str("6Be02d1d3665660d22FF9624b7BE0551ee1Ac91b").unwrap()], para_id, 1280, //ChainId @@ -72,6 +78,7 @@ pub fn get_chain_spec(para_id: ParaId) -> ChainSpec { fn testnet_genesis( root_key: AccountId, + stakers: Vec<(AccountId, Option, Balance)>, endowed_accounts: Vec, para_id: ParaId, chain_id: u64, @@ -99,12 +106,6 @@ fn testnet_genesis( accounts: BTreeMap::new(), }), pallet_ethereum: Some(EthereumConfig {}), - stake: Some(StakeConfig { - stakers: endowed_accounts - .iter() - .cloned() - .map(|k| (k, None, 100_000 * GLMR)) - .collect(), - }), + stake: Some(StakeConfig { stakers }), } } diff --git a/node/standalone/src/chain_spec.rs b/node/standalone/src/chain_spec.rs index f88e41a4e3..576f3447af 100644 --- a/node/standalone/src/chain_spec.rs +++ b/node/standalone/src/chain_spec.rs @@ -15,8 +15,9 @@ // along with Moonbeam. If not, see . use moonbeam_runtime::{ - AccountId, AuraConfig, BalancesConfig, EVMConfig, EthereumChainIdConfig, EthereumConfig, - GenesisConfig, GrandpaConfig, StakeConfig, SudoConfig, SystemConfig, GLMR, WASM_BINARY, + AccountId, AuraConfig, Balance, BalancesConfig, EVMConfig, EthereumChainIdConfig, + EthereumConfig, GenesisConfig, GrandpaConfig, StakeConfig, SudoConfig, SystemConfig, GLMR, + WASM_BINARY, }; use sc_service::ChainType; use sp_consensus_aura::sr25519::AuthorityId as AuraId; @@ -62,6 +63,12 @@ pub fn development_config() -> Result { vec![authority_keys_from_seed("Alice")], // Sudo account AccountId::from_str("6Be02d1d3665660d22FF9624b7BE0551ee1Ac91b").unwrap(), + // Validator at genesis + vec![( + AccountId::from_str("6Be02d1d3665660d22FF9624b7BE0551ee1Ac91b").unwrap(), + None, + 100_000 * GLMR, + )], // Pre-funded accounts vec![AccountId::from_str("6Be02d1d3665660d22FF9624b7BE0551ee1Ac91b").unwrap()], true, @@ -101,6 +108,12 @@ pub fn local_testnet_config() -> Result { ], // Sudo account AccountId::from_str("6Be02d1d3665660d22FF9624b7BE0551ee1Ac91b").unwrap(), + // Validator + vec![( + AccountId::from_str("6Be02d1d3665660d22FF9624b7BE0551ee1Ac91b").unwrap(), + None, + 100_000 * GLMR, + )], // Pre-funded accounts vec![AccountId::from_str("6Be02d1d3665660d22FF9624b7BE0551ee1Ac91b").unwrap()], true, @@ -125,6 +138,7 @@ fn testnet_genesis( wasm_binary: &[u8], initial_authorities: Vec<(AuraId, GrandpaId)>, root_key: AccountId, + stakers: Vec<(AccountId, Option, Balance)>, endowed_accounts: Vec, _enable_println: bool, chain_id: u64, @@ -161,12 +175,6 @@ fn testnet_genesis( accounts: BTreeMap::new(), }), pallet_ethereum: Some(EthereumConfig {}), - stake: Some(StakeConfig { - stakers: endowed_accounts - .iter() - .cloned() - .map(|k| (k, None, 100_000 * GLMR)) - .collect(), - }), + stake: Some(StakeConfig { stakers }), } } diff --git a/node/standalone/src/service.rs b/node/standalone/src/service.rs index 3388d2c491..5722573dde 100644 --- a/node/standalone/src/service.rs +++ b/node/standalone/src/service.rs @@ -16,13 +16,12 @@ //! Service and ServiceFactory implementation. Specialized wrapper over substrate service. -use std::{sync::{Arc, Mutex}, time::Duration, collections::HashMap}; -use fc_rpc_core::types::PendingTransactions; use crate::mock_timestamp::MockTimestampInherentDataProvider; use fc_consensus::FrontierBlockImport; +use fc_rpc_core::types::PendingTransactions; use moonbeam_runtime::{self, opaque::Block, RuntimeApi}; -use sc_client_api::{ExecutorProvider, RemoteBackend, BlockchainEvents}; use parity_scale_codec::Encode; +use sc_client_api::{BlockchainEvents, ExecutorProvider, RemoteBackend}; use sc_consensus_manual_seal::{self as manual_seal}; use sc_executor::native_executor_instance; pub use sc_executor::NativeExecutor; @@ -31,6 +30,11 @@ use sc_service::{error::Error as ServiceError, Configuration, TaskManager}; use sp_consensus_aura::sr25519::AuthorityPair as AuraPair; use sp_core::H160; use sp_inherents::InherentDataProviders; +use std::{ + collections::HashMap, + sync::{Arc, Mutex}, + time::Duration, +}; // Our native executor instance. native_executor_instance!( @@ -117,8 +121,7 @@ pub fn new_partial( client.clone(), ); - let pending_transactions: PendingTransactions - = Some(Arc::new(Mutex::new(HashMap::new()))); + let pending_transactions: PendingTransactions = Some(Arc::new(Mutex::new(HashMap::new()))); if manual_seal { let frontier_block_import = FrontierBlockImport::new(client.clone(), client.clone(), true); @@ -138,7 +141,10 @@ pub fn new_partial( select_chain, transaction_pool, inherent_data_providers, - other: (ConsensusResult::ManualSeal(frontier_block_import), pending_transactions), + other: ( + ConsensusResult::ManualSeal(frontier_block_import), + pending_transactions, + ), }); } @@ -176,7 +182,10 @@ pub fn new_partial( select_chain, transaction_pool, inherent_data_providers, - other: (ConsensusResult::Aura(aura_block_import, grandpa_link), pending_transactions), + other: ( + ConsensusResult::Aura(aura_block_import, grandpa_link), + pending_transactions, + ), }) } @@ -284,43 +293,48 @@ pub fn new_full( // Spawn Frontier pending transactions maintenance task (as essential, otherwise we leak). if pending_transactions.is_some() { + use fp_consensus::{ConsensusLog, FRONTIER_ENGINE_ID}; use futures::StreamExt; - use fp_consensus::{FRONTIER_ENGINE_ID, ConsensusLog}; use sp_runtime::generic::OpaqueDigestItemId; const TRANSACTION_RETAIN_THRESHOLD: u64 = 5; task_manager.spawn_essential_handle().spawn( "frontier-pending-transactions", - client.import_notification_stream().for_each(move |notification| { - - if let Ok(locked) = &mut pending_transactions.clone().unwrap().lock() { - // As pending transactions have a finite lifespan anyway - // we can ignore MultiplePostRuntimeLogs error checks. - let mut frontier_log: Option<_> = None; - for log in notification.header.digest.logs { - let log = log.try_to::(OpaqueDigestItemId::Consensus(&FRONTIER_ENGINE_ID)); - if let Some(log) = log { - frontier_log = Some(log); + client + .import_notification_stream() + .for_each(move |notification| { + if let Ok(locked) = &mut pending_transactions.clone().unwrap().lock() { + // As pending transactions have a finite lifespan anyway + // we can ignore MultiplePostRuntimeLogs error checks. + let mut frontier_log: Option<_> = None; + for log in notification.header.digest.logs { + let log = log.try_to::(OpaqueDigestItemId::Consensus( + &FRONTIER_ENGINE_ID, + )); + if let Some(log) = log { + frontier_log = Some(log); + } } - } - let imported_number: u64 = notification.header.number as u64; + let imported_number: u64 = notification.header.number as u64; - if let Some(ConsensusLog::EndBlock { - block_hash: _, transaction_hashes, - }) = frontier_log { - // Retain all pending transactions that were not - // processed in the current block. - locked.retain(|&k, _| !transaction_hashes.contains(&k)); + if let Some(ConsensusLog::EndBlock { + block_hash: _, + transaction_hashes, + }) = frontier_log + { + // Retain all pending transactions that were not + // processed in the current block. + locked.retain(|&k, _| !transaction_hashes.contains(&k)); + } + locked.retain(|_, v| { + // Drop all the transactions that exceeded the given lifespan. + let lifespan_limit = v.at_block + TRANSACTION_RETAIN_THRESHOLD; + lifespan_limit > imported_number + }); } - locked.retain(|_, v| { - // Drop all the transactions that exceeded the given lifespan. - let lifespan_limit = v.at_block + TRANSACTION_RETAIN_THRESHOLD; - lifespan_limit > imported_number - }); - } - futures::future::ready(()) - }) + futures::future::ready(()) + }), ); } diff --git a/pallets/stake/src/tests.rs b/pallets/stake/src/tests.rs index 2353c41482..77f290b7ae 100644 --- a/pallets/stake/src/tests.rs +++ b/pallets/stake/src/tests.rs @@ -528,5 +528,25 @@ fn multiple_nomination_works() { ]; expected.append(&mut new2); assert_eq!(events(), expected); + assert_ok!(Stake::leave_candidates(Origin::signed(2))); + assert_eq!( + last_event(), + MetaEvent::stake(RawEvent::ValidatorScheduledExit(6, 2, 8)) + ); + roll_to(31); + let mut new3 = vec![ + RawEvent::ValidatorScheduledExit(6, 2, 8), + RawEvent::ValidatorChosen(7, 1, 50), + RawEvent::ValidatorChosen(7, 4, 30), + RawEvent::ValidatorChosen(7, 3, 30), + RawEvent::ValidatorChosen(7, 5, 10), + RawEvent::NewRound(30, 7, 4, 120) + ]; + expected.append(&mut new3); + assert_eq!(events(), expected); + // check that nominations are removed after validator leaves, not before + assert_eq!(::Nominators::get(7).unwrap().nominations.0.len(),2usize); + roll_to(40); + assert_eq!(::Nominators::get(7).unwrap().nominations.0.len(),1usize); }); } From 2144420d1de5319addb3e0809c5ed437ff46fc99 Mon Sep 17 00:00:00 2001 From: 4meta5 Date: Tue, 19 Jan 2021 19:20:13 -0500 Subject: [PATCH 06/23] verify nominations are returned when validator exits --- pallets/stake/src/tests.rs | 50 +++++++++++++++++++++++++++++++++++--- 1 file changed, 46 insertions(+), 4 deletions(-) diff --git a/pallets/stake/src/tests.rs b/pallets/stake/src/tests.rs index 77f290b7ae..f86048233f 100644 --- a/pallets/stake/src/tests.rs +++ b/pallets/stake/src/tests.rs @@ -540,13 +540,55 @@ fn multiple_nomination_works() { RawEvent::ValidatorChosen(7, 4, 30), RawEvent::ValidatorChosen(7, 3, 30), RawEvent::ValidatorChosen(7, 5, 10), - RawEvent::NewRound(30, 7, 4, 120) + RawEvent::NewRound(30, 7, 4, 120), ]; expected.append(&mut new3); assert_eq!(events(), expected); - // check that nominations are removed after validator leaves, not before - assert_eq!(::Nominators::get(7).unwrap().nominations.0.len(),2usize); + // verify that nominations are removed after validator leaves, not before + assert_eq!(::Nominators::get(7).unwrap().total, 90); + assert_eq!( + ::Nominators::get(7) + .unwrap() + .nominations + .0 + .len(), + 2usize + ); + assert_eq!(::Nominators::get(6).unwrap().total, 40); + assert_eq!( + ::Nominators::get(6) + .unwrap() + .nominations + .0 + .len(), + 4usize + ); + assert_eq!(Balances::reserved_balance(&6), 40); + assert_eq!(Balances::reserved_balance(&7), 90); + assert_eq!(Balances::free_balance(&6), 60); + assert_eq!(Balances::free_balance(&7), 10); roll_to(40); - assert_eq!(::Nominators::get(7).unwrap().nominations.0.len(),1usize); + assert_eq!(::Nominators::get(7).unwrap().total, 10); + assert_eq!(::Nominators::get(6).unwrap().total, 30); + assert_eq!( + ::Nominators::get(7) + .unwrap() + .nominations + .0 + .len(), + 1usize + ); + assert_eq!( + ::Nominators::get(6) + .unwrap() + .nominations + .0 + .len(), + 3usize + ); + assert_eq!(Balances::reserved_balance(&6), 30); + assert_eq!(Balances::reserved_balance(&7), 10); + assert_eq!(Balances::free_balance(&6), 70); + assert_eq!(Balances::free_balance(&7), 90); }); } From eca52014a0c2e4454edb57b7318df77763b92285 Mon Sep 17 00:00:00 2001 From: 4meta5 Date: Wed, 20 Jan 2021 22:25:16 -0500 Subject: [PATCH 07/23] candidate bond more less --- pallets/stake/src/lib.rs | 124 ++++++++++++++++++++++++++----------- pallets/stake/src/tests.rs | 46 ++++++++++++++ 2 files changed, 135 insertions(+), 35 deletions(-) diff --git a/pallets/stake/src/lib.rs b/pallets/stake/src/lib.rs index b5ca4f9c52..453a3ecd4a 100644 --- a/pallets/stake/src/lib.rs +++ b/pallets/stake/src/lib.rs @@ -128,7 +128,7 @@ impl Default for ValidatorStatus { #[derive(Encode, Decode, RuntimeDebug)] pub struct Validator { - pub account: AccountId, + pub id: AccountId, pub fee: Perbill, pub bond: Balance, pub nominators: OrderedSet>, @@ -141,10 +141,10 @@ impl< B: AtLeast32BitUnsigned + Ord + Copy + sp_std::ops::AddAssign + sp_std::ops::SubAssign, > Validator { - pub fn new(account: A, fee: Perbill, bond: B) -> Self { + pub fn new(id: A, fee: Perbill, bond: B) -> Self { let total = bond; Validator { - account, + id, fee, bond, nominators: OrderedSet::new(), @@ -162,6 +162,15 @@ impl< false } } + pub fn bond_more(&mut self, more: B) { + self.bond += more; + self.total += more; + } + pub fn bond_less(&mut self, less: B) -> B { + self.bond -= less; + self.total -= less; + self.bond + } pub fn go_offline(&mut self) { self.state = ValidatorStatus::Idle; } @@ -281,6 +290,10 @@ decl_event!( JoinedValidatorCandidates(AccountId, Balance, Balance), /// Round, Validator Account, Total Exposed Amount (includes all nominations) ValidatorChosen(RoundIndex, AccountId, Balance), + /// Validator Account, Old Bond, New Bond + ValidatorBondedMore(AccountId, Balance, Balance), + /// Validator Account, Old Bond, New Bond + ValidatorBondedLess(AccountId, Balance, Balance), ValidatorWentOffline(RoundIndex, AccountId), ValidatorBackOnline(RoundIndex, AccountId), /// Round, Validator Account, Scheduled Exit @@ -417,6 +430,28 @@ decl_module! { Ok(()) } #[weight = 0] + fn leave_candidates(origin) -> DispatchResult { + let validator = ensure_signed(origin)?; + let mut state = >::get(&validator).ok_or(Error::::CandidateDNE)?; + ensure!(!state.is_leaving(),Error::::AlreadyLeaving); + let mut exits = >::get(); + let now = ::get(); + let when = now + T::BondDuration::get(); + ensure!( + exits.insert(Bond{owner:validator.clone(),amount:when}), + Error::::AlreadyLeaving + ); + state.leave_candidates(when); + let mut candidates = >::get(); + if candidates.remove(&Bond::from_owner(validator.clone())) { + >::put(candidates); + } + >::put(exits); + >::insert(&validator,state); + Self::deposit_event(RawEvent::ValidatorScheduledExit(now,validator,when)); + Ok(()) + } + #[weight = 0] fn go_offline(origin) -> DispatchResult { let validator = ensure_signed(origin)?; let mut state = >::get(&validator).ok_or(Error::::CandidateDNE)?; @@ -448,25 +483,41 @@ decl_module! { Ok(()) } #[weight = 0] - fn leave_candidates(origin) -> DispatchResult { + fn candidate_bond_more(origin, more: BalanceOf) -> DispatchResult { let validator = ensure_signed(origin)?; let mut state = >::get(&validator).ok_or(Error::::CandidateDNE)?; - ensure!(!state.is_leaving(),Error::::AlreadyLeaving); - let mut exits = >::get(); - let now = ::get(); - let when = now + T::BondDuration::get(); - ensure!( - exits.insert(Bond{owner:validator.clone(),amount:when}), - Error::::AlreadyLeaving - ); - state.leave_candidates(when); + ensure!(!state.is_leaving(),Error::::CannotActivateIfLeaving); + T::Currency::reserve(&validator, more)?; + let before = state.bond; + state.bond_more(more); + let after = state.bond; let mut candidates = >::get(); if candidates.remove(&Bond::from_owner(validator.clone())) { - >::put(candidates); + if candidates.insert(Bond{owner:validator.clone(),amount:state.total}) { + >::put(candidates); + } } - >::put(exits); >::insert(&validator,state); - Self::deposit_event(RawEvent::ValidatorScheduledExit(now,validator,when)); + Self::deposit_event(RawEvent::ValidatorBondedMore(validator, before, after)); + Ok(()) + } + #[weight = 0] + fn candidate_bond_less(origin, less: BalanceOf) -> DispatchResult { + let validator = ensure_signed(origin)?; + let mut state = >::get(&validator).ok_or(Error::::CandidateDNE)?; + ensure!(!state.is_leaving(),Error::::CannotActivateIfLeaving); + let before = state.bond; + ensure!(state.bond_less(less) >= T::MinValidatorStk::get(), Error::::ValBondBelowMin); + let after = state.bond; + // TODO: make this into an update_if_candidates method (shared with candidate_bond_more) + let mut candidates = >::get(); + if candidates.remove(&Bond::from_owner(validator.clone())) { + if candidates.insert(Bond{owner:validator.clone(),amount:state.total}) { + >::put(candidates); + } + } + >::insert(&validator,state); + Self::deposit_event(RawEvent::ValidatorBondedLess(validator, before, after)); Ok(()) } #[weight = 0] @@ -485,6 +536,25 @@ decl_module! { Ok(()) } #[weight = 0] + fn leave_nominators(origin) -> DispatchResult { + let acc = ensure_signed(origin)?; + let nominator = >::get(&acc).ok_or(Error::::NominatorDNE)?; + for bond in nominator.nominations.0 { + Self::nominator_leaves_validator(acc.clone(), bond.owner.clone())?; + } + >::remove(&acc); + Self::deposit_event(RawEvent::NominatorLeft(acc.clone(), nominator.total)); + Ok(()) + } + #[weight = 0] + fn nominator_bond_more(origin, candidate: T::AccountId, more: BalanceOf) -> DispatchResult { + Ok(()) + } + #[weight = 0] + fn nominator_bond_less(origin, candidate: T::AccountId, less: BalanceOf) -> DispatchResult { + Ok(()) + } + #[weight = 0] fn nominate( origin, validator: T::AccountId, @@ -509,17 +579,6 @@ decl_module! { fn revoke_nomination(origin, validator: T::AccountId) -> DispatchResult { Self::nominator_revokes_validator(ensure_signed(origin)?, validator.clone()) } - #[weight = 0] - fn leave_nominators(origin) -> DispatchResult { - let acc = ensure_signed(origin)?; - let nominator = >::get(&acc).ok_or(Error::::NominatorDNE)?; - for bond in nominator.nominations.0 { - Self::nominator_leaves_validator(acc.clone(), bond.owner.clone())?; - } - >::remove(&acc); - Self::deposit_event(RawEvent::NominatorLeft(acc.clone(), nominator.total)); - Ok(()) - } fn on_finalize(n: T::BlockNumber) { if (n % T::BlocksPerRound::get()).is_zero() { let next = ::get() + 1; @@ -678,13 +737,8 @@ impl Module { } let pct = Perbill::from_rational_approximation(state.bond, state.total); let due = pct * amt_due; - if let Some(imb) = - T::Currency::deposit_into_existing(&state.account, due).ok() - { - Self::deposit_event(RawEvent::Rewarded( - state.account.clone(), - imb.peek(), - )); + if let Some(imb) = T::Currency::deposit_into_existing(&state.id, due).ok() { + Self::deposit_event(RawEvent::Rewarded(state.id.clone(), imb.peek())); } } } @@ -715,7 +769,7 @@ impl Module { } } // return stake to validator - T::Currency::unreserve(&state.account, state.bond); + T::Currency::unreserve(&state.id, state.bond); let new_total = >::get() - state.total; >::put(new_total); >::remove(&x.owner); diff --git a/pallets/stake/src/tests.rs b/pallets/stake/src/tests.rs index f86048233f..72fcdea340 100644 --- a/pallets/stake/src/tests.rs +++ b/pallets/stake/src/tests.rs @@ -592,3 +592,49 @@ fn multiple_nomination_works() { assert_eq!(Balances::free_balance(&7), 90); }); } + +#[test] +fn validators_bond_more_less() { + genesis3().execute_with(|| { + roll_to(4); + assert_noop!( + Stake::candidate_bond_more(Origin::signed(6), 50), + Error::::CandidateDNE + ); + assert_ok!(Stake::candidate_bond_more(Origin::signed(1), 50)); + assert_noop!( + Stake::candidate_bond_more(Origin::signed(1), 40), + DispatchError::Module { + index: 0, + error: 3, + message: Some("InsufficientBalance") + } + ); + assert_ok!(Stake::leave_candidates(Origin::signed(1))); + assert_noop!( + Stake::candidate_bond_more(Origin::signed(1), 30), + Error::::CannotActivateIfLeaving + ); + roll_to(30); + assert_noop!( + Stake::candidate_bond_more(Origin::signed(1), 40), + Error::::CandidateDNE + ); + assert_ok!(Stake::candidate_bond_more(Origin::signed(2), 80)); + assert_ok!(Stake::candidate_bond_less(Origin::signed(2), 90)); + assert_ok!(Stake::candidate_bond_less(Origin::signed(3), 10)); + assert_noop!( + Stake::candidate_bond_less(Origin::signed(2), 1), + Error::::ValBondBelowMin + ); + assert_noop!( + Stake::candidate_bond_less(Origin::signed(3), 1), + Error::::ValBondBelowMin + ); + assert_noop!( + Stake::candidate_bond_less(Origin::signed(4), 11), + Error::::ValBondBelowMin + ); + assert_ok!(Stake::candidate_bond_less(Origin::signed(4), 10)); + }); +} From e4b3a57d812f2748c0829831a2995202d77cc958 Mon Sep 17 00:00:00 2001 From: 4meta5 Date: Thu, 21 Jan 2021 12:01:38 -0800 Subject: [PATCH 08/23] nominator bond more less --- pallets/stake/src/lib.rs | 163 +++++++++++++++++++++++++++++-------- pallets/stake/src/mock.rs | 3 +- pallets/stake/src/tests.rs | 57 ++++++++++++- 3 files changed, 186 insertions(+), 37 deletions(-) diff --git a/pallets/stake/src/lib.rs b/pallets/stake/src/lib.rs index 453a3ecd4a..dbbbcdfa2b 100644 --- a/pallets/stake/src/lib.rs +++ b/pallets/stake/src/lib.rs @@ -166,10 +166,33 @@ impl< self.bond += more; self.total += more; } - pub fn bond_less(&mut self, less: B) -> B { - self.bond -= less; - self.total -= less; - self.bond + // Returns None if underflow or less == self.bond (in which case validator should leave instead of bonding less) + pub fn bond_less(&mut self, less: B) -> Option { + if self.bond > less { + self.bond -= less; + self.total -= less; + Some(self.bond) + } else { + None + } + } + pub fn inc_nominator(&mut self, nominator: A, more: B) { + for x in &mut self.nominators.0 { + if x.owner == nominator { + x.amount += more; + self.total += more; + return; + } + } + } + pub fn dec_nominator(&mut self, nominator: A, less: B) { + for x in &mut self.nominators.0 { + if x.owner == nominator { + x.amount -= less; + self.total -= less; + return; + } + } } pub fn go_offline(&mut self) { self.state = ValidatorStatus::Idle; @@ -198,8 +221,10 @@ pub struct Nominator { pub total: Balance, } -impl - Nominator +impl< + AccountId: Ord + Clone, + Balance: Copy + sp_std::ops::AddAssign + sp_std::ops::SubAssign + PartialOrd, + > Nominator { pub fn new(validator: AccountId, nomination: Balance) -> Self { Nominator { @@ -219,7 +244,8 @@ impl Option { let mut amt: Option = None; let nominations = self @@ -243,6 +269,39 @@ impl Option { + for x in &mut self.nominations.0 { + if x.owner == validator { + x.amount += more; + self.total += more; + return Some(x.amount); + } + } + None + } + // Returns Some(Some(balance)) if successful + // None if nomination not found + // Some(None) if underflow + pub fn dec_nomination( + &mut self, + validator: AccountId, + less: Balance, + ) -> Option> { + for x in &mut self.nominations.0 { + if x.owner == validator { + if x.amount > less { + x.amount -= less; + self.total -= less; + return Some(Some(x.amount)); + } else { + // underflow error; should rm entire nomination if x.amount == validator + return Some(None); + } + } + } + None + } } type RoundIndex = u32; @@ -294,6 +353,10 @@ decl_event!( ValidatorBondedMore(AccountId, Balance, Balance), /// Validator Account, Old Bond, New Bond ValidatorBondedLess(AccountId, Balance, Balance), + // Nominator, Validator, Old Nomination, New Nomination + NominationIncreased(AccountId, AccountId, Balance, Balance), + // Nominator, Validator, Old Nomination, New Nomination + NominationDecreased(AccountId, AccountId, Balance, Balance), ValidatorWentOffline(RoundIndex, AccountId), ValidatorBackOnline(RoundIndex, AccountId), /// Round, Validator Account, Scheduled Exit @@ -334,6 +397,7 @@ decl_error! { AlreadyNominatedValidator, MustNominateAtLeastOne, NominationDNE, + Underflow, } } @@ -491,11 +555,8 @@ decl_module! { let before = state.bond; state.bond_more(more); let after = state.bond; - let mut candidates = >::get(); - if candidates.remove(&Bond::from_owner(validator.clone())) { - if candidates.insert(Bond{owner:validator.clone(),amount:state.total}) { - >::put(candidates); - } + if state.is_active() { + Self::update_active(validator.clone(), state.total); } >::insert(&validator,state); Self::deposit_event(RawEvent::ValidatorBondedMore(validator, before, after)); @@ -507,16 +568,13 @@ decl_module! { let mut state = >::get(&validator).ok_or(Error::::CandidateDNE)?; ensure!(!state.is_leaving(),Error::::CannotActivateIfLeaving); let before = state.bond; - ensure!(state.bond_less(less) >= T::MinValidatorStk::get(), Error::::ValBondBelowMin); - let after = state.bond; - // TODO: make this into an update_if_candidates method (shared with candidate_bond_more) - let mut candidates = >::get(); - if candidates.remove(&Bond::from_owner(validator.clone())) { - if candidates.insert(Bond{owner:validator.clone(),amount:state.total}) { - >::put(candidates); - } + let after = state.bond_less(less).ok_or(Error::::Underflow)?; + ensure!(after >= T::MinValidatorStk::get(), Error::::ValBondBelowMin); + T::Currency::unreserve(&validator, less); + if state.is_active() { + Self::update_active(validator.clone(), state.total); } - >::insert(&validator,state); + >::insert(&validator, state); Self::deposit_event(RawEvent::ValidatorBondedLess(validator, before, after)); Ok(()) } @@ -547,14 +605,6 @@ decl_module! { Ok(()) } #[weight = 0] - fn nominator_bond_more(origin, candidate: T::AccountId, more: BalanceOf) -> DispatchResult { - Ok(()) - } - #[weight = 0] - fn nominator_bond_less(origin, candidate: T::AccountId, less: BalanceOf) -> DispatchResult { - Ok(()) - } - #[weight = 0] fn nominate( origin, validator: T::AccountId, @@ -579,6 +629,49 @@ decl_module! { fn revoke_nomination(origin, validator: T::AccountId) -> DispatchResult { Self::nominator_revokes_validator(ensure_signed(origin)?, validator.clone()) } + #[weight = 0] + fn nominator_bond_more(origin, candidate: T::AccountId, more: BalanceOf) -> DispatchResult { + let nominator = ensure_signed(origin)?; + let mut nominations = >::get(&nominator).ok_or(Error::::NominatorDNE)?; + let mut validator = >::get(&candidate).ok_or(Error::::CandidateDNE)?; + let _ = nominations + .inc_nomination(candidate.clone(), more) + .ok_or(Error::::NominationDNE)?; + T::Currency::reserve(&nominator, more)?; + let before = validator.total; + validator.inc_nominator(nominator.clone(), more); + let after = validator.total; + if validator.is_active() { + Self::update_active(candidate.clone(), validator.total); + } + >::insert(&candidate, validator); + >::insert(&nominator, nominations); + Self::deposit_event(RawEvent::NominationIncreased(nominator, candidate, before, after)); + Ok(()) + } + #[weight = 0] + fn nominator_bond_less(origin, candidate: T::AccountId, less: BalanceOf) -> DispatchResult { + let nominator = ensure_signed(origin)?; + let mut nominations = >::get(&nominator).ok_or(Error::::NominatorDNE)?; + let mut validator = >::get(&candidate).ok_or(Error::::CandidateDNE)?; + let remaining = nominations + .dec_nomination(candidate.clone(), less) + .ok_or(Error::::NominationDNE)? + .ok_or(Error::::Underflow)?; + ensure!(remaining >= T::MinNomination::get(), Error::::NominationBelowMin); + ensure!(nominations.total >= T::MinNominatorStk::get(), Error::::NomBondBelowMin); + T::Currency::unreserve(&nominator, less); + let before = validator.total; + validator.dec_nominator(nominator.clone(), less); + let after = validator.total; + if validator.is_active() { + Self::update_active(candidate.clone(), validator.total); + } + >::insert(&candidate, validator); + >::insert(&nominator, nominations); + Self::deposit_event(RawEvent::NominationDecreased(nominator, candidate, before, after)); + Ok(()) + } fn on_finalize(n: T::BlockNumber) { if (n % T::BlocksPerRound::get()).is_zero() { let next = ::get() + 1; @@ -607,12 +700,12 @@ impl Module { >::get().binary_search(acc).is_ok() } // ensure candidate is active before calling - fn update_active_candidate(candidate: T::AccountId, new_total: BalanceOf) { + fn update_active(candidate: T::AccountId, total: BalanceOf) { let mut candidates = >::get(); candidates.remove(&Bond::from_owner(candidate.clone())); candidates.insert(Bond { owner: candidate.clone(), - amount: new_total, + amount: total, }); >::put(candidates); } @@ -637,7 +730,7 @@ impl Module { T::Currency::reserve(&nominator, amount)?; let new_total = state.total + amount; if state.is_active() { - Self::update_active_candidate(validator.clone(), new_total); + Self::update_active(validator.clone(), new_total); } let new_total_locked = >::get() + amount; >::put(new_total_locked); @@ -684,13 +777,13 @@ impl Module { let nominator_stake = exists.ok_or(Error::::NominatorDNE)?; T::Currency::unreserve(&nominator, nominator_stake); state.nominators = nominators; - let new_total = state.total - nominator_stake; + state.total -= nominator_stake; if state.is_active() { - Self::update_active_candidate(validator.clone(), new_total); + Self::update_active(validator.clone(), state.total); } - state.total = new_total; let new_total_locked = >::get() - nominator_stake; >::put(new_total_locked); + let new_total = state.total; >::insert(&validator, state); Self::deposit_event(RawEvent::NominatorLeftValidator( nominator, diff --git a/pallets/stake/src/mock.rs b/pallets/stake/src/mock.rs index 121fef3436..a42e7dabea 100644 --- a/pallets/stake/src/mock.rs +++ b/pallets/stake/src/mock.rs @@ -104,6 +104,7 @@ parameter_types! { pub const MaxFee: Perbill = Perbill::from_percent(50); pub const MinValidatorStk: u128 = 10; pub const MinNominatorStk: u128 = 5; + pub const MinNomination: u128 = 3; } impl Config for Test { type Event = MetaEvent; @@ -116,8 +117,8 @@ impl Config for Test { type IssuancePerRound = IssuancePerRound; type MaxFee = MaxFee; type MinValidatorStk = MinValidatorStk; - type MinNomination = MinNominatorStk; type MinNominatorStk = MinNominatorStk; + type MinNomination = MinNomination; } pub type Balances = pallet_balances::Module; pub type Stake = Module; diff --git a/pallets/stake/src/tests.rs b/pallets/stake/src/tests.rs index 72fcdea340..7a7cb6d756 100644 --- a/pallets/stake/src/tests.rs +++ b/pallets/stake/src/tests.rs @@ -470,7 +470,7 @@ fn multiple_nomination_works() { Error::::AlreadyNominatedValidator, ); assert_noop!( - Stake::nominate(Origin::signed(6), 2, 4), + Stake::nominate(Origin::signed(6), 2, 2), Error::::NominationBelowMin, ); assert_ok!(Stake::nominate(Origin::signed(6), 2, 10)); @@ -623,6 +623,10 @@ fn validators_bond_more_less() { assert_ok!(Stake::candidate_bond_more(Origin::signed(2), 80)); assert_ok!(Stake::candidate_bond_less(Origin::signed(2), 90)); assert_ok!(Stake::candidate_bond_less(Origin::signed(3), 10)); + assert_noop!( + Stake::candidate_bond_less(Origin::signed(2), 11), + Error::::Underflow + ); assert_noop!( Stake::candidate_bond_less(Origin::signed(2), 1), Error::::ValBondBelowMin @@ -638,3 +642,54 @@ fn validators_bond_more_less() { assert_ok!(Stake::candidate_bond_less(Origin::signed(4), 10)); }); } + +#[test] +fn nominators_bond_more_less() { + genesis3().execute_with(|| { + roll_to(4); + assert_noop!( + Stake::nominator_bond_more(Origin::signed(1), 2, 50), + Error::::NominatorDNE + ); + assert_noop!( + Stake::nominator_bond_more(Origin::signed(6), 2, 50), + Error::::NominationDNE + ); + assert_noop!( + Stake::nominator_bond_more(Origin::signed(7), 6, 50), + Error::::CandidateDNE + ); + assert_noop!( + Stake::nominator_bond_less(Origin::signed(6), 1, 11), + Error::::Underflow + ); + assert_noop!( + Stake::nominator_bond_less(Origin::signed(6), 1, 8), + Error::::NominationBelowMin + ); + assert_noop!( + Stake::nominator_bond_less(Origin::signed(6), 1, 6), + Error::::NomBondBelowMin + ); + assert_ok!(Stake::nominator_bond_more(Origin::signed(6), 1, 10)); + assert_noop!( + Stake::nominator_bond_less(Origin::signed(6), 2, 5), + Error::::NominationDNE + ); + assert_noop!( + Stake::nominator_bond_more(Origin::signed(6), 1, 81), + DispatchError::Module { + index: 0, + error: 3, + message: Some("InsufficientBalance") + } + ); + roll_to(9); + assert_eq!(Balances::reserved_balance(&6), 20); + assert_ok!(Stake::leave_candidates(Origin::signed(1))); + roll_to(31); + assert!(!Stake::is_nominator(&6)); + assert_eq!(Balances::reserved_balance(&6), 0); + assert_eq!(Balances::free_balance(&6), 100); + }); +} From 1cd65aa3d531dd29c79a3b73a4d82331a4722076 Mon Sep 17 00:00:00 2001 From: 4meta5 Date: Thu, 21 Jan 2021 12:30:30 -0800 Subject: [PATCH 09/23] fmt --- pallets/stake/src/lib.rs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/pallets/stake/src/lib.rs b/pallets/stake/src/lib.rs index dbbbcdfa2b..12f53f9271 100644 --- a/pallets/stake/src/lib.rs +++ b/pallets/stake/src/lib.rs @@ -166,7 +166,7 @@ impl< self.bond += more; self.total += more; } - // Returns None if underflow or less == self.bond (in which case validator should leave instead of bonding less) + // Returns None if underflow or less == self.bond (in which case validator should leave instead) pub fn bond_less(&mut self, less: B) -> Option { if self.bond > less { self.bond -= less; @@ -630,7 +630,11 @@ decl_module! { Self::nominator_revokes_validator(ensure_signed(origin)?, validator.clone()) } #[weight = 0] - fn nominator_bond_more(origin, candidate: T::AccountId, more: BalanceOf) -> DispatchResult { + fn nominator_bond_more( + origin, + candidate: T::AccountId, + more: BalanceOf + ) -> DispatchResult { let nominator = ensure_signed(origin)?; let mut nominations = >::get(&nominator).ok_or(Error::::NominatorDNE)?; let mut validator = >::get(&candidate).ok_or(Error::::CandidateDNE)?; @@ -650,7 +654,11 @@ decl_module! { Ok(()) } #[weight = 0] - fn nominator_bond_less(origin, candidate: T::AccountId, less: BalanceOf) -> DispatchResult { + fn nominator_bond_less( + origin, + candidate: T::AccountId, + less: BalanceOf + ) -> DispatchResult { let nominator = ensure_signed(origin)?; let mut nominations = >::get(&nominator).ok_or(Error::::NominatorDNE)?; let mut validator = >::get(&candidate).ok_or(Error::::CandidateDNE)?; From 00d99238a71174bb7edfcdd5f99c4a660b5cc6df Mon Sep 17 00:00:00 2001 From: 4meta5 Date: Sun, 24 Jan 2021 11:38:05 -0800 Subject: [PATCH 10/23] switch nominatiion --- pallets/stake/src/lib.rs | 184 +++++++++++++++++++++++++++++++++++-- pallets/stake/src/tests.rs | 103 +++++++++++++++++++-- 2 files changed, 267 insertions(+), 20 deletions(-) diff --git a/pallets/stake/src/lib.rs b/pallets/stake/src/lib.rs index 12f53f9271..6abf0d0bc7 100644 --- a/pallets/stake/src/lib.rs +++ b/pallets/stake/src/lib.rs @@ -176,6 +176,56 @@ impl< None } } + // infallible so nominator must exist before calling + pub fn rm_nominator(&mut self, nominator: A) -> B { + let mut total = self.total; + let nominators = self + .nominators + .0 + .iter() + .filter_map(|x| { + if x.owner == nominator { + total -= x.amount; + None + } else { + Some(x.clone()) + } + }) + .collect(); + self.nominators = OrderedSet::from(nominators); + self.total = total; + total + } + // infallible so nominator dne before calling + pub fn add_nominator(&mut self, owner: A, amount: B) -> B { + self.nominators.insert(Bond { owner, amount }); + self.total += amount; + self.total + } + // only call with an amount larger than existing amount + pub fn update_nominator(&mut self, nominator: A, amount: B) -> B { + let mut difference: B = 0u32.into(); + let nominators = self + .nominators + .0 + .iter() + .filter_map(|x| { + if x.owner == nominator { + // new amount must be greater or will underflow + difference = amount - x.amount; + Some(Bond { + owner: x.owner.clone(), + amount, + }) + } else { + Some(x.clone()) + } + }) + .collect(); + self.nominators = OrderedSet::from(nominators); + self.total += difference; + self.total + } pub fn inc_nominator(&mut self, nominator: A, more: B) { for x in &mut self.nominators.0 { if x.owner == nominator { @@ -223,7 +273,11 @@ pub struct Nominator { impl< AccountId: Ord + Clone, - Balance: Copy + sp_std::ops::AddAssign + sp_std::ops::SubAssign + PartialOrd, + Balance: Copy + + sp_std::ops::AddAssign + + sp_std::ops::Add + + sp_std::ops::SubAssign + + PartialOrd, > Nominator { pub fn new(validator: AccountId, nomination: Balance) -> Self { @@ -246,7 +300,7 @@ impl< } // Returns Some(remaining balance), must be more than MinNominatorStk // Returns None if nomination not found - pub fn sub_nomination(&mut self, validator: AccountId) -> Option { + pub fn rm_nomination(&mut self, validator: AccountId) -> Option { let mut amt: Option = None; let nominations = self .nominations @@ -269,6 +323,64 @@ impl< None } } + // Returns Some(new balances) if old was nominated and None if it wasn't nominated + pub fn swap_nomination( + &mut self, + old: AccountId, + new: AccountId, + ) -> Option<(Balance, Balance)> { + let mut amt: Option = None; + let nominations = self + .nominations + .0 + .iter() + .filter_map(|x| { + if x.owner == old { + amt = Some(x.amount); + None + } else { + Some(x.clone()) + } + }) + .collect(); + if let Some(swapped_amt) = amt { + let mut old_new_amt: Option = None; + let nominations2 = self + .nominations + .0 + .iter() + .filter_map(|x| { + if x.owner == new { + old_new_amt = Some(x.amount); + None + } else { + Some(x.clone()) + } + }) + .collect(); + let new_amount = if let Some(old_amt) = old_new_amt { + // update existing nomination + self.nominations = OrderedSet::from(nominations2); + let new_amt = old_amt + swapped_amt; + self.nominations.insert(Bond { + owner: new, + amount: new_amt, + }); + new_amt + } else { + // insert completely new nomination + self.nominations = OrderedSet::from(nominations); + self.nominations.insert(Bond { + owner: new, + amount: swapped_amt, + }); + swapped_amt + }; + Some((swapped_amt, new_amount)) + } else { + return None; + } + } // Returns None if nomination not found pub fn inc_nomination(&mut self, validator: AccountId, more: Balance) -> Option { for x in &mut self.nominations.0 { @@ -353,16 +465,18 @@ decl_event!( ValidatorBondedMore(AccountId, Balance, Balance), /// Validator Account, Old Bond, New Bond ValidatorBondedLess(AccountId, Balance, Balance), - // Nominator, Validator, Old Nomination, New Nomination - NominationIncreased(AccountId, AccountId, Balance, Balance), - // Nominator, Validator, Old Nomination, New Nomination - NominationDecreased(AccountId, AccountId, Balance, Balance), ValidatorWentOffline(RoundIndex, AccountId), ValidatorBackOnline(RoundIndex, AccountId), /// Round, Validator Account, Scheduled Exit ValidatorScheduledExit(RoundIndex, AccountId, RoundIndex), /// Account, Amount Unlocked, New Total Amt Locked ValidatorLeft(AccountId, Balance, Balance), + // Nominator, Validator, Old Nomination, New Nomination + NominationIncreased(AccountId, AccountId, Balance, Balance), + // Nominator, Validator, Old Nomination, New Nomination + NominationDecreased(AccountId, AccountId, Balance, Balance), + // Nominator, Swapped Amount, Old Nominator, New Nominator + NominationSwapped(AccountId, Balance, AccountId, AccountId), /// Nominator, Amount Staked NominatorJoined(AccountId, Balance), /// Nominator, Amount Unstaked @@ -605,7 +719,7 @@ decl_module! { Ok(()) } #[weight = 0] - fn nominate( + fn nominate_new( origin, validator: T::AccountId, amount: BalanceOf, @@ -617,12 +731,62 @@ decl_module! { nominator.nominations.0.len() < T::MaxValidatorsPerNominator::get(), Error::::ExceedMaxValidatorsPerNom ); + let mut state = >::get(&validator).ok_or(Error::::CandidateDNE)?; ensure!( nominator.add_nomination(Bond{owner:validator.clone(), amount}), Error::::AlreadyNominatedValidator ); - Self::nominator_joins_validator(acc.clone(), amount, validator.clone())?; + let nomination = Bond { + owner: acc.clone(), + amount, + }; + ensure!( + state.nominators.0.len() < T::MaxNominatorsPerValidator::get(), + Error::::TooManyNominators + ); + ensure!( + state.nominators.insert(nomination), + Error::::NominatorExists + ); + T::Currency::reserve(&acc, amount)?; + let new_total = state.total + amount; + if state.is_active() { + Self::update_active(validator.clone(), new_total); + } + let new_total_locked = >::get() + amount; + >::put(new_total_locked); + state.total = new_total; + >::insert(&validator, state); + >::insert(&acc, nominator); + Self::deposit_event(RawEvent::ValidatorNominated( + acc, amount, validator, new_total, + )); + Ok(()) + } + #[weight = 0] + fn switch_nomination(origin, old: T::AccountId, new: T::AccountId) -> DispatchResult { + let acc = ensure_signed(origin)?; + let mut nominator = >::get(&acc).ok_or(Error::::NominatorDNE)?; + let mut old_validator = >::get(&old).ok_or(Error::::CandidateDNE)?; + let mut new_validator = >::get(&new).ok_or(Error::::CandidateDNE)?; + let (swapped_amt, new_amt) = nominator + .swap_nomination(old.clone(), new.clone()) + .ok_or(Error::::NominationDNE)?; + let (new_old, new_new) = if new_amt > swapped_amt { + (old_validator.rm_nominator(acc.clone()), new_validator.update_nominator(acc.clone(), new_amt)) + } else { + (old_validator.rm_nominator(acc.clone()), new_validator.add_nominator(acc.clone(), swapped_amt)) + }; + if old_validator.is_active() { + Self::update_active(old.clone(), new_old); + } + if new_validator.is_active() { + Self::update_active(new.clone(), new_new); + } + >::insert(&old, old_validator); + >::insert(&new, new_validator); >::insert(&acc, nominator); + Self::deposit_event(RawEvent::NominationSwapped(acc, swapped_amt, old, new)); Ok(()) } #[weight = 0] @@ -752,7 +916,7 @@ impl Module { fn nominator_revokes_validator(acc: T::AccountId, validator: T::AccountId) -> DispatchResult { let mut nominator = >::get(&acc).ok_or(Error::::NominatorDNE)?; let remaining = nominator - .sub_nomination(validator.clone()) + .rm_nomination(validator.clone()) .ok_or(Error::::NominationDNE)?; ensure!( remaining >= T::MinNominatorStk::get(), @@ -860,7 +1024,7 @@ impl Module { T::Currency::unreserve(&bond.owner, bond.amount); // remove nomination from nominator state if let Some(mut nominator) = >::get(&bond.owner) { - if let Some(remaining) = nominator.sub_nomination(x.owner.clone()) { + if let Some(remaining) = nominator.rm_nomination(x.owner.clone()) { if remaining.is_zero() { >::remove(&bond.owner); } else { diff --git a/pallets/stake/src/tests.rs b/pallets/stake/src/tests.rs index 7a7cb6d756..d69b3fbc00 100644 --- a/pallets/stake/src/tests.rs +++ b/pallets/stake/src/tests.rs @@ -458,26 +458,26 @@ fn multiple_nomination_works() { ]; assert_eq!(events(), expected); assert_noop!( - Stake::nominate(Origin::signed(5), 2, 10), + Stake::nominate_new(Origin::signed(5), 2, 10), Error::::NominatorDNE, ); assert_noop!( - Stake::nominate(Origin::signed(11), 1, 10), + Stake::nominate_new(Origin::signed(11), 1, 10), Error::::NominatorDNE, ); assert_noop!( - Stake::nominate(Origin::signed(6), 1, 10), + Stake::nominate_new(Origin::signed(6), 1, 10), Error::::AlreadyNominatedValidator, ); assert_noop!( - Stake::nominate(Origin::signed(6), 2, 2), + Stake::nominate_new(Origin::signed(6), 2, 2), Error::::NominationBelowMin, ); - assert_ok!(Stake::nominate(Origin::signed(6), 2, 10)); - assert_ok!(Stake::nominate(Origin::signed(6), 3, 10)); - assert_ok!(Stake::nominate(Origin::signed(6), 4, 10)); + assert_ok!(Stake::nominate_new(Origin::signed(6), 2, 10)); + assert_ok!(Stake::nominate_new(Origin::signed(6), 3, 10)); + assert_ok!(Stake::nominate_new(Origin::signed(6), 4, 10)); assert_noop!( - Stake::nominate(Origin::signed(6), 5, 10), + Stake::nominate_new(Origin::signed(6), 5, 10), Error::::ExceedMaxValidatorsPerNom, ); roll_to(16); @@ -501,9 +501,9 @@ fn multiple_nomination_works() { expected.append(&mut new); assert_eq!(events(), expected); roll_to(21); - assert_ok!(Stake::nominate(Origin::signed(7), 2, 80)); + assert_ok!(Stake::nominate_new(Origin::signed(7), 2, 80)); assert_noop!( - Stake::nominate(Origin::signed(7), 3, 11), + Stake::nominate_new(Origin::signed(7), 3, 11), DispatchError::Module { index: 0, error: 3, @@ -693,3 +693,86 @@ fn nominators_bond_more_less() { assert_eq!(Balances::free_balance(&6), 100); }); } + +#[test] +fn switch_nomination_works() { + genesis3().execute_with(|| { + roll_to(4); + roll_to(8); + let mut expected = vec![ + RawEvent::ValidatorChosen(2, 1, 50), + RawEvent::ValidatorChosen(2, 2, 40), + RawEvent::ValidatorChosen(2, 4, 20), + RawEvent::ValidatorChosen(2, 3, 20), + RawEvent::ValidatorChosen(2, 5, 10), + RawEvent::NewRound(5, 2, 5, 140), + ]; + assert_eq!(events(), expected); + assert_noop!( + Stake::switch_nomination(Origin::signed(1), 1, 2), + Error::::NominatorDNE + ); + assert_noop!( + Stake::switch_nomination(Origin::signed(6), 1, 7), + Error::::CandidateDNE + ); + assert_noop!( + Stake::switch_nomination(Origin::signed(6), 2, 1), + Error::::NominationDNE + ); + assert_ok!(Stake::switch_nomination(Origin::signed(6), 1, 2)); + assert_eq!( + last_event(), + MetaEvent::stake(RawEvent::NominationSwapped(6, 10, 1, 2)) + ); + assert_ok!(Stake::switch_nomination(Origin::signed(7), 1, 2)); + assert_ok!(Stake::switch_nomination(Origin::signed(8), 2, 1)); + assert_eq!( + last_event(), + MetaEvent::stake(RawEvent::NominationSwapped(8, 10, 2, 1)) + ); + assert_ok!(Stake::switch_nomination(Origin::signed(9), 2, 1)); + assert_ok!(Stake::switch_nomination(Origin::signed(10), 1, 2)); + assert_eq!( + last_event(), + MetaEvent::stake(RawEvent::NominationSwapped(10, 10, 1, 2)) + ); + // verify nothing changed with roles or balances since genesis + for x in 1..5 { + assert!(Stake::is_candidate(&x)); + assert_eq!(Balances::free_balance(&x), 80); + assert_eq!(Balances::reserved_balance(&x), 20); + } + assert!(Stake::is_candidate(&5)); + assert_eq!(Balances::free_balance(&5), 90); + assert_eq!(Balances::reserved_balance(&5), 10); + for x in 6..11 { + assert!(Stake::is_nominator(&x)); + assert_eq!(Balances::free_balance(&x), 90); + assert_eq!(Balances::reserved_balance(&x), 10); + } + roll_to(10); + roll_to(16); + let mut new = vec![ + RawEvent::NominationSwapped(6, 10, 1, 2), + RawEvent::NominationSwapped(7, 10, 1, 2), + RawEvent::NominationSwapped(8, 10, 2, 1), + RawEvent::NominationSwapped(9, 10, 2, 1), + RawEvent::NominationSwapped(10, 10, 1, 2), + RawEvent::ValidatorChosen(3, 2, 50), + RawEvent::ValidatorChosen(3, 1, 40), + RawEvent::ValidatorChosen(3, 4, 20), + RawEvent::ValidatorChosen(3, 3, 20), + RawEvent::ValidatorChosen(3, 5, 10), + RawEvent::NewRound(10, 3, 5, 140), + RawEvent::ValidatorChosen(4, 2, 50), + RawEvent::ValidatorChosen(4, 1, 40), + RawEvent::ValidatorChosen(4, 4, 20), + RawEvent::ValidatorChosen(4, 3, 20), + RawEvent::ValidatorChosen(4, 5, 10), + RawEvent::NewRound(15, 4, 5, 140), + ]; + expected.append(&mut new); + assert_eq!(events(), expected); + }); +} From 59e134107c156fb391515a331f876b2236609582 Mon Sep 17 00:00:00 2001 From: 4meta5 Date: Sun, 24 Jan 2021 22:10:51 -0800 Subject: [PATCH 11/23] clean --- pallets/stake/src/lib.rs | 3 --- tests/tests/test-stake.ts | 12 ++++++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/pallets/stake/src/lib.rs b/pallets/stake/src/lib.rs index 6abf0d0bc7..ccc4a0bdc4 100644 --- a/pallets/stake/src/lib.rs +++ b/pallets/stake/src/lib.rs @@ -494,10 +494,8 @@ decl_error! { // Nominator Does Not Exist NominatorDNE, CandidateDNE, - ValidatorDNE, NominatorExists, CandidateExists, - ValidatorExists, FeeOverMax, ValBondBelowMin, NomBondBelowMin, @@ -509,7 +507,6 @@ decl_error! { CannotActivateIfLeaving, ExceedMaxValidatorsPerNom, AlreadyNominatedValidator, - MustNominateAtLeastOne, NominationDNE, Underflow, } diff --git a/tests/tests/test-stake.ts b/tests/tests/test-stake.ts index b7e7da7cf6..dde03e23c0 100644 --- a/tests/tests/test-stake.ts +++ b/tests/tests/test-stake.ts @@ -18,21 +18,25 @@ describeWithMoonbeam("Moonbeam RPC (Stake)", `simple-specs.json`, (context) => { }); step("issuance minted to the sole validator for authoring blocks", async function () { - const expectedBalance = BigInt(GENESIS_ACCOUNT_BALANCE) + 49n * GLMR; - const expectedBalance2 = expectedBalance + 49n * GLMR; + const issuanceEveryRound = 49n * GLMR; + // payment transfer is delayed by two rounds + const balanceAfterBlock40 = BigInt(GENESIS_ACCOUNT_BALANCE) + issuanceEveryRound; + const balanceAfterBlock60 = balanceAfterBlock40 + issuanceEveryRound; var block = await context.web3.eth.getBlockNumber(); while (block < 40) { await createAndFinalizeBlock(context.polkadotApi); block = await context.web3.eth.getBlockNumber(); } - expect(await context.web3.eth.getBalance(GENESIS_ACCOUNT)).to.equal(expectedBalance.toString()); + expect(await context.web3.eth.getBalance(GENESIS_ACCOUNT)).to.equal( + balanceAfterBlock40.toString() + ); while (block < 60) { await createAndFinalizeBlock(context.polkadotApi); block = await context.web3.eth.getBlockNumber(); } expect(await context.web3.eth.getBalance(GENESIS_ACCOUNT)).to.equal( - expectedBalance2.toString() + balanceAfterBlock60.toString() ); }); }); From b2de65a278fdf099837866c93b4639607c5aedca Mon Sep 17 00:00:00 2001 From: 4meta5 Date: Mon, 25 Jan 2021 07:47:49 -0800 Subject: [PATCH 12/23] cannot switch to same nomination --- pallets/stake/src/lib.rs | 2 ++ pallets/stake/src/tests.rs | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/pallets/stake/src/lib.rs b/pallets/stake/src/lib.rs index ccc4a0bdc4..d911457c3e 100644 --- a/pallets/stake/src/lib.rs +++ b/pallets/stake/src/lib.rs @@ -509,6 +509,7 @@ decl_error! { AlreadyNominatedValidator, NominationDNE, Underflow, + CannotSwitchToSameNomination, } } @@ -763,6 +764,7 @@ decl_module! { #[weight = 0] fn switch_nomination(origin, old: T::AccountId, new: T::AccountId) -> DispatchResult { let acc = ensure_signed(origin)?; + ensure!(old != new, Error::::CannotSwitchToSameNomination); let mut nominator = >::get(&acc).ok_or(Error::::NominatorDNE)?; let mut old_validator = >::get(&old).ok_or(Error::::CandidateDNE)?; let mut new_validator = >::get(&new).ok_or(Error::::CandidateDNE)?; diff --git a/pallets/stake/src/tests.rs b/pallets/stake/src/tests.rs index d69b3fbc00..c93269bc27 100644 --- a/pallets/stake/src/tests.rs +++ b/pallets/stake/src/tests.rs @@ -720,6 +720,10 @@ fn switch_nomination_works() { Stake::switch_nomination(Origin::signed(6), 2, 1), Error::::NominationDNE ); + assert_noop!( + Stake::switch_nomination(Origin::signed(6), 1, 1), + Error::::CannotSwitchToSameNomination + ); assert_ok!(Stake::switch_nomination(Origin::signed(6), 1, 2)); assert_eq!( last_event(), From a08d62b70fe007f61074cc83f38b32e6687b37f2 Mon Sep 17 00:00:00 2001 From: 4meta5 Date: Mon, 25 Jan 2021 10:58:31 -0800 Subject: [PATCH 13/23] remove BlockNumber generic from ValidatorStatus --- pallets/stake/src/lib.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pallets/stake/src/lib.rs b/pallets/stake/src/lib.rs index d911457c3e..6f672b3bc0 100644 --- a/pallets/stake/src/lib.rs +++ b/pallets/stake/src/lib.rs @@ -111,17 +111,17 @@ impl Into> for Bond { #[derive(Copy, Clone, PartialEq, Eq, Encode, Decode, RuntimeDebug)] /// The activity status of the validator -pub enum ValidatorStatus { +pub enum ValidatorStatus { /// Committed to be online and producing valid blocks (not equivocating) Active, /// Temporarily inactive and excused for inactivity Idle, - /// Bonded until the wrapped block - Leaving(BlockNumber), + /// Bonded until the inner round + Leaving(RoundIndex), } -impl Default for ValidatorStatus { - fn default() -> ValidatorStatus { +impl Default for ValidatorStatus { + fn default() -> ValidatorStatus { ValidatorStatus::Active } } @@ -133,7 +133,7 @@ pub struct Validator { pub bond: Balance, pub nominators: OrderedSet>, pub total: Balance, - pub state: ValidatorStatus, + pub state: ValidatorStatus, } impl< From 1676494bb1f1d480b1f282df38134f5ada679065 Mon Sep 17 00:00:00 2001 From: 4meta5 Date: Mon, 25 Jan 2021 22:59:56 -0800 Subject: [PATCH 14/23] more payout distribution unit tests --- pallets/stake/src/lib.rs | 32 +++++++++++-------- pallets/stake/src/mock.rs | 6 ++++ pallets/stake/src/tests.rs | 65 ++++++++++++++++++++++++++++++-------- 3 files changed, 75 insertions(+), 28 deletions(-) diff --git a/pallets/stake/src/lib.rs b/pallets/stake/src/lib.rs index 6f672b3bc0..591a6c1091 100644 --- a/pallets/stake/src/lib.rs +++ b/pallets/stake/src/lib.rs @@ -250,8 +250,8 @@ impl< pub fn go_online(&mut self) { self.state = ValidatorStatus::Active; } - pub fn leave_candidates(&mut self, block: RoundIndex) { - self.state = ValidatorStatus::Leaving(block); + pub fn leave_candidates(&mut self, round: RoundIndex) { + self.state = ValidatorStatus::Leaving(round); } } @@ -965,6 +965,13 @@ impl Module { Ok(()) } fn pay_stakers(next: RoundIndex) { + let mint = |amt: BalanceOf, to: T::AccountId| { + if amt > T::Currency::minimum_balance() { + if let Some(imb) = T::Currency::deposit_into_existing(&to, amt).ok() { + Self::deposit_event(RawEvent::Rewarded(to.clone(), imb.peek())); + } + } + }; let duration = T::BondDuration::get(); if next > duration { let round_to_payout = next - duration; @@ -976,34 +983,31 @@ impl Module { for (val, pts) in >::drain_prefix(round_to_payout) { let pct_due = Perbill::from_rational_approximation(pts, total); let mut amt_due = pct_due * issuance; - if amt_due < T::Currency::minimum_balance() { + if amt_due <= T::Currency::minimum_balance() { continue; } if let Some(state) = >::get(&val) { - if state.nominators.0.len() == 0usize { + if state.nominators.0.is_empty() { // solo validator with no nominators if let Some(imb) = T::Currency::deposit_into_existing(&val, amt_due).ok() { Self::deposit_event(RawEvent::Rewarded(val.clone(), imb.peek())); } } else { let fee = state.fee * amt_due; - if let Some(imb) = T::Currency::deposit_into_existing(&val, fee).ok() { - Self::deposit_event(RawEvent::Rewarded(val.clone(), imb.peek())); + if fee > T::Currency::minimum_balance() { + if let Some(imb) = T::Currency::deposit_into_existing(&val, fee).ok() { + amt_due -= fee; + Self::deposit_event(RawEvent::Rewarded(val.clone(), imb.peek())); + } } - amt_due -= fee; for Bond { owner, amount } in state.nominators.0 { let percent = Perbill::from_rational_approximation(amount, state.total); let due = percent * amt_due; - if let Some(imb) = T::Currency::deposit_into_existing(&owner, due).ok() - { - Self::deposit_event(RawEvent::Rewarded(owner.clone(), imb.peek())); - } + mint(due, owner); } let pct = Perbill::from_rational_approximation(state.bond, state.total); let due = pct * amt_due; - if let Some(imb) = T::Currency::deposit_into_existing(&state.id, due).ok() { - Self::deposit_event(RawEvent::Rewarded(state.id.clone(), imb.peek())); - } + mint(due, val.clone()); } } } diff --git a/pallets/stake/src/mock.rs b/pallets/stake/src/mock.rs index a42e7dabea..85f47c1b53 100644 --- a/pallets/stake/src/mock.rs +++ b/pallets/stake/src/mock.rs @@ -268,3 +268,9 @@ pub fn events() -> Vec> { }) .collect::>() } + +// Same storage changes as EventHandler::note_author impl +pub fn set_author(round: u32, acc: u64, pts: u32) { + ::Points::mutate(round, |p| *p += pts); + ::AwardedPts::mutate(round, acc, |p| *p += pts); +} diff --git a/pallets/stake/src/tests.rs b/pallets/stake/src/tests.rs index c93269bc27..5545ecf728 100644 --- a/pallets/stake/src/tests.rs +++ b/pallets/stake/src/tests.rs @@ -337,13 +337,8 @@ fn exit_queue_works() { } #[test] -fn payout_distribution_works() { +fn payout_distribution_for_just_validators_works() { genesis2().execute_with(|| { - // same storage changes as EventHandler::note_author impl - fn set_pts(round: u32, acc: u64, pts: u32) { - ::Points::mutate(round, |p| *p += pts); - ::AwardedPts::insert(round, acc, pts); - } roll_to(4); roll_to(8); // should choose top MaxValidators (5), in order @@ -357,7 +352,7 @@ fn payout_distribution_works() { ]; assert_eq!(events(), expected); // ~ set block author as 1 for all blocks this round - set_pts(2, 1, 100); + set_author(2, 1, 100); roll_to(16); // pay total issuance (=10) to 1 let mut new = vec![ @@ -378,9 +373,9 @@ fn payout_distribution_works() { expected.append(&mut new); assert_eq!(events(), expected); // ~ set block author as 1 for 3 blocks this round - set_pts(4, 1, 60); + set_author(4, 1, 60); // ~ set block author as 2 for 2 blocks this round - set_pts(4, 2, 40); + set_author(4, 2, 40); roll_to(26); // pay 60% total issuance to 1 and 40% total issuance to 2 let mut new1 = vec![ @@ -402,11 +397,11 @@ fn payout_distribution_works() { expected.append(&mut new1); assert_eq!(events(), expected); // ~ each validator produces 1 block this round - set_pts(6, 1, 20); - set_pts(6, 2, 20); - set_pts(6, 3, 20); - set_pts(6, 4, 20); - set_pts(6, 5, 20); + set_author(6, 1, 20); + set_author(6, 2, 20); + set_author(6, 3, 20); + set_author(6, 4, 20); + set_author(6, 5, 20); roll_to(36); // pay 20% issuance for all validators let mut new2 = vec![ @@ -442,6 +437,48 @@ fn payout_distribution_works() { }); } +#[test] +fn payout_distribution_for_validators_with_nominators_works() { + genesis3().execute_with(|| { + roll_to(4); + roll_to(8); + // chooses top MaxValidators (5), in order + let mut expected = vec![ + RawEvent::ValidatorChosen(2, 1, 50), + RawEvent::ValidatorChosen(2, 2, 40), + RawEvent::ValidatorChosen(2, 4, 20), + RawEvent::ValidatorChosen(2, 3, 20), + RawEvent::ValidatorChosen(2, 5, 10), + RawEvent::NewRound(5, 2, 5, 140), + ]; + assert_eq!(events(), expected); + // ~ set block author as 1 for all blocks this round + set_author(2, 1, 100); + roll_to(16); + // distribute total issuance (=10) to validator 1 and its nominators 6, 7, 19 + let mut new = vec![ + RawEvent::ValidatorChosen(3, 1, 50), + RawEvent::ValidatorChosen(3, 2, 40), + RawEvent::ValidatorChosen(3, 4, 20), + RawEvent::ValidatorChosen(3, 3, 20), + RawEvent::ValidatorChosen(3, 5, 10), + RawEvent::NewRound(10, 3, 5, 140), + RawEvent::Rewarded(6, 2), + RawEvent::Rewarded(7, 2), + RawEvent::Rewarded(10, 2), + RawEvent::Rewarded(1, 4), + RawEvent::ValidatorChosen(4, 1, 50), + RawEvent::ValidatorChosen(4, 2, 40), + RawEvent::ValidatorChosen(4, 4, 20), + RawEvent::ValidatorChosen(4, 3, 20), + RawEvent::ValidatorChosen(4, 5, 10), + RawEvent::NewRound(15, 4, 5, 140), + ]; + expected.append(&mut new); + assert_eq!(events(), expected); + }); +} + #[test] fn multiple_nomination_works() { genesis3().execute_with(|| { From 4484b3046ffaff213cb4a85a6b8d7ecd39b4677a Mon Sep 17 00:00:00 2001 From: 4meta5 Date: Mon, 25 Jan 2021 23:23:37 -0800 Subject: [PATCH 15/23] fmt --- pallets/author-inherent/src/lib.rs | 8 +++--- pallets/stake/src/lib.rs | 40 +++++++++++++----------------- 2 files changed, 20 insertions(+), 28 deletions(-) diff --git a/pallets/author-inherent/src/lib.rs b/pallets/author-inherent/src/lib.rs index 0a3b9617fc..55fc8e47ae 100644 --- a/pallets/author-inherent/src/lib.rs +++ b/pallets/author-inherent/src/lib.rs @@ -199,11 +199,9 @@ impl ProvideInherent for Module { let author_raw = data .get_data::(&INHERENT_IDENTIFIER) .expect("Gets and decodes authorship inherent data") - .ok_or_else(|| { - InherentError::Other(sp_runtime::RuntimeString::Borrowed( - "Decode authorship inherent data failed", - )) - })?; + .ok_or(InherentError::Other(sp_runtime::RuntimeString::Borrowed( + "Decode authorship inherent data failed", + )))?; let author = T::AccountId::decode(&mut &author_raw[..]).expect("Decodes author raw inherent data"); ensure!( diff --git a/pallets/stake/src/lib.rs b/pallets/stake/src/lib.rs index 591a6c1091..ec686891c2 100644 --- a/pallets/stake/src/lib.rs +++ b/pallets/stake/src/lib.rs @@ -156,11 +156,7 @@ impl< self.state == ValidatorStatus::Active } pub fn is_leaving(&self) -> bool { - if let ValidatorStatus::Leaving(_) = self.state { - true - } else { - false - } + matches!(self.state, ValidatorStatus::Leaving(_)) } pub fn bond_more(&mut self, more: B) { self.bond += more; @@ -209,16 +205,16 @@ impl< .nominators .0 .iter() - .filter_map(|x| { + .map(|x| { if x.owner == nominator { // new amount must be greater or will underflow difference = amount - x.amount; - Some(Bond { + Bond { owner: x.owner.clone(), amount, - }) + } } else { - Some(x.clone()) + x.clone() } }) .collect(); @@ -283,7 +279,7 @@ impl< pub fn new(validator: AccountId, nomination: Balance) -> Self { Nominator { nominations: OrderedSet::from(vec![Bond { - owner: validator.clone(), + owner: validator, amount: nomination, }]), total: nomination, @@ -378,7 +374,7 @@ impl< }; Some((swapped_amt, new_amount)) } else { - return None; + None } } // Returns None if nomination not found @@ -701,8 +697,8 @@ decl_module! { ensure!(!Self::is_nominator(&acc),Error::::NominatorExists); ensure!(!Self::is_candidate(&acc),Error::::CandidateExists); Self::nominator_joins_validator(acc.clone(), amount, validator.clone())?; - >::insert(&acc, Nominator::new(validator.clone(),amount)); - Self::deposit_event(RawEvent::NominatorJoined(acc,amount)); + >::insert(&acc, Nominator::new(validator, amount)); + Self::deposit_event(RawEvent::NominatorJoined(acc, amount)); Ok(()) } #[weight = 0] @@ -713,7 +709,7 @@ decl_module! { Self::nominator_leaves_validator(acc.clone(), bond.owner.clone())?; } >::remove(&acc); - Self::deposit_event(RawEvent::NominatorLeft(acc.clone(), nominator.total)); + Self::deposit_event(RawEvent::NominatorLeft(acc, nominator.total)); Ok(()) } #[weight = 0] @@ -790,7 +786,7 @@ decl_module! { } #[weight = 0] fn revoke_nomination(origin, validator: T::AccountId) -> DispatchResult { - Self::nominator_revokes_validator(ensure_signed(origin)?, validator.clone()) + Self::nominator_revokes_validator(ensure_signed(origin)?, validator) } #[weight = 0] fn nominator_bond_more( @@ -875,7 +871,7 @@ impl Module { let mut candidates = >::get(); candidates.remove(&Bond::from_owner(candidate.clone())); candidates.insert(Bond { - owner: candidate.clone(), + owner: candidate, amount: total, }); >::put(candidates); @@ -921,7 +917,7 @@ impl Module { remaining >= T::MinNominatorStk::get(), Error::::NomBondBelowMin ); - Self::nominator_leaves_validator(acc.clone(), validator.clone())?; + Self::nominator_leaves_validator(acc.clone(), validator)?; >::insert(&acc, nominator); Ok(()) } @@ -967,7 +963,7 @@ impl Module { fn pay_stakers(next: RoundIndex) { let mint = |amt: BalanceOf, to: T::AccountId| { if amt > T::Currency::minimum_balance() { - if let Some(imb) = T::Currency::deposit_into_existing(&to, amt).ok() { + if let Ok(imb) = T::Currency::deposit_into_existing(&to, amt) { Self::deposit_event(RawEvent::Rewarded(to.clone(), imb.peek())); } } @@ -989,13 +985,11 @@ impl Module { if let Some(state) = >::get(&val) { if state.nominators.0.is_empty() { // solo validator with no nominators - if let Some(imb) = T::Currency::deposit_into_existing(&val, amt_due).ok() { - Self::deposit_event(RawEvent::Rewarded(val.clone(), imb.peek())); - } + mint(amt_due, val.clone()); } else { let fee = state.fee * amt_due; if fee > T::Currency::minimum_balance() { - if let Some(imb) = T::Currency::deposit_into_existing(&val, fee).ok() { + if let Ok(imb) = T::Currency::deposit_into_existing(&val, fee) { amt_due -= fee; Self::deposit_event(RawEvent::Rewarded(val.clone(), imb.peek())); } @@ -1042,7 +1036,7 @@ impl Module { >::put(new_total); >::remove(&x.owner); Self::deposit_event(RawEvent::ValidatorLeft( - x.owner.clone(), + x.owner, state.total, new_total, )); From 30f1d45399b1d8dde3a9f76ca0090c908b59d621 Mon Sep 17 00:00:00 2001 From: 4meta5 Date: Tue, 26 Jan 2021 17:39:09 -0800 Subject: [PATCH 16/23] pay validators before nominators --- pallets/stake/src/lib.rs | 22 +++++++------ pallets/stake/src/mock.rs | 38 +++++++++++++++++---- pallets/stake/src/tests.rs | 67 +++++++++++++++++++++++++++++++++++--- 3 files changed, 106 insertions(+), 21 deletions(-) diff --git a/pallets/stake/src/lib.rs b/pallets/stake/src/lib.rs index ec686891c2..8528a91b20 100644 --- a/pallets/stake/src/lib.rs +++ b/pallets/stake/src/lib.rs @@ -987,21 +987,23 @@ impl Module { // solo validator with no nominators mint(amt_due, val.clone()); } else { - let fee = state.fee * amt_due; - if fee > T::Currency::minimum_balance() { - if let Ok(imb) = T::Currency::deposit_into_existing(&val, fee) { - amt_due -= fee; - Self::deposit_event(RawEvent::Rewarded(val.clone(), imb.peek())); - } - } + // pay validator first; commission + due_portion + let val_pct = Perbill::from_rational_approximation(state.bond, state.total); + let commission = state.fee * amt_due; + let val_due = if commission > T::Currency::minimum_balance() { + amt_due -= commission; + (val_pct * amt_due) + commission + } else { + // commission is negligible so not applied + val_pct * amt_due + }; + mint(val_due, val.clone()); + // pay nominators due portion for Bond { owner, amount } in state.nominators.0 { let percent = Perbill::from_rational_approximation(amount, state.total); let due = percent * amt_due; mint(due, owner); } - let pct = Perbill::from_rational_approximation(state.bond, state.total); - let due = pct * amt_due; - mint(due, val.clone()); } } } diff --git a/pallets/stake/src/mock.rs b/pallets/stake/src/mock.rs index 85f47c1b53..ca4c3daf07 100644 --- a/pallets/stake/src/mock.rs +++ b/pallets/stake/src/mock.rs @@ -124,7 +124,7 @@ pub type Balances = pallet_balances::Module; pub type Stake = Module; pub type Sys = frame_system::Module; -pub fn genesis() -> sp_io::TestExternalities { +pub(crate) fn genesis() -> sp_io::TestExternalities { let mut storage = frame_system::GenesisConfig::default() .build_storage::() .unwrap(); @@ -161,7 +161,7 @@ pub fn genesis() -> sp_io::TestExternalities { ext } -pub fn genesis2() -> sp_io::TestExternalities { +pub(crate) fn genesis2() -> sp_io::TestExternalities { let mut storage = frame_system::GenesisConfig::default() .build_storage::() .unwrap(); @@ -197,7 +197,7 @@ pub fn genesis2() -> sp_io::TestExternalities { ext } -pub fn genesis3() -> sp_io::TestExternalities { +pub(crate) fn genesis3() -> sp_io::TestExternalities { let mut storage = frame_system::GenesisConfig::default() .build_storage::() .unwrap(); @@ -239,7 +239,31 @@ pub fn genesis3() -> sp_io::TestExternalities { ext } -pub fn roll_to(n: u64) { +pub(crate) fn genesis4() -> sp_io::TestExternalities { + let mut storage = frame_system::GenesisConfig::default() + .build_storage::() + .unwrap(); + let genesis = pallet_balances::GenesisConfig:: { + balances: vec![(1, 100), (2, 100), (3, 100), (4, 100), (5, 100), (6, 100)], + }; + genesis.assimilate_storage(&mut storage).unwrap(); + GenesisConfig:: { + stakers: vec![ + // validators + (1, None, 20), + // nominators + (2, Some(1), 10), + (3, Some(1), 10), + ], + } + .assimilate_storage(&mut storage) + .unwrap(); + let mut ext = sp_io::TestExternalities::from(storage); + ext.execute_with(|| Sys::set_block_number(1)); + ext +} + +pub(crate) fn roll_to(n: u64) { while Sys::block_number() < n { Stake::on_finalize(Sys::block_number()); Balances::on_finalize(Sys::block_number()); @@ -251,11 +275,11 @@ pub fn roll_to(n: u64) { } } -pub fn last_event() -> MetaEvent { +pub(crate) fn last_event() -> MetaEvent { Sys::events().pop().expect("Event expected").event } -pub fn events() -> Vec> { +pub(crate) fn events() -> Vec> { Sys::events() .into_iter() .map(|r| r.event) @@ -270,7 +294,7 @@ pub fn events() -> Vec> { } // Same storage changes as EventHandler::note_author impl -pub fn set_author(round: u32, acc: u64, pts: u32) { +pub(crate) fn set_author(round: u32, acc: u64, pts: u32) { ::Points::mutate(round, |p| *p += pts); ::AwardedPts::mutate(round, acc, |p| *p += pts); } diff --git a/pallets/stake/src/tests.rs b/pallets/stake/src/tests.rs index 5545ecf728..f6e74bb4cb 100644 --- a/pallets/stake/src/tests.rs +++ b/pallets/stake/src/tests.rs @@ -337,7 +337,7 @@ fn exit_queue_works() { } #[test] -fn payout_distribution_for_just_validators_works() { +fn payout_distribution_to_solo_validators() { genesis2().execute_with(|| { roll_to(4); roll_to(8); @@ -438,7 +438,7 @@ fn payout_distribution_for_just_validators_works() { } #[test] -fn payout_distribution_for_validators_with_nominators_works() { +fn payout_distribution_to_nominators() { genesis3().execute_with(|| { roll_to(4); roll_to(8); @@ -456,6 +456,8 @@ fn payout_distribution_for_validators_with_nominators_works() { set_author(2, 1, 100); roll_to(16); // distribute total issuance (=10) to validator 1 and its nominators 6, 7, 19 + // -> NOTE that no fee is taken because validators at genesis set default 2% fee + // and 2% of 10 is ~0 by the Perbill arithmetic let mut new = vec![ RawEvent::ValidatorChosen(3, 1, 50), RawEvent::ValidatorChosen(3, 2, 40), @@ -463,10 +465,10 @@ fn payout_distribution_for_validators_with_nominators_works() { RawEvent::ValidatorChosen(3, 3, 20), RawEvent::ValidatorChosen(3, 5, 10), RawEvent::NewRound(10, 3, 5, 140), + RawEvent::Rewarded(1, 4), RawEvent::Rewarded(6, 2), RawEvent::Rewarded(7, 2), RawEvent::Rewarded(10, 2), - RawEvent::Rewarded(1, 4), RawEvent::ValidatorChosen(4, 1, 50), RawEvent::ValidatorChosen(4, 2, 40), RawEvent::ValidatorChosen(4, 4, 20), @@ -480,7 +482,64 @@ fn payout_distribution_for_validators_with_nominators_works() { } #[test] -fn multiple_nomination_works() { +fn pays_validator_commission() { + genesis4().execute_with(|| { + roll_to(4); + roll_to(8); + // chooses top MaxValidators (5), in order + let mut expected = vec![ + RawEvent::ValidatorChosen(2, 1, 40), + RawEvent::NewRound(5, 2, 1, 40), + ]; + assert_eq!(events(), expected); + assert_ok!(Stake::join_candidates( + Origin::signed(4), + Perbill::from_percent(20), + 20u128 + )); + assert_eq!( + last_event(), + MetaEvent::stake(RawEvent::JoinedValidatorCandidates(4, 20u128, 60u128)) + ); + roll_to(9); + assert_ok!(Stake::join_nominators(Origin::signed(5), 4, 10)); + assert_ok!(Stake::join_nominators(Origin::signed(6), 4, 10)); + roll_to(11); + let mut new = vec![ + RawEvent::JoinedValidatorCandidates(4, 20, 60), + RawEvent::ValidatorNominated(5, 10, 4, 30), + RawEvent::NominatorJoined(5, 10), + RawEvent::ValidatorNominated(6, 10, 4, 40), + RawEvent::NominatorJoined(6, 10), + RawEvent::ValidatorChosen(3, 4, 40), + RawEvent::ValidatorChosen(3, 1, 40), + RawEvent::NewRound(10, 3, 2, 80), + ]; + expected.append(&mut new); + assert_eq!(events(), expected); + // only reward author with id 4 + set_author(3, 4, 100); + roll_to(21); + // 20% of 10 is commission + due_portion (4) = 2 + 4 = 6 + // all nominator payouts are 10-2 = 8 * stake_pct + let mut new2 = vec![ + RawEvent::ValidatorChosen(4, 4, 40), + RawEvent::ValidatorChosen(4, 1, 40), + RawEvent::NewRound(15, 4, 2, 80), + RawEvent::Rewarded(4, 6), + RawEvent::Rewarded(5, 2), + RawEvent::Rewarded(6, 2), + RawEvent::ValidatorChosen(5, 4, 40), + RawEvent::ValidatorChosen(5, 1, 40), + RawEvent::NewRound(20, 5, 2, 80), + ]; + expected.append(&mut new2); + assert_eq!(events(), expected); + }); +} + +#[test] +fn multiple_nominations() { genesis3().execute_with(|| { roll_to(4); roll_to(8); From a629b822a3b06fee25be65a60b4d0484f0d97004 Mon Sep 17 00:00:00 2001 From: 4meta5 Date: Wed, 27 Jan 2021 11:45:22 -0800 Subject: [PATCH 17/23] remove hidden default fee from genesis config and create no fee validator registration path --- pallets/stake/src/lib.rs | 38 +++++++++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/pallets/stake/src/lib.rs b/pallets/stake/src/lib.rs index 8528a91b20..4c698bfce4 100644 --- a/pallets/stake/src/lib.rs +++ b/pallets/stake/src/lib.rs @@ -554,9 +554,8 @@ decl_storage! { balance, ) } else { - >::join_candidates( + >::join_candidates_no_fee( T::Origin::from(Some(actor.clone()).into()), - Perbill::from_percent(2),// default fee for validators set at genesis is 2% balance, ) }; @@ -576,6 +575,30 @@ decl_module! { type Error = Error; fn deposit_event() = default; + #[weight = 0] + fn join_candidates_no_fee( + origin, + bond: BalanceOf, + ) -> DispatchResult { + let acc = ensure_signed(origin)?; + ensure!(!Self::is_candidate(&acc),Error::::CandidateExists); + ensure!(!Self::is_nominator(&acc),Error::::NominatorExists); + ensure!(bond >= T::MinValidatorStk::get(),Error::::ValBondBelowMin); + let mut candidates = >::get(); + ensure!( + candidates.insert(Bond{owner: acc.clone(), amount: bond}), + Error::::CandidateExists + ); + T::Currency::reserve(&acc, bond)?; + // fee is 0 + let candidate: Candidate = Validator::new(acc.clone(), Perbill::zero(), bond); + let new_total = >::get() + bond; + >::put(new_total); + >::insert(&acc, candidate); + >::put(candidates); + Self::deposit_event(RawEvent::JoinedValidatorCandidates(acc, bond, new_total)); + Ok(()) + } #[weight = 0] fn join_candidates( origin, @@ -592,13 +615,13 @@ decl_module! { candidates.insert(Bond{owner: acc.clone(), amount: bond}), Error::::CandidateExists ); - T::Currency::reserve(&acc,bond)?; - let candidate: Candidate = Validator::new(acc.clone(),fee,bond); + T::Currency::reserve(&acc, bond)?; + let candidate: Candidate = Validator::new(acc.clone(), fee, bond); let new_total = >::get() + bond; >::put(new_total); - >::insert(&acc,candidate); + >::insert(&acc, candidate); >::put(candidates); - Self::deposit_event(RawEvent::JoinedValidatorCandidates(acc,bond,new_total)); + Self::deposit_event(RawEvent::JoinedValidatorCandidates(acc, bond, new_total)); Ok(()) } #[weight = 0] @@ -972,9 +995,6 @@ impl Module { if next > duration { let round_to_payout = next - duration; let total = ::get(round_to_payout); - if total == 0u32 { - return; - } let issuance = T::IssuancePerRound::get(); for (val, pts) in >::drain_prefix(round_to_payout) { let pct_due = Perbill::from_rational_approximation(pts, total); From a47d0d115859e1d9f2736cc9c6fe5a07bf68e695 Mon Sep 17 00:00:00 2001 From: 4meta5 Date: Wed, 27 Jan 2021 12:10:42 -0800 Subject: [PATCH 18/23] bump impl version to 11 --- runtime/src/parachain.rs | 2 +- runtime/src/standalone.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/runtime/src/parachain.rs b/runtime/src/parachain.rs index 413b808d80..eafc39e813 100644 --- a/runtime/src/parachain.rs +++ b/runtime/src/parachain.rs @@ -22,7 +22,7 @@ macro_rules! runtime_parachain { spec_name: create_runtime_str!("moonbase-alphanet"), impl_name: create_runtime_str!("moonbase-alphanet"), authoring_version: 3, - spec_version: 10, + spec_version: 11, impl_version: 1, apis: RUNTIME_API_VERSIONS, transaction_version: 2, diff --git a/runtime/src/standalone.rs b/runtime/src/standalone.rs index d163234a76..298298cd66 100644 --- a/runtime/src/standalone.rs +++ b/runtime/src/standalone.rs @@ -31,7 +31,7 @@ macro_rules! runtime_standalone { spec_name: create_runtime_str!("moonbeam-standalone"), impl_name: create_runtime_str!("moonbeam-standalone"), authoring_version: 3, - spec_version: 10, + spec_version: 11, impl_version: 1, apis: RUNTIME_API_VERSIONS, transaction_version: 2, From 6dc665cd588b8ceeed0c2ce9df0550d263aef2a7 Mon Sep 17 00:00:00 2001 From: 4meta5 Date: Thu, 28 Jan 2021 09:00:53 -0800 Subject: [PATCH 19/23] fix genesis config --- pallets/stake/src/lib.rs | 27 ++------------------------- 1 file changed, 2 insertions(+), 25 deletions(-) diff --git a/pallets/stake/src/lib.rs b/pallets/stake/src/lib.rs index 4c698bfce4..acdebd1a5c 100644 --- a/pallets/stake/src/lib.rs +++ b/pallets/stake/src/lib.rs @@ -554,8 +554,9 @@ decl_storage! { balance, ) } else { - >::join_candidates_no_fee( + >::join_candidates( T::Origin::from(Some(actor.clone()).into()), + Perbill::zero(), // default fee for validators registered at genesis is 0% balance, ) }; @@ -575,30 +576,6 @@ decl_module! { type Error = Error; fn deposit_event() = default; - #[weight = 0] - fn join_candidates_no_fee( - origin, - bond: BalanceOf, - ) -> DispatchResult { - let acc = ensure_signed(origin)?; - ensure!(!Self::is_candidate(&acc),Error::::CandidateExists); - ensure!(!Self::is_nominator(&acc),Error::::NominatorExists); - ensure!(bond >= T::MinValidatorStk::get(),Error::::ValBondBelowMin); - let mut candidates = >::get(); - ensure!( - candidates.insert(Bond{owner: acc.clone(), amount: bond}), - Error::::CandidateExists - ); - T::Currency::reserve(&acc, bond)?; - // fee is 0 - let candidate: Candidate = Validator::new(acc.clone(), Perbill::zero(), bond); - let new_total = >::get() + bond; - >::put(new_total); - >::insert(&acc, candidate); - >::put(candidates); - Self::deposit_event(RawEvent::JoinedValidatorCandidates(acc, bond, new_total)); - Ok(()) - } #[weight = 0] fn join_candidates( origin, From 45bcea280a90b323f81ddcb41ebd06e73509d648 Mon Sep 17 00:00:00 2001 From: 4meta5 Date: Tue, 2 Feb 2021 16:19:08 -0800 Subject: [PATCH 20/23] improve test env naming and organization --- pallets/stake/src/mock.rs | 95 ++++++++++++++------------------------ pallets/stake/src/tests.rs | 32 ++++++------- 2 files changed, 48 insertions(+), 79 deletions(-) diff --git a/pallets/stake/src/mock.rs b/pallets/stake/src/mock.rs index ca4c3daf07..7dc738fef7 100644 --- a/pallets/stake/src/mock.rs +++ b/pallets/stake/src/mock.rs @@ -124,12 +124,26 @@ pub type Balances = pallet_balances::Module; pub type Stake = Module; pub type Sys = frame_system::Module; -pub(crate) fn genesis() -> sp_io::TestExternalities { +fn genesis( + balances: Vec<(AccountId, Balance)>, + stakers: Vec<(AccountId, Option, Balance)>, +) -> sp_io::TestExternalities { let mut storage = frame_system::GenesisConfig::default() .build_storage::() .unwrap(); - let genesis = pallet_balances::GenesisConfig:: { - balances: vec![ + let genesis = pallet_balances::GenesisConfig:: { balances }; + genesis.assimilate_storage(&mut storage).unwrap(); + GenesisConfig:: { stakers } + .assimilate_storage(&mut storage) + .unwrap(); + let mut ext = sp_io::TestExternalities::from(storage); + ext.execute_with(|| Sys::set_block_number(1)); + ext +} + +pub(crate) fn two_validators_four_nominators() -> sp_io::TestExternalities { + genesis( + vec![ (1, 1000), (2, 300), (3, 100), @@ -140,10 +154,7 @@ pub(crate) fn genesis() -> sp_io::TestExternalities { (8, 9), (9, 4), ], - }; - genesis.assimilate_storage(&mut storage).unwrap(); - GenesisConfig:: { - stakers: vec![ + vec![ // validators (1, None, 500), (2, None, 200), @@ -153,20 +164,12 @@ pub(crate) fn genesis() -> sp_io::TestExternalities { (5, Some(2), 100), (6, Some(2), 100), ], - } - .assimilate_storage(&mut storage) - .unwrap(); - let mut ext = sp_io::TestExternalities::from(storage); - ext.execute_with(|| Sys::set_block_number(1)); - ext + ) } -pub(crate) fn genesis2() -> sp_io::TestExternalities { - let mut storage = frame_system::GenesisConfig::default() - .build_storage::() - .unwrap(); - let genesis = pallet_balances::GenesisConfig:: { - balances: vec![ +pub(crate) fn five_validators_no_nominators() -> sp_io::TestExternalities { + genesis( + vec![ (1, 1000), (2, 1000), (3, 1000), @@ -177,10 +180,7 @@ pub(crate) fn genesis2() -> sp_io::TestExternalities { (8, 33), (9, 33), ], - }; - genesis.assimilate_storage(&mut storage).unwrap(); - GenesisConfig:: { - stakers: vec![ + vec![ // validators (1, None, 100), (2, None, 90), @@ -189,20 +189,12 @@ pub(crate) fn genesis2() -> sp_io::TestExternalities { (5, None, 60), (6, None, 50), ], - } - .assimilate_storage(&mut storage) - .unwrap(); - let mut ext = sp_io::TestExternalities::from(storage); - ext.execute_with(|| Sys::set_block_number(1)); - ext + ) } -pub(crate) fn genesis3() -> sp_io::TestExternalities { - let mut storage = frame_system::GenesisConfig::default() - .build_storage::() - .unwrap(); - let genesis = pallet_balances::GenesisConfig:: { - balances: vec![ +pub(crate) fn five_validators_five_nominators() -> sp_io::TestExternalities { + genesis( + vec![ (1, 100), (2, 100), (3, 100), @@ -214,10 +206,7 @@ pub(crate) fn genesis3() -> sp_io::TestExternalities { (9, 100), (10, 100), ], - }; - genesis.assimilate_storage(&mut storage).unwrap(); - GenesisConfig:: { - stakers: vec![ + vec![ // validators (1, None, 20), (2, None, 20), @@ -231,36 +220,20 @@ pub(crate) fn genesis3() -> sp_io::TestExternalities { (9, Some(2), 10), (10, Some(1), 10), ], - } - .assimilate_storage(&mut storage) - .unwrap(); - let mut ext = sp_io::TestExternalities::from(storage); - ext.execute_with(|| Sys::set_block_number(1)); - ext + ) } -pub(crate) fn genesis4() -> sp_io::TestExternalities { - let mut storage = frame_system::GenesisConfig::default() - .build_storage::() - .unwrap(); - let genesis = pallet_balances::GenesisConfig:: { - balances: vec![(1, 100), (2, 100), (3, 100), (4, 100), (5, 100), (6, 100)], - }; - genesis.assimilate_storage(&mut storage).unwrap(); - GenesisConfig:: { - stakers: vec![ +pub(crate) fn one_validator_two_nominators() -> sp_io::TestExternalities { + genesis( + vec![(1, 100), (2, 100), (3, 100), (4, 100), (5, 100), (6, 100)], + vec![ // validators (1, None, 20), // nominators (2, Some(1), 10), (3, Some(1), 10), ], - } - .assimilate_storage(&mut storage) - .unwrap(); - let mut ext = sp_io::TestExternalities::from(storage); - ext.execute_with(|| Sys::set_block_number(1)); - ext + ) } pub(crate) fn roll_to(n: u64) { diff --git a/pallets/stake/src/tests.rs b/pallets/stake/src/tests.rs index f6e74bb4cb..c9803cb533 100644 --- a/pallets/stake/src/tests.rs +++ b/pallets/stake/src/tests.rs @@ -22,7 +22,7 @@ use sp_runtime::DispatchError; #[test] fn genesis_works() { - genesis().execute_with(|| { + two_validators_four_nominators().execute_with(|| { assert!(Sys::events().is_empty()); // validators assert_eq!(Balances::reserved_balance(&1), 500); @@ -48,11 +48,7 @@ fn genesis_works() { assert_eq!(Balances::free_balance(&9), 4); assert_eq!(Balances::reserved_balance(&9), 0); }); -} - -#[test] -fn genesis3_works() { - genesis3().execute_with(|| { + five_validators_five_nominators().execute_with(|| { assert!(Sys::events().is_empty()); // validators for x in 1..5 { @@ -74,7 +70,7 @@ fn genesis3_works() { #[test] fn online_offline_behaves() { - genesis().execute_with(|| { + two_validators_four_nominators().execute_with(|| { roll_to(4); assert_noop!( Stake::go_offline(Origin::signed(3)), @@ -129,7 +125,7 @@ fn online_offline_behaves() { #[test] fn join_validator_candidates_works() { - genesis().execute_with(|| { + two_validators_four_nominators().execute_with(|| { assert_noop!( Stake::join_candidates(Origin::signed(1), Perbill::from_percent(2), 11u128,), Error::::CandidateExists @@ -169,7 +165,7 @@ fn join_validator_candidates_works() { #[test] fn validator_exit_executes_after_delay() { - genesis().execute_with(|| { + two_validators_four_nominators().execute_with(|| { roll_to(4); assert_noop!( Stake::leave_candidates(Origin::signed(3)), @@ -207,7 +203,7 @@ fn validator_exit_executes_after_delay() { #[test] fn validator_selection_chooses_top_candidates() { - genesis2().execute_with(|| { + five_validators_no_nominators().execute_with(|| { roll_to(4); roll_to(8); // should choose top MaxValidators (5), in order @@ -278,7 +274,7 @@ fn validator_selection_chooses_top_candidates() { #[test] fn exit_queue_works() { - genesis2().execute_with(|| { + five_validators_no_nominators().execute_with(|| { roll_to(4); roll_to(8); // should choose top MaxValidators (5), in order @@ -338,7 +334,7 @@ fn exit_queue_works() { #[test] fn payout_distribution_to_solo_validators() { - genesis2().execute_with(|| { + five_validators_no_nominators().execute_with(|| { roll_to(4); roll_to(8); // should choose top MaxValidators (5), in order @@ -439,7 +435,7 @@ fn payout_distribution_to_solo_validators() { #[test] fn payout_distribution_to_nominators() { - genesis3().execute_with(|| { + five_validators_five_nominators().execute_with(|| { roll_to(4); roll_to(8); // chooses top MaxValidators (5), in order @@ -483,7 +479,7 @@ fn payout_distribution_to_nominators() { #[test] fn pays_validator_commission() { - genesis4().execute_with(|| { + one_validator_two_nominators().execute_with(|| { roll_to(4); roll_to(8); // chooses top MaxValidators (5), in order @@ -540,7 +536,7 @@ fn pays_validator_commission() { #[test] fn multiple_nominations() { - genesis3().execute_with(|| { + five_validators_five_nominators().execute_with(|| { roll_to(4); roll_to(8); // chooses top MaxValidators (5), in order @@ -691,7 +687,7 @@ fn multiple_nominations() { #[test] fn validators_bond_more_less() { - genesis3().execute_with(|| { + five_validators_five_nominators().execute_with(|| { roll_to(4); assert_noop!( Stake::candidate_bond_more(Origin::signed(6), 50), @@ -741,7 +737,7 @@ fn validators_bond_more_less() { #[test] fn nominators_bond_more_less() { - genesis3().execute_with(|| { + five_validators_five_nominators().execute_with(|| { roll_to(4); assert_noop!( Stake::nominator_bond_more(Origin::signed(1), 2, 50), @@ -792,7 +788,7 @@ fn nominators_bond_more_less() { #[test] fn switch_nomination_works() { - genesis3().execute_with(|| { + five_validators_five_nominators().execute_with(|| { roll_to(4); roll_to(8); let mut expected = vec![ From 84c7b90c3b7aa37d754606543090f3f0983f23c6 Mon Sep 17 00:00:00 2001 From: 4meta5 Date: Tue, 2 Feb 2021 21:02:29 -0800 Subject: [PATCH 21/23] clean --- moonbeam-types-bundle/index.ts | 7 ++++++- node/src/chain_spec.rs | 6 ++++++ tests/tests/test-stake.ts | 2 +- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/moonbeam-types-bundle/index.ts b/moonbeam-types-bundle/index.ts index aef584c169..1cc408059c 100644 --- a/moonbeam-types-bundle/index.ts +++ b/moonbeam-types-bundle/index.ts @@ -78,12 +78,17 @@ export const moonbeamDefinitions = { ExtrinsicSignature: "EthereumSignature", RoundIndex: "u32", Candidate: { - validator: "AccountId", + id: "AccountId", fee: "Perbill", + bond: "Balance", nominators: "Vec", total: "Balance", state: "ValidatorStatus", }, + Nominator: { + nominations: "Vec", + total: "Balance", + }, Bond: { owner: "AccountId", amount: "Balance", diff --git a/node/src/chain_spec.rs b/node/src/chain_spec.rs index 839ec794cc..9b2be89bee 100644 --- a/node/src/chain_spec.rs +++ b/node/src/chain_spec.rs @@ -54,6 +54,12 @@ pub fn development_chain_spec() -> ChainSpec { move || { testnet_genesis( AccountId::from_str("6Be02d1d3665660d22FF9624b7BE0551ee1Ac91b").unwrap(), + // Validator + vec![( + AccountId::from_str("6Be02d1d3665660d22FF9624b7BE0551ee1Ac91b").unwrap(), + None, + 100_000 * GLMR, + )], vec![AccountId::from_str("6Be02d1d3665660d22FF9624b7BE0551ee1Ac91b").unwrap()], Default::default(), // para_id 1281, //ChainId diff --git a/tests/tests/test-stake.ts b/tests/tests/test-stake.ts index 13bf7eb1c2..88442c122d 100644 --- a/tests/tests/test-stake.ts +++ b/tests/tests/test-stake.ts @@ -41,6 +41,6 @@ describeWithMoonbeam("Moonbeam RPC (Stake)", `simple-specs.json`, (context) => { }); it("candidates set in genesis", async function () { const candidates = await context.polkadotApi.query.stake.candidates(GENESIS_ACCOUNT); - expect((candidates.toHuman() as any).validator.toLowerCase()).equal(GENESIS_ACCOUNT); + expect((candidates.toHuman() as any).id.toLowerCase()).equal(GENESIS_ACCOUNT); }); }); From 9243e613066fee7763762c66256c4ce790ccea12 Mon Sep 17 00:00:00 2001 From: 4meta5 Date: Tue, 2 Feb 2021 21:22:59 -0800 Subject: [PATCH 22/23] fix merge into master --- runtime/src/parachain.rs | 81 -------------------------------- runtime/src/standalone.rs | 97 --------------------------------------- 2 files changed, 178 deletions(-) delete mode 100644 runtime/src/parachain.rs delete mode 100644 runtime/src/standalone.rs diff --git a/runtime/src/parachain.rs b/runtime/src/parachain.rs deleted file mode 100644 index eafc39e813..0000000000 --- a/runtime/src/parachain.rs +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright 2019-2020 PureStake Inc. -// This file is part of Moonbeam. - -// Moonbeam is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// Moonbeam is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with Moonbeam. If not, see . - -#[macro_export] -macro_rules! runtime_parachain { - () => { - /// This runtime version. - pub const VERSION: RuntimeVersion = RuntimeVersion { - spec_name: create_runtime_str!("moonbase-alphanet"), - impl_name: create_runtime_str!("moonbase-alphanet"), - authoring_version: 3, - spec_version: 11, - impl_version: 1, - apis: RUNTIME_API_VERSIONS, - transaction_version: 2, - }; - - impl cumulus_parachain_upgrade::Config for Runtime { - type Event = Event; - type OnValidationData = (); - type SelfParaId = ParachainInfo; - } - - impl parachain_info::Config for Runtime {} - - // TODO Consensus not supported in parachain - impl> FindAuthor for EthereumFindAuthor { - fn find_author<'a, I>(_digests: I) -> Option - where - I: 'a + IntoIterator, - { - None - } - } - - pub struct PhantomAura; - impl FindAuthor for PhantomAura { - fn find_author<'a, I>(_digests: I) -> Option - where - I: 'a + IntoIterator, - { - Some(0 as u32) - } - } - - construct_runtime! { - pub enum Runtime where - Block = Block, - NodeBlock = opaque::Block, - UncheckedExtrinsic = UncheckedExtrinsic - { - System: frame_system::{Module, Call, Storage, Config, Event}, - Timestamp: pallet_timestamp::{Module, Call, Storage, Inherent}, - Balances: pallet_balances::{Module, Call, Storage, Config, Event}, - Sudo: pallet_sudo::{Module, Call, Storage, Config, Event}, - RandomnessCollectiveFlip: pallet_randomness_collective_flip::{Module, Call, Storage}, - ParachainUpgrade: cumulus_parachain_upgrade::{Module, Call, Storage, Inherent, Event}, - TransactionPayment: pallet_transaction_payment::{Module, Storage}, - ParachainInfo: parachain_info::{Module, Storage, Config}, - EthereumChainId: pallet_ethereum_chain_id::{Module, Storage, Config}, - EVM: pallet_evm::{Module, Config, Call, Storage, Event}, - Ethereum: pallet_ethereum::{Module, Call, Storage, Event, Config, ValidateUnsigned}, - Stake: stake::{Module, Call, Storage, Event, Config}, - AuthorInherent: author_inherent::{Module, Call, Storage, Inherent, Event}, - } - } - }; -} diff --git a/runtime/src/standalone.rs b/runtime/src/standalone.rs deleted file mode 100644 index 298298cd66..0000000000 --- a/runtime/src/standalone.rs +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright 2019-2020 PureStake Inc. -// This file is part of Moonbeam. - -// Moonbeam is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// Moonbeam is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with Moonbeam. If not, see . - -pub use frame_support::traits::KeyOwnerProofSystem; -pub use pallet_grandpa::{ - fg_primitives, AuthorityId as GrandpaId, AuthorityList as GrandpaAuthorityList, -}; -pub use sp_consensus_aura::sr25519::AuthorityId as AuraId; -pub use sp_core::crypto::{KeyTypeId, Public}; -pub use sp_runtime::traits::NumberFor; - -#[macro_export] -macro_rules! runtime_standalone { - () => { - - /// This runtime version. - pub const VERSION: RuntimeVersion = RuntimeVersion { - spec_name: create_runtime_str!("moonbeam-standalone"), - impl_name: create_runtime_str!("moonbeam-standalone"), - authoring_version: 3, - spec_version: 11, - impl_version: 1, - apis: RUNTIME_API_VERSIONS, - transaction_version: 2, - }; - - impl pallet_aura::Config for Runtime { - type AuthorityId = AuraId; - } - - impl pallet_grandpa::Config for Runtime { - type Event = Event; - type Call = Call; - - type KeyOwnerProofSystem = (); - - type KeyOwnerProof = - >::Proof; - - type KeyOwnerIdentification = >::IdentificationTuple; - - type HandleEquivocation = (); - type WeightInfo = (); - } - - impl> FindAuthor for EthereumFindAuthor { - fn find_author<'a, I>(digests: I) -> Option - where - I: 'a + IntoIterator, - { - if let Some(author_index) = F::find_author(digests) { - let authority_id = Aura::authorities()[author_index as usize].clone(); - return Some(H160::from_slice(&authority_id.to_raw_vec()[4..24])); - } - None - } - } - - construct_runtime!( - pub enum Runtime where - Block = Block, - NodeBlock = opaque::Block, - UncheckedExtrinsic = UncheckedExtrinsic - { - System: frame_system::{Module, Call, Config, Storage, Event}, - RandomnessCollectiveFlip: pallet_randomness_collective_flip::{Module, Call, Storage}, - Timestamp: pallet_timestamp::{Module, Call, Storage, Inherent}, - Aura: pallet_aura::{Module, Config, Inherent}, - Grandpa: pallet_grandpa::{Module, Call, Storage, Config, Event}, - Balances: pallet_balances::{Module, Call, Storage, Config, Event}, - TransactionPayment: pallet_transaction_payment::{Module, Storage}, - Sudo: pallet_sudo::{Module, Call, Config, Storage, Event}, - EthereumChainId: pallet_ethereum_chain_id::{Module, Storage, Config}, - Ethereum: pallet_ethereum::{Module, Call, Storage, Event, Config, ValidateUnsigned}, - EVM: pallet_evm::{Module, Config, Call, Storage, Event}, - Stake: stake::{Module, Call, Storage, Event, Config}, - AuthorInherent: author_inherent::{Module, Call, Storage, Inherent, Event}, - } - ); - }; -} From 06f7fab72558cdd4aba26f71c583ce93b157d284 Mon Sep 17 00:00:00 2001 From: 4meta5 Date: Tue, 2 Feb 2021 21:52:48 -0800 Subject: [PATCH 23/23] tests cover all decl error variants --- pallets/stake/src/mock.rs | 2 +- pallets/stake/src/tests.rs | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/pallets/stake/src/mock.rs b/pallets/stake/src/mock.rs index 7dc738fef7..3b3b0ee2f6 100644 --- a/pallets/stake/src/mock.rs +++ b/pallets/stake/src/mock.rs @@ -98,7 +98,7 @@ parameter_types! { pub const BlocksPerRound: u32 = 5; pub const BondDuration: u32 = 2; pub const MaxValidators: u32 = 5; - pub const MaxNominatorsPerValidator: usize = 10; + pub const MaxNominatorsPerValidator: usize = 4; pub const MaxValidatorsPerNominator: usize = 4; pub const IssuancePerRound: u128 = 10; pub const MaxFee: Perbill = Perbill::from_percent(50); diff --git a/pallets/stake/src/tests.rs b/pallets/stake/src/tests.rs index c9803cb533..5249b05cc9 100644 --- a/pallets/stake/src/tests.rs +++ b/pallets/stake/src/tests.rs @@ -304,6 +304,10 @@ fn exit_queue_works() { last_event(), MetaEvent::stake(RawEvent::ValidatorScheduledExit(4, 4, 6)) ); + assert_noop!( + Stake::leave_candidates(Origin::signed(4)), + Error::::AlreadyLeaving + ); roll_to(21); let mut new_events = vec![ RawEvent::ValidatorScheduledExit(2, 6, 4), @@ -602,6 +606,10 @@ fn multiple_nominations() { message: Some("InsufficientBalance") }, ); + assert_noop!( + Stake::nominate_new(Origin::signed(10), 2, 10), + Error::::TooManyNominators + ); roll_to(26); let mut new2 = vec![ RawEvent::ValidatorChosen(5, 2, 50),