More tests for council voting.

Also allow AsRef to be used for Public keys to simplify test code.
This commit is contained in:
Gav
2018-03-05 12:11:56 +01:00
parent 8d84ca8b48
commit 7d378d3de3
11 changed files with 273 additions and 66 deletions
+16 -6
View File
@@ -44,6 +44,8 @@ enum InternalFunctionId {
StakingSetValidatorCount = 0x22,
/// Force a new staking era.
StakingForceNewEra = 0x23,
/// See below.
DemocracyCancelReferendum = 0x30,
}
impl InternalFunctionId {
@@ -57,6 +59,7 @@ impl InternalFunctionId {
InternalFunctionId::StakingSetBondingDuration,
InternalFunctionId::StakingSetValidatorCount,
InternalFunctionId::StakingForceNewEra,
InternalFunctionId::DemocracyCancelReferendum,
];
functions.iter().map(|&f| f).find(|&f| value == f as u8)
}
@@ -80,24 +83,27 @@ pub enum Proposal {
StakingSetValidatorCount(u32),
/// Force a new staking era.
StakingForceNewEra,
/// Cancel a referendum.
DemocracyCancelReferendum(u32),
}
impl Slicable for Proposal {
fn decode<I: Input>(input: &mut I) -> Option<Self> {
let id = try_opt!(u8::decode(input).and_then(InternalFunctionId::from_u8));
let id = u8::decode(input).and_then(InternalFunctionId::from_u8)?;
let function = match id {
InternalFunctionId::SystemSetCode =>
Proposal::SystemSetCode(try_opt!(Slicable::decode(input))),
Proposal::SystemSetCode(Slicable::decode(input)?),
InternalFunctionId::SessionSetLength =>
Proposal::SessionSetLength(try_opt!(Slicable::decode(input))),
Proposal::SessionSetLength(Slicable::decode(input)?),
InternalFunctionId::SessionForceNewSession => Proposal::SessionForceNewSession,
InternalFunctionId::StakingSetSessionsPerEra =>
Proposal::StakingSetSessionsPerEra(try_opt!(Slicable::decode(input))),
Proposal::StakingSetSessionsPerEra(Slicable::decode(input)?),
InternalFunctionId::StakingSetBondingDuration =>
Proposal::StakingSetBondingDuration(try_opt!(Slicable::decode(input))),
Proposal::StakingSetBondingDuration(Slicable::decode(input)?),
InternalFunctionId::StakingSetValidatorCount =>
Proposal::StakingSetValidatorCount(try_opt!(Slicable::decode(input))),
Proposal::StakingSetValidatorCount(Slicable::decode(input)?),
InternalFunctionId::StakingForceNewEra => Proposal::StakingForceNewEra,
InternalFunctionId::DemocracyCancelReferendum => Proposal::DemocracyCancelReferendum(Slicable::decode(input)?),
};
Some(function)
@@ -132,6 +138,10 @@ impl Slicable for Proposal {
Proposal::StakingForceNewEra => {
(InternalFunctionId::StakingForceNewEra as u8).using_encoded(|s| v.extend(s));
}
Proposal::DemocracyCancelReferendum(ref data) => {
(InternalFunctionId::DemocracyCancelReferendum as u8).using_encoded(|s| v.extend(s));
data.using_encoded(|s| v.extend(s));
}
}
v
+4 -1
View File
@@ -17,7 +17,7 @@
//! Democratic system: Handles administration of general stakeholder voting.
use demo_primitives::Proposal;
use runtime::{staking, system, session};
use runtime::{staking, system, session, democracy};
pub fn enact_proposal(proposal: Proposal) {
match proposal {
@@ -42,5 +42,8 @@ pub fn enact_proposal(proposal: Proposal) {
Proposal::StakingForceNewEra => {
staking::privileged::force_new_era()
}
Proposal::DemocracyCancelReferendum(ref_index) => {
democracy::privileged::clear_referendum(ref_index)
}
}
}
@@ -91,8 +91,8 @@ pub const TERM_DURATION: &[u8] = b"cou:trm";
pub const DESIRED_SEATS: &[u8] = b"cou:sts";
// permanent state (always relevant, changes only at the finalisation of voting)
pub const ACTIVE_COUNCIL: &[u8] = b"cou:act";
pub const VOTE_COUNT: &[u8] = b"cou:vco";
pub const ACTIVE_COUNCIL: &[u8] = b"cou:act"; // Vec<(AccountId, expiry: BlockNumber)>
pub const VOTE_COUNT: &[u8] = b"cou:vco"; // VoteIndex
// persistent state (always relevant, changes constantly)
pub const APPROVALS_OF: &[u8] = b"cou:apr:"; // Vec<bool>
@@ -51,27 +51,27 @@ pub fn was_vetoed(proposal: &ProposalHash) -> bool {
storage::exists(&proposal.to_keyed_vec(VETOED_PROPOSAL))
}
pub fn will_still_be_councillor_at(who: &AccountId, n: BlockNumber) -> bool {
pub fn will_still_be_councillor_at<P: AsRef<AccountId>>(who: P, n: BlockNumber) -> bool {
council::active_council().iter()
.find(|&&(ref a, _)| a == who)
.find(|&&(ref a, _)| a == who.as_ref())
.map(|&(_, expires)| expires > n)
.unwrap_or(false)
}
pub fn vote_of(who: &AccountId, proposal: &ProposalHash) -> Option<bool> {
storage::get(&(*who, *proposal).to_keyed_vec(COUNCIL_VOTE_OF))
pub fn vote_of<P: AsRef<AccountId>>(who: P, proposal: &ProposalHash) -> Option<bool> {
storage::get(&(*proposal, *who.as_ref()).to_keyed_vec(COUNCIL_VOTE_OF))
}
pub fn take_vote_of(who: &AccountId, proposal: &ProposalHash) -> Option<bool> {
storage::get(&(*who, *proposal).to_keyed_vec(COUNCIL_VOTE_OF))
pub fn proposal_voters(proposal: &ProposalHash) -> Vec<AccountId> {
storage::get_or_default(&proposal.to_keyed_vec(PROPOSAL_VOTERS))
}
pub fn tally(proposal_hash: &ProposalHash) -> (u32, u32, u32) {
generic_tally(proposal_hash, vote_of)
generic_tally(proposal_hash, |w: &AccountId, p: &ProposalHash| storage::get(&(*p, *w).to_keyed_vec(COUNCIL_VOTE_OF)))
}
fn take_tally(proposal_hash: &ProposalHash) -> (u32, u32, u32) {
generic_tally(proposal_hash, take_vote_of)
generic_tally(proposal_hash, |w: &AccountId, p: &ProposalHash| storage::get(&(*p, *w).to_keyed_vec(COUNCIL_VOTE_OF)))
}
fn generic_tally<F: Fn(&AccountId, &ProposalHash) -> Option<bool>>(proposal_hash: &ProposalHash, vote_of: F) -> (u32, u32, u32) {
@@ -103,7 +103,7 @@ fn take_proposal_if_expiring_at(n: BlockNumber) -> Option<(Proposal, ProposalHas
pub mod public {
use super::*;
pub fn propose(signed: &AccountId, proposal: &Proposal) {
pub fn propose<P: AsRef<AccountId> + Copy>(signed: P, proposal: &Proposal) {
let expiry = system::block_number() + voting_period();
assert!(will_still_be_councillor_at(signed, expiry));
@@ -116,19 +116,24 @@ pub mod public {
set_proposals(&proposals);
storage::put(&proposal_hash.to_keyed_vec(PROPOSAL_OF), proposal);
storage::put(&proposal_hash.to_keyed_vec(PROPOSAL_VOTERS), &vec![*signed]);
storage::put(&(proposal_hash, *signed).to_keyed_vec(COUNCIL_VOTE_OF), &true);
storage::put(&proposal_hash.to_keyed_vec(PROPOSAL_VOTERS), &vec![*signed.as_ref()]);
storage::put(&(proposal_hash, *(signed.as_ref())).to_keyed_vec(COUNCIL_VOTE_OF), &true);
}
pub fn vote(signed: AccountId, proposal: &ProposalHash, approve: bool) {
pub fn vote<P: AsRef<AccountId> + Copy>(signed: P, proposal: &ProposalHash, approve: bool) {
if vote_of(signed, proposal).is_none() {
let mut voters = proposal_voters(proposal);
voters.push(*signed.as_ref());
storage::put(&proposal.to_keyed_vec(PROPOSAL_VOTERS), &voters);
}
storage::put(&(*proposal, *(signed.as_ref())).to_keyed_vec(COUNCIL_VOTE_OF), &approve);
}
pub fn veto<P: AsRef<AccountId> + Copy>(signed: P, proposal: &ProposalHash) {
}
pub fn veto(signed: AccountId, proposal: &ProposalHash) {
}
pub fn repropose(signed: AccountId, proposal: &Proposal) {
pub fn repropose<P: AsRef<AccountId> + Copy>(signed: P, proposal: &Proposal) {
}
}
@@ -153,17 +158,147 @@ pub mod internal {
pub fn end_block(now: BlockNumber) {
while let Some((proposal, proposal_hash)) = take_proposal_if_expiring_at(now) {
let tally = take_tally(&proposal_hash);
let vote_threshold = match tally.0 {
x if x == tally.2 => VoteThreshold::SuperMajorityAgainst,
x if x > tally.2 / 2 => VoteThreshold::SimpleMajority,
_ => VoteThreshold::SuperMajorityApprove,
};
start_referendum(proposal, vote_threshold);
if let Proposal::DemocracyCancelReferendum(ref_index) = proposal {
if tally.0 == tally.2 {
democracy::privileged::clear_referendum(ref_index);
}
} else {
match tally {
(_, 0, 0) => start_referendum(proposal, VoteThreshold::SuperMajorityAgainst),
(y, n, x) if y > n + x => start_referendum(proposal, VoteThreshold::SimpleMajority),
_ => {},
};
}
}
}
}
#[cfg(test)]
mod tests {
pub mod testing {
use super::*;
use runtime_io::{twox_128, TestExternalities};
use keyring::Keyring::{Alice, Bob, Charlie};
use codec::Joiner;
use runtime::council;
pub fn externalities() -> TestExternalities {
let expiry: BlockNumber = 10;
let extras: TestExternalities = map![
twox_128(council::ACTIVE_COUNCIL).to_vec() => vec![].and(&vec![
(Alice.to_raw_public(), expiry),
(Bob.into(), expiry),
(Charlie.into(), expiry)
]),
twox_128(COOLOFF_PERIOD).to_vec() => vec![].and(&2u64),
twox_128(VOTING_PERIOD).to_vec() => vec![].and(&1u64)
];
council::testing::externalities()
.into_iter().chain(extras.into_iter()).collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use runtime_io::{with_externalities, twox_128, TestExternalities};
use codec::{KeyedVec, Joiner};
use keyring::Keyring::{Alice, Bob, Charlie, Dave};
use environment::with_env;
use demo_primitives::{AccountId, Proposal};
use runtime::{staking, council, democracy};
use runtime::democracy::VoteThreshold;
fn new_test_ext() -> TestExternalities {
testing::externalities()
}
#[test]
fn basic_environment_works() {
with_externalities(&mut new_test_ext(), || {
with_env(|e| e.block_number = 1);
assert_eq!(staking::bonding_duration(), 0);
assert_eq!(cooloff_period(), 2);
assert_eq!(voting_period(), 1);
assert_eq!(will_still_be_councillor_at(Alice, 1), true);
assert_eq!(will_still_be_councillor_at(Alice, 10), false);
assert_eq!(will_still_be_councillor_at(Dave, 10), false);
assert_eq!(proposals(), Vec::<(BlockNumber, ProposalHash)>::new());
assert_eq!(proposal_voters(&ProposalHash::default()), Vec::<AccountId>::new());
assert_eq!(was_vetoed(&ProposalHash::default()), false);
assert_eq!(vote_of(Alice, &ProposalHash::default()), None);
assert_eq!(tally(&ProposalHash::default()), (0, 0, 3));
});
}
#[test]
fn simple_propose_should_work() {
with_externalities(&mut new_test_ext(), || {
with_env(|e| e.block_number = 1);
let proposal = Proposal::StakingSetBondingDuration(42);
let hash = proposal.blake2_256();
public::propose(Alice, &proposal);
assert_eq!(proposals().len(), 1);
assert_eq!(proposal_voters(&hash), vec![Alice.to_raw_public()]);
assert_eq!(vote_of(Alice, &hash), Some(true));
assert_eq!(tally(&hash), (1, 0, 2));
});
}
#[test]
fn unvoted_proposal_should_expire_without_action() {
with_externalities(&mut new_test_ext(), || {
with_env(|e| e.block_number = 1);
public::propose(Alice, &Proposal::StakingSetBondingDuration(42));
assert_eq!(tally(&Proposal::StakingSetBondingDuration(42).blake2_256()), (1, 0, 2));
internal::end_block(1);
with_env(|e| e.block_number = 2);
internal::end_block(2);
assert_eq!(proposals().len(), 0);
assert_eq!(democracy::active_referendums().len(), 0);
});
}
#[test]
fn unanimous_proposal_should_expire_with_biased_referendum() {
with_externalities(&mut new_test_ext(), || {
with_env(|e| e.block_number = 1);
public::propose(Alice, &Proposal::StakingSetBondingDuration(42));
public::vote(Bob, &Proposal::StakingSetBondingDuration(42).blake2_256(), true);
public::vote(Charlie, &Proposal::StakingSetBondingDuration(42).blake2_256(), true);
assert_eq!(tally(&Proposal::StakingSetBondingDuration(42).blake2_256()), (3, 0, 0));
internal::end_block(1);
with_env(|e| e.block_number = 2);
internal::end_block(2);
assert_eq!(proposals().len(), 0);
assert_eq!(democracy::active_referendums(), vec![(0, 3, Proposal::StakingSetBondingDuration(42), VoteThreshold::SuperMajorityAgainst)]);
});
}
#[test]
fn majority_proposal_should_expire_with_unbiased_referendum() {
with_externalities(&mut new_test_ext(), || {
with_env(|e| e.block_number = 1);
public::propose(Alice, &Proposal::StakingSetBondingDuration(42));
public::vote(Bob, &Proposal::StakingSetBondingDuration(42).blake2_256(), true);
public::vote(Charlie, &Proposal::StakingSetBondingDuration(42).blake2_256(), false);
assert_eq!(tally(&Proposal::StakingSetBondingDuration(42).blake2_256()), (2, 1, 0));
internal::end_block(1);
with_env(|e| e.block_number = 2);
internal::end_block(2);
assert_eq!(proposals().len(), 0);
assert_eq!(democracy::active_referendums(), vec![(0, 3, Proposal::StakingSetBondingDuration(42), VoteThreshold::SimpleMajority)]);
});
}
#[test]
#[should_panic]
fn propose_by_public_should_panic() {
with_externalities(&mut new_test_ext(), || {
with_env(|e| e.block_number = 1);
public::propose(Dave, &Proposal::StakingSetBondingDuration(42));
});
}
}
@@ -27,6 +27,8 @@ use runtime::staking::Balance;
pub type PropIndex = u32;
pub type ReferendumIndex = u32;
#[cfg_attr(test, derive(Debug))]
#[derive(Clone, Copy, PartialEq)]
pub enum VoteThreshold {
SuperMajorityApprove,
SuperMajorityAgainst,
@@ -138,6 +140,15 @@ pub fn referendum_info(ref_index: ReferendumIndex) -> Option<(BlockNumber, Propo
storage::get(&ref_index.to_keyed_vec(REFERENDUM_INFO_OF))
}
/// Get all referendums currently active.
pub fn active_referendums() -> Vec<(ReferendumIndex, BlockNumber, Proposal, VoteThreshold)> {
let next: ReferendumIndex = storage::get_or_default(NEXT_TALLY);
let last: ReferendumIndex = storage::get_or_default(REFERENDUM_COUNT);
(next..last).into_iter()
.filter_map(|i| referendum_info(i).map(|(n, p, t)| (i, n, p, t)))
.collect()
}
/// Get all referendums ready for tally at block `n`.
pub fn maturing_referendums_at(n: BlockNumber) -> Vec<(ReferendumIndex, BlockNumber, Proposal, VoteThreshold)> {
let next: ReferendumIndex = storage::get_or_default(NEXT_TALLY);