Introduce srml/scored-pool (#3381)

* Introduce srml/scored-pool

* Bump impl_version

* Apply suggestions from code review

Co-Authored-By: Bastian Köcher <bkchr@users.noreply.github.com>

* Remove unnecessary pub use

* Remove unnecessary import

* Adapt to InitializeMembers

* Bump impl_version

* Implement remarks (shortens code)

* Improve complexity of score()

Search and remove and search again for the
new spot and insert then => O(2LogN).

* Get rid of a clone()

* Reduce complexity of issue_candidacy()

* Add CandidateScored event + Improve comments

* Fix naming

* Use Lookup instead of AccountId as param

* Use set_members_sorted instead of computing diff

* Remove function which is only used during genesis

* Get rid of rev() by changing sort order of Pool

* Rename issue_candidacy to submit_candidacy

* Shorten code

* Remove find_in_pool() and have transactor submit index

* Remove unnecessary dependency

* Improve error messages

* Improve naming

* Improve comments

* Make code clearer wrt which receiver to invoke

* Adapt to new system trait

* Refactor to request CandidateDeposit only once

* Refactor to request Pool only once

* Improve structure and comments
This commit is contained in:
Michael Müller
2019-08-19 09:26:21 +02:00
committed by Gavin Wood
parent 0bb44f5024
commit 017752df41
8 changed files with 965 additions and 2 deletions
+15
View File
@@ -3995,6 +3995,21 @@ dependencies = [
"substrate-primitives 2.0.0",
]
[[package]]
name = "srml-scored-pool"
version = "1.0.0"
dependencies = [
"parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)",
"sr-io 2.0.0",
"sr-primitives 2.0.0",
"sr-std 2.0.0",
"srml-balances 2.0.0",
"srml-support 2.0.0",
"srml-system 2.0.0",
"substrate-primitives 2.0.0",
]
[[package]]
name = "srml-session"
version = "2.0.0"
+1
View File
@@ -86,6 +86,7 @@ members = [
"srml/membership",
"srml/metadata",
"srml/offences",
"srml/scored-pool",
"srml/session",
"srml/staking",
"srml/sudo",
+1 -1
View File
@@ -68,7 +68,7 @@ include!("../without_std.rs");
pub mod prelude {
pub use crate::vec::Vec;
pub use crate::boxed::Box;
pub use crate::cmp::{Eq, PartialEq};
pub use crate::cmp::{Eq, PartialEq, Reverse};
pub use crate::clone::Clone;
// Re-export `vec!` macro here, but not in `std` mode, since
+1 -1
View File
@@ -81,7 +81,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion {
// implementation changes and behavior does not, then leave spec_version as
// is and increment impl_version.
spec_version: 147,
impl_version: 147,
impl_version: 148,
apis: RUNTIME_API_VERSIONS,
};
+30
View File
@@ -0,0 +1,30 @@
[package]
name = "srml-scored-pool"
version = "1.0.0"
authors = ["Parity Technologies <admin@parity.io>"]
edition = "2018"
[dependencies]
codec = { package = "parity-scale-codec", version = "1.0.0", default-features = false, features = ["derive"] }
serde = { version = "1.0", optional = true }
sr-io = { path = "../../core/sr-io", default-features = false }
sr-primitives = { path = "../../core/sr-primitives", default-features = false }
sr-std = { path = "../../core/sr-std", default-features = false }
srml-support = { path = "../support", default-features = false }
system = { package = "srml-system", path = "../system", default-features = false }
[dev-dependencies]
balances = { package = "srml-balances", path = "../balances" }
primitives = { package = "substrate-primitives", path = "../../core/primitives" }
[features]
default = ["std"]
std = [
"codec/std",
"serde",
"sr-io/std",
"sr-primitives/std",
"sr-std/std",
"srml-support/std",
"system/std",
]
+457
View File
@@ -0,0 +1,457 @@
// Copyright 2019 Parity Technologies (UK) Ltd.
// This file is part of Substrate.
// Substrate 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.
// Substrate 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 Substrate. If not, see <http://www.gnu.org/licenses/>.
//! # Scored Pool Module
//!
//! The module maintains a scored membership pool. Each entity in the
//! pool can be attributed a `Score`. From this pool a set `Members`
//! is constructed. This set contains the `MemberCount` highest
//! scoring entities. Unscored entities are never part of `Members`.
//!
//! If an entity wants to be part of the pool a deposit is required.
//! The deposit is returned when the entity withdraws or when it
//! is removed by an entity with the appropriate authority.
//!
//! Every `Period` blocks the set of `Members` is refreshed from the
//! highest scoring members in the pool and, no matter if changes
//! occurred, `T::MembershipChanged::set_members_sorted` is invoked.
//! On first load `T::MembershipInitialized::initialize_members` is
//! invoked with the initial `Members` set.
//!
//! It is possible to withdraw candidacy/resign your membership at any
//! time. If an entity is currently a member, this results in removal
//! from the `Pool` and `Members`; the entity is immediately replaced
//! by the next highest scoring candidate in the pool, if available.
//!
//! - [`scored_pool::Trait`](./trait.Trait.html)
//! - [`Call`](./enum.Call.html)
//! - [`Module`](./struct.Module.html)
//!
//! ## Interface
//!
//! ### Public Functions
//!
//! - `submit_candidacy` - Submit candidacy to become a member. Requires a deposit.
//! - `withdraw_candidacy` - Withdraw candidacy. Deposit is returned.
//! - `score` - Attribute a quantitative score to an entity.
//! - `kick` - Remove an entity from the pool and members. Deposit is returned.
//! - `change_member_count` - Changes the amount of candidates taken into `Members`.
//!
//! ## Usage
//!
//! ```
//! use srml_support::{decl_module, dispatch::Result};
//! use system::ensure_signed;
//! use srml_scored_pool::{self as scored_pool};
//!
//! pub trait Trait: scored_pool::Trait {}
//!
//! decl_module! {
//! pub struct Module<T: Trait> for enum Call where origin: T::Origin {
//! pub fn candidate(origin) -> Result {
//! let who = ensure_signed(origin)?;
//!
//! let _ = <scored_pool::Module<T>>::submit_candidacy(
//! T::Origin::from(Some(who.clone()).into())
//! );
//! Ok(())
//! }
//! }
//! }
//!
//! # fn main() { }
//! ```
//!
//! ## Dependencies
//!
//! This module depends on the [System module](../srml_system/index.html).
// Ensure we're `no_std` when compiling for Wasm.
#![cfg_attr(not(feature = "std"), no_std)]
#[cfg(test)]
mod mock;
#[cfg(test)]
mod tests;
use codec::{Encode, Decode};
use sr_std::prelude::*;
use srml_support::{
StorageValue, StorageMap, decl_module, decl_storage, decl_event, ensure,
traits::{ChangeMembers, InitializeMembers, Currency, Get, ReservableCurrency},
};
use system::{self, ensure_root, ensure_signed};
use sr_primitives::{
traits::{EnsureOrigin, SimpleArithmetic, MaybeSerializeDebug, Zero, StaticLookup},
};
type BalanceOf<T, I> = <<T as Trait<I>>::Currency as Currency<<T as system::Trait>::AccountId>>::Balance;
type PoolT<T, I> = Vec<(<T as system::Trait>::AccountId, Option<<T as Trait<I>>::Score>)>;
/// The enum is supplied when refreshing the members set.
/// Depending on the enum variant the corresponding associated
/// type function will be invoked.
enum ChangeReceiver {
/// Should call `T::MembershipInitialized`.
MembershipInitialized,
/// Should call `T::MembershipChanged`.
MembershipChanged,
}
pub trait Trait<I=DefaultInstance>: system::Trait {
/// The currency used for deposits.
type Currency: Currency<Self::AccountId> + ReservableCurrency<Self::AccountId>;
/// The score attributed to a member or candidate.
type Score: SimpleArithmetic + Clone + Copy + Default + Encode + Decode + MaybeSerializeDebug;
/// The overarching event type.
type Event: From<Event<Self, I>> + Into<<Self as system::Trait>::Event>;
// The deposit which is reserved from candidates if they want to
// start a candidacy. The deposit gets returned when the candidacy is
// withdrawn or when the candidate is kicked.
type CandidateDeposit: Get<BalanceOf<Self, I>>;
/// Every `Period` blocks the `Members` are filled with the highest scoring
/// members in the `Pool`.
type Period: Get<Self::BlockNumber>;
/// The receiver of the signal for when the membership has been initialized.
/// This happens pre-genesis and will usually be the same as `MembershipChanged`.
/// If you need to do something different on initialization, then you can change
/// this accordingly.
type MembershipInitialized: InitializeMembers<Self::AccountId>;
/// The receiver of the signal for when the members have changed.
type MembershipChanged: ChangeMembers<Self::AccountId>;
/// Allows a configurable origin type to set a score to a candidate in the pool.
type ScoreOrigin: EnsureOrigin<Self::Origin>;
/// Required origin for removing a member (though can always be Root).
/// Configurable origin which enables removing an entity. If the entity
/// is part of the `Members` it is immediately replaced by the next
/// highest scoring candidate, if available.
type KickOrigin: EnsureOrigin<Self::Origin>;
}
decl_storage! {
trait Store for Module<T: Trait<I>, I: Instance=DefaultInstance> as ScoredPool {
/// The current pool of candidates, stored as an ordered Vec
/// (ordered descending by score, `None` last, highest first).
Pool get(pool) config(): PoolT<T, I>;
/// A Map of the candidates. The information in this Map is redundant
/// to the information in the `Pool`. But the Map enables us to easily
/// check if a candidate is already in the pool, without having to
/// iterate over the entire pool (the `Pool` is not sorted by
/// `T::AccountId`, but by `T::Score` instead).
CandidateExists get(candidate_exists): map T::AccountId => bool;
/// The current membership, stored as an ordered Vec.
Members get(members): Vec<T::AccountId>;
/// Size of the `Members` set.
MemberCount get(member_count) config(): u32;
}
add_extra_genesis {
config(members): Vec<T::AccountId>;
config(phantom): sr_std::marker::PhantomData<I>;
build(|
storage: &mut (sr_primitives::StorageOverlay, sr_primitives::ChildrenStorageOverlay),
config: &Self,
| {
sr_io::with_storage(storage, || {
let mut pool = config.pool.clone();
// reserve balance for each candidate in the pool.
// panicking here is ok, since this just happens one time, pre-genesis.
pool
.iter()
.for_each(|(who, _)| {
T::Currency::reserve(&who, T::CandidateDeposit::get())
.expect("balance too low to create candidacy");
<CandidateExists<T, I>>::insert(who, true);
});
/// Sorts the `Pool` by score in a descending order. Entities which
/// have a score of `None` are sorted to the beginning of the vec.
pool.sort_by_key(|(_, maybe_score)|
Reverse(maybe_score.unwrap_or_default())
);
<Pool<T, I>>::put(&pool);
<Module<T, I>>::refresh_members(pool, ChangeReceiver::MembershipInitialized);
});
})
}
}
decl_event!(
pub enum Event<T, I=DefaultInstance> where
<T as system::Trait>::AccountId,
{
/// The given member was removed. See the transaction for who.
MemberRemoved,
/// An entity has issued a candidacy. See the transaction for who.
CandidateAdded,
/// An entity withdrew candidacy. See the transaction for who.
CandidateWithdrew,
/// The candidacy was forcefully removed for an entity.
/// See the transaction for who.
CandidateKicked,
/// A score was attributed to the candidate.
/// See the transaction for who.
CandidateScored,
/// Phantom member, never used.
Dummy(sr_std::marker::PhantomData<(AccountId, I)>),
}
);
decl_module! {
pub struct Module<T: Trait<I>, I: Instance=DefaultInstance>
for enum Call
where origin: T::Origin
{
fn deposit_event<T, I>() = default;
/// Every `Period` blocks the `Members` set is refreshed from the
/// highest scoring members in the pool.
fn on_initialize(n: T::BlockNumber) {
if n % T::Period::get() == Zero::zero() {
let pool = <Pool<T, I>>::get();
<Module<T, I>>::refresh_members(pool, ChangeReceiver::MembershipChanged);
}
}
/// Add `origin` to the pool of candidates.
///
/// This results in `CandidateDeposit` being reserved from
/// the `origin` account. The deposit is returned once
/// candidacy is withdrawn by the candidate or the entity
/// is kicked by `KickOrigin`.
///
/// The dispatch origin of this function must be signed.
///
/// The `index` parameter of this function must be set to
/// the index of the transactor in the `Pool`.
pub fn submit_candidacy(origin) {
let who = ensure_signed(origin)?;
ensure!(!<CandidateExists<T, I>>::exists(&who), "already a member");
let deposit = T::CandidateDeposit::get();
T::Currency::reserve(&who, deposit)
.map_err(|_| "balance too low to submit candidacy")?;
// can be inserted as last element in pool, since entities with
// `None` are always sorted to the end.
if let Err(e) = <Pool<T, I>>::append(&[(who.clone(), None)]) {
T::Currency::unreserve(&who, deposit);
return Err(e);
}
<CandidateExists<T, I>>::insert(&who, true);
Self::deposit_event(RawEvent::CandidateAdded);
}
/// An entity withdraws candidacy and gets its deposit back.
///
/// If the entity is part of the `Members`, then the highest member
/// of the `Pool` that is not currently in `Members` is immediately
/// placed in the set instead.
///
/// The dispatch origin of this function must be signed.
///
/// The `index` parameter of this function must be set to
/// the index of the transactor in the `Pool`.
pub fn withdraw_candidacy(
origin,
index: u32
) {
let who = ensure_signed(origin)?;
let pool = <Pool<T, I>>::get();
Self::ensure_index(&pool, &who, index)?;
Self::remove_member(pool, who, index)?;
Self::deposit_event(RawEvent::CandidateWithdrew);
}
/// Kick a member `who` from the set.
///
/// May only be called from `KickOrigin` or root.
///
/// The `index` parameter of this function must be set to
/// the index of `dest` in the `Pool`.
pub fn kick(
origin,
dest: <T::Lookup as StaticLookup>::Source,
index: u32
) {
T::KickOrigin::try_origin(origin)
.map(|_| ())
.or_else(ensure_root)
.map_err(|_| "bad origin")?;
let who = T::Lookup::lookup(dest)?;
let pool = <Pool<T, I>>::get();
Self::ensure_index(&pool, &who, index)?;
Self::remove_member(pool, who, index)?;
Self::deposit_event(RawEvent::CandidateKicked);
}
/// Score a member `who` with `score`.
///
/// May only be called from `ScoreOrigin` or root.
///
/// The `index` parameter of this function must be set to
/// the index of the `dest` in the `Pool`.
pub fn score(
origin,
dest: <T::Lookup as StaticLookup>::Source,
index: u32,
score: T::Score
) {
T::ScoreOrigin::try_origin(origin)
.map(|_| ())
.or_else(ensure_root)
.map_err(|_| "bad origin")?;
let who = T::Lookup::lookup(dest)?;
let mut pool = <Pool<T, I>>::get();
Self::ensure_index(&pool, &who, index)?;
pool.remove(index as usize);
// we binary search the pool (which is sorted descending by score).
// if there is already an element with `score`, we insert
// right before that. if not, the search returns a location
// where we can insert while maintaining order.
let item = (who.clone(), Some(score.clone()));
let location = pool
.binary_search_by_key(
&Reverse(score),
|(_, maybe_score)| Reverse(maybe_score.unwrap_or_default())
)
.unwrap_or_else(|l| l);
pool.insert(location, item);
<Pool<T, I>>::put(&pool);
Self::deposit_event(RawEvent::CandidateScored);
}
/// Dispatchable call to change `MemberCount`.
///
/// This will only have an effect the next time a refresh happens
/// (this happens each `Period`).
///
/// May only be called from root.
pub fn change_member_count(origin, count: u32) {
ensure_root(origin)?;
<MemberCount<I>>::put(&count);
}
}
}
impl<T: Trait<I>, I: Instance> Module<T, I> {
/// Fetches the `MemberCount` highest scoring members from
/// `Pool` and puts them into `Members`.
///
/// The `notify` parameter is used to deduct which associated
/// type function to invoke at the end of the method.
fn refresh_members(
pool: PoolT<T, I>,
notify: ChangeReceiver
) {
let count = <MemberCount<I>>::get();
let mut new_members: Vec<T::AccountId> = pool
.into_iter()
.filter(|(_, score)| score.is_some())
.take(count as usize)
.map(|(account_id, _)| account_id)
.collect();
new_members.sort();
let old_members = <Members<T, I>>::get();
<Members<T, I>>::put(&new_members);
match notify {
ChangeReceiver::MembershipInitialized =>
T::MembershipInitialized::initialize_members(&new_members),
ChangeReceiver::MembershipChanged =>
T::MembershipChanged::set_members_sorted(
&new_members[..],
&old_members[..],
),
}
}
/// Removes an entity `remove` at `index` from the `Pool`.
///
/// If the entity is a member it is also removed from `Members` and
/// the deposit is returned.
fn remove_member(
mut pool: PoolT<T, I>,
remove: T::AccountId,
index: u32
) -> Result<(), &'static str> {
// all callers of this function in this module also check
// the index for validity before calling this function.
// nevertheless we check again here, to assert that there was
// no mistake when invoking this sensible function.
Self::ensure_index(&pool, &remove, index)?;
pool.remove(index as usize);
<Pool<T, I>>::put(&pool);
// remove from set, if it was in there
let members = <Members<T, I>>::get();
if members.binary_search(&remove).is_ok() {
Self::refresh_members(pool, ChangeReceiver::MembershipChanged);
}
<CandidateExists<T, I>>::remove(&remove);
T::Currency::unreserve(&remove, T::CandidateDeposit::get());
Self::deposit_event(RawEvent::MemberRemoved);
Ok(())
}
/// Checks if `index` is a valid number and if the element found
/// at `index` in `Pool` is equal to `who`.
fn ensure_index(
pool: &PoolT<T, I>,
who: &T::AccountId,
index: u32
) -> Result<(), &'static str> {
ensure!(index < pool.len() as u32, "index out of bounds");
let (index_who, _index_score) = &pool[index as usize];
ensure!(index_who == who, "index does not match requested account");
Ok(())
}
}
+177
View File
@@ -0,0 +1,177 @@
// Copyright 2019 Parity Technologies (UK) Ltd.
// This file is part of Substrate.
// Substrate 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.
// Substrate 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 Substrate. If not, see <http://www.gnu.org/licenses/>.
//! Test utilities
use super::*;
use std::cell::RefCell;
use srml_support::{impl_outer_origin, parameter_types};
use primitives::{H256, Blake2Hasher};
// The testing primitives are very useful for avoiding having to work with signatures
// or public keys. `u64` is used as the `AccountId` and no `Signature`s are requried.
use sr_primitives::{
Perbill, traits::{BlakeTwo256, IdentityLookup}, testing::Header,
};
use system::EnsureSignedBy;
impl_outer_origin! {
pub enum Origin for Test {}
}
// For testing the module, we construct most of a mock runtime. This means
// first constructing a configuration type (`Test`) which `impl`s each of the
// configuration traits of modules we want to use.
#[derive(Clone, Eq, PartialEq)]
pub struct Test;
parameter_types! {
pub const CandidateDeposit: u64 = 25;
pub const Period: u64 = 4;
pub const KickOrigin: u64 = 2;
pub const ScoreOrigin: u64 = 3;
pub const BlockHashCount: u64 = 250;
pub const MaximumBlockWeight: u32 = 1024;
pub const MaximumBlockLength: u32 = 2 * 1024;
pub const AvailableBlockRatio: Perbill = Perbill::one();
pub const ExistentialDeposit: u64 = 0;
pub const TransferFee: u64 = 0;
pub const CreationFee: u64 = 0;
pub const TransactionBaseFee: u64 = 0;
pub const TransactionByteFee: u64 = 0;
}
impl system::Trait for Test {
type Origin = Origin;
type Index = u64;
type BlockNumber = u64;
type Hash = H256;
type Call = ();
type Hashing = BlakeTwo256;
type AccountId = u64;
type Lookup = IdentityLookup<Self::AccountId>;
type Header = Header;
type WeightMultiplierUpdate = ();
type Event = ();
type BlockHashCount = BlockHashCount;
type MaximumBlockWeight = MaximumBlockWeight;
type MaximumBlockLength = MaximumBlockLength;
type AvailableBlockRatio = AvailableBlockRatio;
type Version = ();
}
impl balances::Trait for Test {
type Balance = u64;
type OnFreeBalanceZero = ();
type OnNewAccount = ();
type Event = ();
type TransactionPayment = ();
type TransferPayment = ();
type DustRemoval = ();
type ExistentialDeposit = ExistentialDeposit;
type TransferFee = TransferFee;
type CreationFee = CreationFee;
type TransactionBaseFee = TransactionBaseFee;
type TransactionByteFee = TransactionByteFee;
type WeightToFee = ();
}
thread_local! {
pub static MEMBERS: RefCell<Vec<u64>> = RefCell::new(vec![]);
}
pub struct TestChangeMembers;
impl ChangeMembers<u64> for TestChangeMembers {
fn change_members_sorted(incoming: &[u64], outgoing: &[u64], new: &[u64]) {
let mut old_plus_incoming = MEMBERS.with(|m| m.borrow().to_vec());
old_plus_incoming.extend_from_slice(incoming);
old_plus_incoming.sort();
let mut new_plus_outgoing = new.to_vec();
new_plus_outgoing.extend_from_slice(outgoing);
new_plus_outgoing.sort();
assert_eq!(old_plus_incoming, new_plus_outgoing);
MEMBERS.with(|m| *m.borrow_mut() = new.to_vec());
}
}
impl InitializeMembers<u64> for TestChangeMembers {
fn initialize_members(new_members: &[u64]) {
MEMBERS.with(|m| *m.borrow_mut() = new_members.to_vec());
}
}
impl Trait for Test {
type Event = ();
type KickOrigin = EnsureSignedBy<KickOrigin, u64>;
type MembershipInitialized = TestChangeMembers;
type MembershipChanged = TestChangeMembers;
type Currency = balances::Module<Self>;
type CandidateDeposit = CandidateDeposit;
type Period = Period;
type Score = u64;
type ScoreOrigin = EnsureSignedBy<ScoreOrigin, u64>;
}
// This function basically just builds a genesis storage key/value store according to
// our desired mockup.
pub fn new_test_ext() -> sr_io::TestExternalities<Blake2Hasher> {
let mut t = system::GenesisConfig::default().build_storage::<Test>().unwrap();
// We use default for brevity, but you can configure as desired if needed.
balances::GenesisConfig::<Test> {
balances: vec![
(5, 500_000),
(10, 500_000),
(15, 500_000),
(20, 500_000),
(31, 500_000),
(40, 500_000),
(99, 1),
],
vesting: vec![],
}.assimilate_storage(&mut t).unwrap();
GenesisConfig::<Test>{
pool: vec![
(5, None),
(10, Some(1)),
(20, Some(2)),
(31, Some(2)),
(40, Some(3)),
],
member_count: 2,
.. Default::default()
}.assimilate_storage(&mut t).unwrap();
t.into()
}
/// Fetch an entity from the pool, if existent.
pub fn fetch_from_pool(who: u64) -> Option<(u64, Option<u64>)> {
<Module<Test>>::pool()
.into_iter()
.find(|item| item.0 == who)
}
/// Find an entity in the pool.
/// Returns its position in the `Pool` vec, if existent.
pub fn find_in_pool(who: u64) -> Option<usize> {
<Module<Test>>::pool()
.into_iter()
.position(|item| item.0 == who)
}
+283
View File
@@ -0,0 +1,283 @@
// Copyright 2019 Parity Technologies (UK) Ltd.
// This file is part of Substrate.
// Substrate 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.
// Substrate 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 Substrate. If not, see <http://www.gnu.org/licenses/>.
//! Tests for the module.
use super::*;
use mock::*;
use srml_support::{assert_ok, assert_noop};
use sr_io::with_externalities;
use sr_primitives::traits::OnInitialize;
type ScoredPool = Module<Test>;
type System = system::Module<Test>;
type Balances = balances::Module<Test>;
const OOB_ERR: &str = "index out of bounds";
const INDEX_ERR: &str = "index does not match requested account";
#[test]
fn query_membership_works() {
with_externalities(&mut new_test_ext(), || {
assert_eq!(ScoredPool::members(), vec![20, 40]);
assert_eq!(Balances::reserved_balance(&31), CandidateDeposit::get());
assert_eq!(Balances::reserved_balance(&40), CandidateDeposit::get());
assert_eq!(MEMBERS.with(|m| m.borrow().clone()), vec![20, 40]);
});
}
#[test]
fn submit_candidacy_must_not_work() {
with_externalities(&mut new_test_ext(), || {
assert_noop!(
ScoredPool::submit_candidacy(Origin::signed(99)),
"balance too low to submit candidacy"
);
assert_noop!(
ScoredPool::submit_candidacy(Origin::signed(40)),
"already a member"
);
});
}
#[test]
fn submit_candidacy_works() {
with_externalities(&mut new_test_ext(), || {
// given
let who = 15;
// when
assert_ok!(ScoredPool::submit_candidacy(Origin::signed(who)));
assert_eq!(fetch_from_pool(15), Some((who, None)));
// then
assert_eq!(Balances::reserved_balance(&who), CandidateDeposit::get());
});
}
#[test]
fn scoring_works() {
with_externalities(&mut new_test_ext(), || {
// given
let who = 15;
let score = 99;
assert_ok!(ScoredPool::submit_candidacy(Origin::signed(who)));
// when
let index = find_in_pool(who).expect("entity must be in pool") as u32;
assert_ok!(ScoredPool::score(Origin::signed(ScoreOrigin::get()), who, index, score));
// then
assert_eq!(fetch_from_pool(who), Some((who, Some(score))));
assert_eq!(find_in_pool(who), Some(0)); // must be first element, since highest scored
});
}
#[test]
fn scoring_same_element_with_same_score_works() {
with_externalities(&mut new_test_ext(), || {
// given
let who = 31;
let index = find_in_pool(who).expect("entity must be in pool") as u32;
let score = 2;
// when
assert_ok!(ScoredPool::score(Origin::signed(ScoreOrigin::get()), who, index, score));
// then
assert_eq!(fetch_from_pool(who), Some((who, Some(score))));
// must have been inserted right before the `20` element which is
// of the same score as `31`. so sort order is maintained.
assert_eq!(find_in_pool(who), Some(1));
});
}
#[test]
fn kicking_works_only_for_authorized() {
with_externalities(&mut new_test_ext(), || {
let who = 40;
let index = find_in_pool(who).expect("entity must be in pool") as u32;
assert_noop!(ScoredPool::kick(Origin::signed(99), who, index), "bad origin");
});
}
#[test]
fn kicking_works() {
with_externalities(&mut new_test_ext(), || {
// given
let who = 40;
assert_eq!(Balances::reserved_balance(&who), CandidateDeposit::get());
assert_eq!(find_in_pool(who), Some(0));
// when
let index = find_in_pool(who).expect("entity must be in pool") as u32;
assert_ok!(ScoredPool::kick(Origin::signed(KickOrigin::get()), who, index));
// then
assert_eq!(find_in_pool(who), None);
assert_eq!(ScoredPool::members(), vec![20, 31]);
assert_eq!(MEMBERS.with(|m| m.borrow().clone()), ScoredPool::members());
assert_eq!(Balances::reserved_balance(&who), 0); // deposit must have been returned
});
}
#[test]
fn unscored_entities_must_not_be_used_for_filling_members() {
with_externalities(&mut new_test_ext(), || {
// given
// we submit a candidacy, score will be `None`
assert_ok!(ScoredPool::submit_candidacy(Origin::signed(15)));
// when
// we remove every scored member
ScoredPool::pool()
.into_iter()
.for_each(|(who, score)| {
if let Some(_) = score {
let index = find_in_pool(who).expect("entity must be in pool") as u32;
assert_ok!(ScoredPool::kick(Origin::signed(KickOrigin::get()), who, index));
}
});
// then
// the `None` candidates should not have been filled in
assert_eq!(ScoredPool::members(), vec![]);
assert_eq!(MEMBERS.with(|m| m.borrow().clone()), ScoredPool::members());
});
}
#[test]
fn refreshing_works() {
with_externalities(&mut new_test_ext(), || {
// given
let who = 15;
assert_ok!(ScoredPool::submit_candidacy(Origin::signed(who)));
let index = find_in_pool(who).expect("entity must be in pool") as u32;
assert_ok!(ScoredPool::score(Origin::signed(ScoreOrigin::get()), who, index, 99));
// when
ScoredPool::refresh_members(ScoredPool::pool(), ChangeReceiver::MembershipChanged);
// then
assert_eq!(ScoredPool::members(), vec![15, 40]);
assert_eq!(MEMBERS.with(|m| m.borrow().clone()), ScoredPool::members());
});
}
#[test]
fn refreshing_happens_every_period() {
with_externalities(&mut new_test_ext(), || {
// given
System::set_block_number(1);
assert_ok!(ScoredPool::submit_candidacy(Origin::signed(15)));
let index = find_in_pool(15).expect("entity must be in pool") as u32;
assert_ok!(ScoredPool::score(Origin::signed(ScoreOrigin::get()), 15, index, 99));
assert_eq!(ScoredPool::members(), vec![20, 40]);
// when
System::set_block_number(4);
ScoredPool::on_initialize(4);
// then
assert_eq!(ScoredPool::members(), vec![15, 40]);
assert_eq!(MEMBERS.with(|m| m.borrow().clone()), ScoredPool::members());
});
}
#[test]
fn withdraw_candidacy_must_only_work_for_members() {
with_externalities(&mut new_test_ext(), || {
let who = 77;
let index = 0;
assert_noop!( ScoredPool::withdraw_candidacy(Origin::signed(who), index), INDEX_ERR);
});
}
#[test]
fn oob_index_should_abort() {
with_externalities(&mut new_test_ext(), || {
let who = 40;
let oob_index = ScoredPool::pool().len() as u32;
assert_noop!(ScoredPool::withdraw_candidacy(Origin::signed(who), oob_index), OOB_ERR);
assert_noop!(ScoredPool::score(Origin::signed(ScoreOrigin::get()), who, oob_index, 99), OOB_ERR);
assert_noop!(ScoredPool::kick(Origin::signed(KickOrigin::get()), who, oob_index), OOB_ERR);
});
}
#[test]
fn index_mismatches_should_abort() {
with_externalities(&mut new_test_ext(), || {
let who = 40;
let index = 3;
assert_noop!(ScoredPool::withdraw_candidacy(Origin::signed(who), index), INDEX_ERR);
assert_noop!(ScoredPool::score(Origin::signed(ScoreOrigin::get()), who, index, 99), INDEX_ERR);
assert_noop!(ScoredPool::kick(Origin::signed(KickOrigin::get()), who, index), INDEX_ERR);
});
}
#[test]
fn withdraw_unscored_candidacy_must_work() {
with_externalities(&mut new_test_ext(), || {
// given
let who = 5;
// when
let index = find_in_pool(who).expect("entity must be in pool") as u32;
assert_ok!(ScoredPool::withdraw_candidacy(Origin::signed(who), index));
// then
assert_eq!(fetch_from_pool(5), None);
});
}
#[test]
fn withdraw_scored_candidacy_must_work() {
with_externalities(&mut new_test_ext(), || {
// given
let who = 40;
assert_eq!(Balances::reserved_balance(&who), CandidateDeposit::get());
// when
let index = find_in_pool(who).expect("entity must be in pool") as u32;
assert_ok!(ScoredPool::withdraw_candidacy(Origin::signed(who), index));
// then
assert_eq!(fetch_from_pool(who), None);
assert_eq!(ScoredPool::members(), vec![20, 31]);
assert_eq!(Balances::reserved_balance(&who), 0);
});
}
#[test]
fn candidacy_resubmitting_works() {
with_externalities(&mut new_test_ext(), || {
// given
let who = 15;
// when
assert_ok!(ScoredPool::submit_candidacy(Origin::signed(who)));
assert_eq!(ScoredPool::candidate_exists(who), true);
let index = find_in_pool(who).expect("entity must be in pool") as u32;
assert_ok!(ScoredPool::withdraw_candidacy(Origin::signed(who), index));
assert_eq!(ScoredPool::candidate_exists(who), false);
assert_ok!(ScoredPool::submit_candidacy(Origin::signed(who)));
// then
assert_eq!(ScoredPool::candidate_exists(who), true);
});
}