From 2ffcf8ff26028689425e35236004bec22734ae0b Mon Sep 17 00:00:00 2001 From: Gav Date: Sun, 25 Feb 2018 18:37:07 +0100 Subject: [PATCH] Initial draft of stakeholder democracy. --- substrate/Cargo.lock | 7 + substrate/demo/runtime/Cargo.toml | 1 + substrate/demo/runtime/src/dispatch.rs | 49 +++++ substrate/demo/runtime/src/lib.rs | 3 + .../demo/runtime/src/runtime/democracy.rs | 185 ++++++++++++++++++ .../demo/runtime/src/runtime/governance.rs | 31 +-- substrate/demo/runtime/src/runtime/mod.rs | 3 + substrate/demo/runtime/src/runtime/staking.rs | 7 + substrate/demo/runtime/wasm/Cargo.toml | 1 + .../polkadot/runtime/src/runtime/staking.rs | 2 + 10 files changed, 259 insertions(+), 30 deletions(-) create mode 100644 substrate/demo/runtime/src/dispatch.rs create mode 100644 substrate/demo/runtime/src/runtime/democracy.rs diff --git a/substrate/Cargo.lock b/substrate/Cargo.lock index 2af889e5c9..4f4a414d7a 100644 --- a/substrate/Cargo.lock +++ b/substrate/Cargo.lock @@ -241,6 +241,7 @@ version = "0.1.0" dependencies = [ "demo-primitives 0.1.0", "hex-literal 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "integer-sqrt 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", "rustc-hex 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "substrate-codec 0.1.0", @@ -653,6 +654,11 @@ dependencies = [ "xmltree 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "integer-sqrt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "iovec" version = "0.1.2" @@ -2066,6 +2072,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum hyper 0.11.17 (registry+https://github.com/rust-lang/crates.io-index)" = "7f4de6edd503089841ebfa88341e1c00fb19b6bf93d820d908db15960fd31226" "checksum idna 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "014b298351066f1512874135335d62a789ffe78a9974f94b43ed5621951eaf7d" "checksum igd 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "356a0dc23a4fa0f8ce4777258085d00a01ea4923b2efd93538fc44bf5e1bda76" +"checksum integer-sqrt 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8833702c315192502093b244e29c6ab9c55454adfe21b879a87a039ea8fe8520" "checksum iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "dbe6e417e7d0975db6512b90796e8ce223145ac4e33c377e4a42882a0e88bb08" "checksum ipnetwork 0.12.7 (registry+https://github.com/rust-lang/crates.io-index)" = "2134e210e2a024b5684f90e1556d5f71a1ce7f8b12e9ac9924c67fb36f63b336" "checksum isatty 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "8f2a233726c7bb76995cec749d59582e5664823b7245d4970354408f1d79a7a2" diff --git a/substrate/demo/runtime/Cargo.toml b/substrate/demo/runtime/Cargo.toml index 8c175b818c..f98345cae6 100644 --- a/substrate/demo/runtime/Cargo.toml +++ b/substrate/demo/runtime/Cargo.toml @@ -13,6 +13,7 @@ substrate-runtime-io = { path = "../../substrate/runtime-io" } substrate-runtime-support = { path = "../../substrate/runtime-support" } substrate-primitives = { path = "../../substrate/primitives" } demo-primitives = { path = "../primitives" } +integer-sqrt = "0.1.0" [dev-dependencies] substrate-keyring = { path = "../../substrate/keyring" } diff --git a/substrate/demo/runtime/src/dispatch.rs b/substrate/demo/runtime/src/dispatch.rs new file mode 100644 index 0000000000..6b03667ff9 --- /dev/null +++ b/substrate/demo/runtime/src/dispatch.rs @@ -0,0 +1,49 @@ +// Copyright 2017 Parity Technologies (UK) Ltd. +// This file is part of Substrate Demo. + +// Substrate Demo 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 Demo 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 Demo. If not, see . + +//! Democratic system: Handles administration of general stakeholder voting. + +use demo_primitives::Proposal; +use runtime::{staking, system, session, governance}; + +pub fn enact_proposal(proposal: Proposal) { + match proposal { + Proposal::SystemSetCode(code) => { + system::privileged::set_code(&code); + } + Proposal::SessionSetLength(value) => { + session::privileged::set_length(value); + } + Proposal::SessionForceNewSession => { + session::privileged::force_new_session(); + } + Proposal::StakingSetSessionsPerEra(value) => { + staking::privileged::set_sessions_per_era(value); + } + Proposal::StakingSetBondingDuration(value) => { + staking::privileged::set_bonding_duration(value); + } + Proposal::StakingSetValidatorCount(value) => { + staking::privileged::set_validator_count(value); + } + Proposal::StakingForceNewEra => { + staking::privileged::force_new_era() + } + Proposal::GovernanceSetApprovalPpmRequired(value) => { + governance::privileged::set_approval_ppm_required(value); + } + } +} diff --git a/substrate/demo/runtime/src/lib.rs b/substrate/demo/runtime/src/lib.rs index 4bb5dc4169..f42d16016a 100644 --- a/substrate/demo/runtime/src/lib.rs +++ b/substrate/demo/runtime/src/lib.rs @@ -31,9 +31,12 @@ extern crate demo_primitives; #[cfg(test)] #[macro_use] extern crate hex_literal; +extern crate integer_sqrt; + pub mod environment; pub mod runtime; pub mod api; +pub mod dispatch; #[cfg(feature = "std")] pub mod genesismap; diff --git a/substrate/demo/runtime/src/runtime/democracy.rs b/substrate/demo/runtime/src/runtime/democracy.rs new file mode 100644 index 0000000000..cad53ead18 --- /dev/null +++ b/substrate/demo/runtime/src/runtime/democracy.rs @@ -0,0 +1,185 @@ +// Copyright 2017 Parity Technologies (UK) Ltd. +// This file is part of Substrate Demo. + +// Substrate Demo 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 Demo 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 Demo. If not, see . + +//! Democratic system: Handles administration of general stakeholder voting. + +use rstd::prelude::*; +use integer_sqrt::IntegerSquareRoot; +use codec::KeyedVec; +use runtime_support::storage; +use demo_primitives::{Proposal, AccountId, Hash, BlockNumber}; +use runtime::{staking, system, session}; + +const CURRENT_PROPOSAL: &[u8] = b"gov:pro"; +const VOTE_OF: &[u8] = b"gov:vot:"; +const VOTERS: &[u8] = b"gov:vtr:"; + +pub mod public { + use super::*; + + /// Get the voters for the current proposal. + pub fn voters() -> Vec { + storage::get_or_default(VOTERS) + } + + /// Get the vote, if Some, of `who`. + pub fn vote_of(who: &AccountId) -> Option { + storage::get(&who.to_keyed_vec(VOTE_OF)) + } + + /// Get the voters for the current proposal. + pub fn tally() -> (staking::Balance, staking::Balance) { + voters().iter() + .map(|a| (staking::balance(a), vote_of(a).expect("all items come from `voters`; for an item to be in `voters` there must be a vote registered; qed"))) + .map(|(bal, vote)| if vote { (bal, 0) } else { (0, bal) }) + .fold((0, 0), |(a, b), (c, d)| (a + c, b + d)) + } + + /// Propose a sensitive action to be taken. Any action that is enactable by `Proposal` is valid. + /// Proposal is by the `transactor` and will automatically count as an approval. Transactor must + /// be a current validator. It is illegal to propose when there is already a proposal in effect. + pub fn propose(validator: &AccountId, proposal: &Proposal) { + if storage::exists(CURRENT_PROPOSAL) { + panic!("there may only be one proposal per era."); + } + storage::put(CURRENT_PROPOSAL, proposal); + } + + /// Approve the current era's proposal. Transactor must be a validator. This may not be done more + /// than once for any validator in an era. + pub fn vote(who: &AccountId, era_index: BlockNumber, way: bool) { + if era_index != staking::current_era() { + panic!("approval vote applied on non-current era.") + } + if !storage::exists(CURRENT_PROPOSAL) { + panic!("there must be a proposal in order to approve."); + } + if staking::balance(who) == 0 { + panic!("transactor must have balance to signal approval."); + } + let key = who.to_keyed_vec(VOTE_OF); + if !storage::exists(&key) { + let mut voters = voters(); + voters.push(who.clone()); + storage::put(VOTERS, &voters); + } + storage::put(&key, &way); + } +} + +pub mod privileged { + use super::*; + + pub fn clear_proposal() { + for v in public::voters() { + storage::kill(&v.to_keyed_vec(VOTE_OF)); + } + storage::kill(VOTERS); + } +} + +pub mod internal { + use super::*; + use demo_primitives::Proposal; + use dispatch::enact_proposal; + + /// Current era is ending; we should finish up any proposals. + pub fn end_of_an_era() { + // tally up votes for the current proposal, if any. enact if there are sufficient approvals. + if let Some(proposal) = storage::take::(CURRENT_PROPOSAL) { + let tally = public::tally(); + let total_stake = staking::total_stake(); + privileged::clear_proposal(); + + // TODO: protect against overflows. + let threshold = (tally.0 + tally.1).integer_sqrt() * tally.0 / total_stake.integer_sqrt(); + + if tally.1 < threshold { + enact_proposal(proposal); + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use runtime_io::{with_externalities, twox_128, TestExternalities}; + use codec::{KeyedVec, Joiner}; + use keyring::Keyring; + use environment::with_env; + use demo_primitives::{AccountId, Proposal}; + use runtime::{staking, session, democracy}; + + fn new_test_ext() -> TestExternalities { + let alice = Keyring::Alice.to_raw_public(); + let bob = Keyring::Bob.to_raw_public(); + let charlie = Keyring::Charlie.to_raw_public(); + let dave = Keyring::Dave.to_raw_public(); + let eve = Keyring::Eve.to_raw_public(); + let ferdie = Keyring::Ferdie.to_raw_public(); + + map![ + twox_128(b"ses:len").to_vec() => vec![].and(&1u64), + twox_128(b"ses:val:len").to_vec() => vec![].and(&3u32), + twox_128(&0u32.to_keyed_vec(b"ses:val:")).to_vec() => alice.to_vec(), + twox_128(&1u32.to_keyed_vec(b"ses:val:")).to_vec() => bob.to_vec(), + twox_128(&2u32.to_keyed_vec(b"ses:val:")).to_vec() => charlie.to_vec(), + twox_128(b"sta:wil:len").to_vec() => vec![].and(&3u32), + twox_128(&0u32.to_keyed_vec(b"sta:wil:")).to_vec() => alice.to_vec(), + twox_128(&1u32.to_keyed_vec(b"sta:wil:")).to_vec() => bob.to_vec(), + twox_128(&2u32.to_keyed_vec(b"sta:wil:")).to_vec() => charlie.to_vec(), + twox_128(&alice.to_keyed_vec(b"sta:bal:")).to_vec() => vec![].and(&10u64), + twox_128(&bob.to_keyed_vec(b"sta:bal:")).to_vec() => vec![].and(&20u64), + twox_128(&charlie.to_keyed_vec(b"sta:bal:")).to_vec() => vec![].and(&30u64), + twox_128(&dave.to_keyed_vec(b"sta:bal:")).to_vec() => vec![].and(&40u64), + twox_128(&eve.to_keyed_vec(b"sta:bal:")).to_vec() => vec![].and(&50u64), + twox_128(&ferdie.to_keyed_vec(b"sta:bal:")).to_vec() => vec![].and(&60u64), + twox_128(b"sta:tot").to_vec() => vec![].and(&210u64), + twox_128(b"sta:spe").to_vec() => vec![].and(&1u64), + twox_128(b"sta:vac").to_vec() => vec![].and(&3u64), + twox_128(b"sta:era").to_vec() => vec![].and(&1u64) + ] + } + + #[test] + fn majority_voting_should_work() { + let alice = Keyring::Alice.to_raw_public(); + let bob = Keyring::Bob.to_raw_public(); + let charlie = Keyring::Charlie.to_raw_public(); + let dave = Keyring::Dave.to_raw_public(); + let eve = Keyring::Eve.to_raw_public(); + let ferdie = Keyring::Ferdie.to_raw_public(); + let mut t = new_test_ext(); + + with_externalities(&mut t, || { + assert_eq!(staking::era_length(), 1u64); + assert_eq!(staking::total_stake(), 210u64); + + // Block 1: Make proposal. Approve it. Era length changes. + with_env(|e| e.block_number = 1); + public::propose(&alice, &Proposal::StakingSetSessionsPerEra(2)); + public::vote(&ferdie, 1, true); + + assert_eq!(public::tally(), (60, 0)); + + democracy::internal::end_of_an_era(); + staking::internal::check_new_era(); + + assert_eq!(staking::era_length(), 2u64); + }); + } +} diff --git a/substrate/demo/runtime/src/runtime/governance.rs b/substrate/demo/runtime/src/runtime/governance.rs index b7e2fca386..0db372c481 100644 --- a/substrate/demo/runtime/src/runtime/governance.rs +++ b/substrate/demo/runtime/src/runtime/governance.rs @@ -30,6 +30,7 @@ use codec::KeyedVec; use runtime_support::storage; use demo_primitives::{Proposal, AccountId, Hash, BlockNumber}; use runtime::{staking, system, session}; +use dispatch::enact_proposal; const APPROVALS_REQUIRED: &[u8] = b"gov:apr"; const CURRENT_PROPOSAL: &[u8] = b"gov:pro"; @@ -110,36 +111,6 @@ pub mod internal { } } } - - fn enact_proposal(proposal: Proposal) { - match proposal { - Proposal::SystemSetCode(code) => { - system::privileged::set_code(&code); - } - Proposal::SessionSetLength(value) => { - session::privileged::set_length(value); - } - Proposal::SessionForceNewSession => { - session::privileged::force_new_session(); - } - Proposal::StakingSetSessionsPerEra(value) => { - staking::privileged::set_sessions_per_era(value); - } - Proposal::StakingSetBondingDuration(value) => { - staking::privileged::set_bonding_duration(value); - } - Proposal::StakingSetValidatorCount(value) => { - staking::privileged::set_validator_count(value); - } - Proposal::StakingForceNewEra => { - staking::privileged::force_new_era() - } - Proposal::GovernanceSetApprovalPpmRequired(value) => { - self::privileged::set_approval_ppm_required(value); - } - - } - } } #[cfg(test)] diff --git a/substrate/demo/runtime/src/runtime/mod.rs b/substrate/demo/runtime/src/runtime/mod.rs index cfb45f3c8f..2dab848343 100644 --- a/substrate/demo/runtime/src/runtime/mod.rs +++ b/substrate/demo/runtime/src/runtime/mod.rs @@ -28,5 +28,8 @@ pub mod timestamp; pub mod session; #[allow(unused)] pub mod governance; +#[allow(unused)] +pub mod democracy; + // TODO: polkadao diff --git a/substrate/demo/runtime/src/runtime/staking.rs b/substrate/demo/runtime/src/runtime/staking.rs index c9ca26dd24..e5369be6f9 100644 --- a/substrate/demo/runtime/src/runtime/staking.rs +++ b/substrate/demo/runtime/src/runtime/staking.rs @@ -43,6 +43,7 @@ const SESSIONS_PER_ERA: &[u8] = b"sta:spe"; const NEXT_SESSIONS_PER_ERA: &[u8] = b"sta:nse"; const CURRENT_ERA: &[u8] = b"sta:era"; const LAST_ERA_LENGTH_CHANGE: &[u8] = b"sta:lec"; +const TOTAL_STAKE: &[u8] = b"sta:tot"; const BALANCE_OF: &[u8] = b"sta:bal:"; const BONDAGE_OF: &[u8] = b"sta:bon:"; @@ -89,6 +90,11 @@ pub fn bondage(who: &AccountId) -> Bondage { storage::get_or_default(&who.to_keyed_vec(BONDAGE_OF)) } +/// The total amount of stake on the system. +pub fn total_stake() -> Balance { + storage::get_or(TOTAL_STAKE, 0) +} + // Each identity's stake may be in one of three bondage states, given by an integer: // - n | n <= current_era(): inactive: free to be transferred. // - ~0: active: currently representing a validator. @@ -434,6 +440,7 @@ mod tests { twox_128(SESSIONS_PER_ERA).to_vec() => vec![].and(&2u64), twox_128(VALIDATOR_COUNT).to_vec() => vec![].and(&2u32), twox_128(BONDING_DURATION).to_vec() => vec![].and(&3u64), + twox_128(TOTAL_STAKE).to_vec() => vec![].and(&100u64), twox_128(&one.to_keyed_vec(BALANCE_OF)).to_vec() => vec![].and(&10u64), twox_128(&two.to_keyed_vec(BALANCE_OF)).to_vec() => vec![].and(&20u64), twox_128(&three.to_keyed_vec(BALANCE_OF)).to_vec() => vec![].and(&30u64), diff --git a/substrate/demo/runtime/wasm/Cargo.toml b/substrate/demo/runtime/wasm/Cargo.toml index 2c6f440187..04d8999e27 100644 --- a/substrate/demo/runtime/wasm/Cargo.toml +++ b/substrate/demo/runtime/wasm/Cargo.toml @@ -13,6 +13,7 @@ substrate-runtime-io = { path = "../../../substrate/runtime-io", default-feature substrate-runtime-support = { path = "../../../substrate/runtime-support", default-features = false } substrate-primitives = { path = "../../../substrate/primitives", default-features = false } demo-primitives = { path = "../../primitives", default-features = false } +integer-sqrt = "0.1.0" [features] default = [] diff --git a/substrate/polkadot/runtime/src/runtime/staking.rs b/substrate/polkadot/runtime/src/runtime/staking.rs index 0bb4372c6d..1b69698265 100644 --- a/substrate/polkadot/runtime/src/runtime/staking.rs +++ b/substrate/polkadot/runtime/src/runtime/staking.rs @@ -42,6 +42,7 @@ const SESSIONS_PER_ERA: &[u8] = b"sta:spe"; const NEXT_SESSIONS_PER_ERA: &[u8] = b"sta:nse"; const CURRENT_ERA: &[u8] = b"sta:era"; const LAST_ERA_LENGTH_CHANGE: &[u8] = b"sta:lec"; +const TOTAL_STAKE: &[u8] = b"sta:tot"; const BALANCE_OF: &[u8] = b"sta:bal:"; const BONDAGE_OF: &[u8] = b"sta:bon:"; @@ -235,6 +236,7 @@ mod tests { twox_128(SESSIONS_PER_ERA).to_vec() => vec![].and(&2u64), twox_128(VALIDATOR_COUNT).to_vec() => vec![].and(&2u32), twox_128(BONDING_DURATION).to_vec() => vec![].and(&3u64), + twox_128(TOTAL_STAKE).to_vec() => vec![].and(&100u64), twox_128(&one.to_keyed_vec(BALANCE_OF)).to_vec() => vec![].and(&10u64), twox_128(&two.to_keyed_vec(BALANCE_OF)).to_vec() => vec![].and(&20u64), twox_128(&three.to_keyed_vec(BALANCE_OF)).to_vec() => vec![].and(&30u64),