From 54f3350ee95b8795c14467c9a764b4728cec76bd Mon Sep 17 00:00:00 2001 From: Saeedeh Date: Wed, 28 Jun 2023 13:46:46 -0400 Subject: [PATCH] add collective pallet. (#105) * add collective pallet. * finished setting members for council. * store council members to subtensor concilmember storgaemap. * add storagemap to store concil members inside subtensor pallet. * add a council call. * add council_set_max_registrations_per_block for council members. * Begin switching to pallet_membership for council mgnmt * Add test random collective * Add pallet-membership to Cargo.toml * Add pallet-membership config * EnsureRootOrHalfCouncil -> EnsureRootOrMajorityCouncil * Add pallet-membership to chainspec.rs * Remove --execution native flag from localnet * Enforce specific toolchain for project * Allow localnet to discover local validators instead of bootnode * Re-add pallet-collective from git over fs * Properly insert properties map into chainspec * Add SudoRuntimeCall, CouncilOrigin to subtensor::Config * Add sudo extrinsic to subtensor * Add Sudid event to subtensor * Fix testing for subtensor by removing collective, membership from runtime * Create EnsureCouncilMajority origin in runtime * Remove pallet-sudo from runtime, remove pallet-collective from dev-deps * CouncilMaxMembers -> 3, CouncilMaxProposals -> 10 * Remove sudo pallet from construct_runtime! * Remove sudo pallet config from runtime * Remove vestiges of sudo from chain_spec * Add localnet chain-spec for testing * Fix tabulation in subtensor::sudo comment * Re-add sudo to runtime * Ensure root for membership mngmt * Return DispatchResult of call from sudo * Add sudoUncheckedWeight extrinsic to subtensor * CouncilMotionDuration -> 1 DAY * Move collective pallet into repository * Add SenateCollective, SenateMembers to manage senate votes * :robot: :butter: Reformat source code * Add VoteOrigin, ProposalOrigin to collective pallet * Add more test accounts to localnet_genesis * Fix removed finney testnet sudo key * Add CanPropose, CanVote, GetVotingMembers traits * Add CanPropose, CanVote implementations to runtime for collective * Remove threshold argument from collective::propose * Add max members for senate * Remove threshold argument from collective::benchmarks::propose * Add GetVotingMembers implementation for Collective * Remove unused pallet-collective in subtensor * Remove pallet_collective imports from subtensor * Add MemberManagement trait to subtensor, implementation in runtime * Add swap_member, members to MemberManagement trait * Implement new MemberManagement interface methods * Add senate module, do_join_senate, do_leave_senate extrinsics to subtensor * Add max_members to MemberManagement, fix vec includes for interface * Add subtensor::SenateRequiredStakePercentage storage * Add benchmarks for join_senate, leave_senate * Fix all broken benchmarks * Reverse sort stake comparison in join_senate for descending order * Fix all benchmarks properly this time * Speed up benchmarking process for subtensor * Use a full senate for senate benchmarking * Update weights and DB reads for join_senate, leave_senate * Remove senate member if required stake drops below threshold * Update remove_stake benchmark to include senate removal threshold * Update remove_stake weight * Fix division by zero case in join_senate * Remove cast votes of senate member when leaving * CouncilMotionDuration 7200 blocks -> 100 blocks * Use coldkey as signing origin for senate actions Add add_vote to TriumvirateInterface Update benchmarks for new signing scheme Fix some tabulation differences in benchmarks.rs * Update Cargo.lock for subtensor * Add collective, membership pallets to subtensor Cargo.toml * Update collective testing to support modifications * Add triumvirate+senate to subtensor testing * Up spec version 121 -> 122 * Handle result in CollectiveInterface::remove_votes * Begin implementing subtensor-specific senate testing * Add verbose errors for senate actions * Fix staking removal tests broken by weight updates * Add pallet-collective and pallet-membership to subtensor std for testing * Fix division by zero in remove_stake for senate removal * Add has_voted helper to pallet-collective * Add complete test suite for senate activities in subtensor * Fix errors from resolved merge conflicts * Update Cargo.toml authors and remove collective/migrations * Remove migrations testing in pallet-collective --------- Co-authored-by: Rubberbandits --- Cargo.lock | 37 + node/src/chain_spec.rs | 216 ++-- pallets/collective/Cargo.toml | 47 + pallets/collective/README.md | 25 + pallets/collective/src/benchmarking.rs | 611 ++++++++++ pallets/collective/src/lib.rs | 1198 ++++++++++++++++++++ pallets/collective/src/tests.rs | 1409 ++++++++++++++++++++++++ pallets/collective/src/weights.rs | 554 ++++++++++ pallets/subtensor/Cargo.toml | 6 + pallets/subtensor/src/benchmarks.rs | 195 +++- pallets/subtensor/src/lib.rs | 201 +++- pallets/subtensor/src/senate.rs | 102 ++ pallets/subtensor/src/staking.rs | 11 + pallets/subtensor/src/utils.rs | 6 +- pallets/subtensor/tests/mock.rs | 163 ++- pallets/subtensor/tests/senate.rs | 492 +++++++++ pallets/subtensor/tests/staking.rs | 2 +- runtime/Cargo.toml | 18 +- runtime/src/lib.rs | 171 ++- 19 files changed, 5316 insertions(+), 148 deletions(-) create mode 100644 pallets/collective/Cargo.toml create mode 100644 pallets/collective/README.md create mode 100644 pallets/collective/src/benchmarking.rs create mode 100644 pallets/collective/src/lib.rs create mode 100644 pallets/collective/src/tests.rs create mode 100644 pallets/collective/src/weights.rs create mode 100644 pallets/subtensor/src/senate.rs create mode 100644 pallets/subtensor/tests/senate.rs diff --git a/Cargo.lock b/Cargo.lock index 703544d75..367708fa4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4190,8 +4190,10 @@ dependencies = [ "frame-try-runtime", "pallet-aura", "pallet-balances", + "pallet-collective", "pallet-grandpa", "pallet-insecure-randomness-collective-flip", + "pallet-membership", "pallet-multisig", "pallet-subtensor", "pallet-sudo", @@ -4457,6 +4459,22 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-collective" +version = "4.0.0-dev" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "log", + "parity-scale-codec", + "scale-info", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", +] + [[package]] name = "pallet-grandpa" version = "4.0.0-dev" @@ -4494,6 +4512,23 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-membership" +version = "4.0.0-dev" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.39#8c4b84520cee2d7de53cc33cb67605ce4efefba8" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "log", + "parity-scale-codec", + "scale-info", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", +] + [[package]] name = "pallet-multisig" version = "4.0.0-dev" @@ -4541,6 +4576,8 @@ dependencies = [ "log", "ndarray", "pallet-balances", + "pallet-collective", + "pallet-membership", "pallet-transaction-payment", "pallet-utility", "parity-scale-codec", diff --git a/node/src/chain_spec.rs b/node/src/chain_spec.rs index 4084c11c2..c09e55f0a 100644 --- a/node/src/chain_spec.rs +++ b/node/src/chain_spec.rs @@ -1,10 +1,11 @@ use node_subtensor_runtime::{ AccountId, AuraConfig, BalancesConfig, GenesisConfig, GrandpaConfig, Signature, SudoConfig, - SystemConfig, WASM_BINARY, SubtensorModuleConfig + SystemConfig, WASM_BINARY, SubtensorModuleConfig, TriumvirateConfig, TriumvirateMembersConfig, + SenateConfig, SenateMembersConfig }; use sc_service::ChainType; use sp_consensus_aura::sr25519::AuthorityId as AuraId; -use sp_core::{sr25519, Pair, Public}; +use sp_core::{sr25519, Pair, Public, bounded_vec}; use sp_finality_grandpa::AuthorityId as GrandpaId; use sp_runtime::traits::{IdentifyAccount, Verify}; use sp_core::crypto::Ss58Codec; @@ -286,84 +287,6 @@ pub fn finney_testnet_config() -> Result { )) } -// Configure initial storage state for FRAME modules. -fn testnet_genesis( - wasm_binary: &[u8], - initial_authorities: Vec<(AuraId, GrandpaId)>, - root_key: AccountId, - _endowed_accounts: Vec, - _enable_println: bool, - _stakes: Vec<(AccountId, Vec<(AccountId, (u64, u16))>)>, - _balances: Vec<(AccountId, u64)>, - _balances_issuance: u64 -) -> GenesisConfig { - GenesisConfig { - system: SystemConfig { - // Add Wasm runtime to storage. - code: wasm_binary.to_vec(), - }, - balances: BalancesConfig { - // Configure sudo balance - balances: vec![ - (Ss58Codec::from_ss58check("5GpzQgpiAKHMWNSH3RN4GLf96GVTDct9QxYEFAY7LWcVzTbx").unwrap(),1000000000000) - ] - }, - aura: AuraConfig { - authorities: initial_authorities.iter().map(|x| (x.0.clone())).collect(), - }, - grandpa: GrandpaConfig { - authorities: initial_authorities.iter().map(|x| (x.1.clone(), 1)).collect(), - }, - sudo: SudoConfig { - // Assign network admin rights. - key: Some(root_key), - }, - transaction_payment: Default::default(), - subtensor_module: Default::default(), - } -} - - -// Configure initial storage state for FRAME modules. -fn finney_genesis( - wasm_binary: &[u8], - initial_authorities: Vec<(AuraId, GrandpaId)>, - root_key: AccountId, - _endowed_accounts: Vec, - _enable_println: bool, - stakes: Vec<(AccountId, Vec<(AccountId, (u64, u16))>)>, - balances: Vec<(AccountId, u64)>, - balances_issuance: u64 - -) -> GenesisConfig { - GenesisConfig { - system: SystemConfig { - // Add Wasm runtime to storage. - code: wasm_binary.to_vec(), - }, - balances: BalancesConfig { - // Configure endowed accounts with initial balance of 1 << 60. - //balances: balances.iter().cloned().map(|k| k).collect(), - balances: balances.iter().cloned().map(|k| k).collect(), - }, - aura: AuraConfig { - authorities: initial_authorities.iter().map(|x| (x.0.clone())).collect(), - }, - grandpa: GrandpaConfig { - authorities: initial_authorities.iter().map(|x| (x.1.clone(), 1)).collect(), - }, - sudo: SudoConfig { - // Assign network admin rights. - key: Some(root_key), - }, - transaction_payment: Default::default(), - subtensor_module: SubtensorModuleConfig { - stakes: stakes, - balances_issuance: balances_issuance - }, - } -} - pub fn localnet_config() -> Result { let path: PathBuf = std::path::PathBuf::from("./snapshot.json"); let wasm_binary = WASM_BINARY.ok_or_else(|| "Development wasm not available".to_string())?; @@ -483,5 +406,138 @@ fn localnet_genesis( }, transaction_payment: Default::default(), subtensor_module: Default::default(), + triumvirate: TriumvirateConfig { + members: Default::default(), + phantom: Default::default(), + }, + triumvirate_members: TriumvirateMembersConfig { + members: bounded_vec![ + get_account_id_from_seed::("Alice"), + get_account_id_from_seed::("Bob"), + get_account_id_from_seed::("Charlie"), + ], + phantom: Default::default() + }, + senate: SenateConfig { + members: Default::default(), + phantom: Default::default(), + }, + senate_members: SenateMembersConfig { + members: bounded_vec![ + get_account_id_from_seed::("Dave"), + get_account_id_from_seed::("Eve"), + get_account_id_from_seed::("Ferdie"), + ], + phantom: Default::default() + } } } + + +// Configure initial storage state for FRAME modules. +fn testnet_genesis( + wasm_binary: &[u8], + initial_authorities: Vec<(AuraId, GrandpaId)>, + root_key: AccountId, + _endowed_accounts: Vec, + _enable_println: bool, + _stakes: Vec<(AccountId, Vec<(AccountId, (u64, u16))>)>, + _balances: Vec<(AccountId, u64)>, + _balances_issuance: u64 +) -> GenesisConfig { + GenesisConfig { + system: SystemConfig { + // Add Wasm runtime to storage. + code: wasm_binary.to_vec(), + }, + balances: BalancesConfig { + // Configure sudo balance + balances: vec![ + (Ss58Codec::from_ss58check("5GpzQgpiAKHMWNSH3RN4GLf96GVTDct9QxYEFAY7LWcVzTbx").unwrap(),1000000000000) + ] + }, + aura: AuraConfig { + authorities: initial_authorities.iter().map(|x| (x.0.clone())).collect(), + }, + grandpa: GrandpaConfig { + authorities: initial_authorities.iter().map(|x| (x.1.clone(), 1)).collect(), + }, + sudo: SudoConfig { + key: Some(Ss58Codec::from_ss58check("5GpzQgpiAKHMWNSH3RN4GLf96GVTDct9QxYEFAY7LWcVzTbx").unwrap()), + }, + transaction_payment: Default::default(), + subtensor_module: Default::default(), + triumvirate: TriumvirateConfig { // Add initial authorities as collective members + members: Default::default(),//initial_authorities.iter().map(|x| x.0.clone()).collect::>(), + phantom: Default::default(), + }, + triumvirate_members: TriumvirateMembersConfig { + members: Default::default(), + phantom: Default::default() + }, + senate: SenateConfig { + members: Default::default(), + phantom: Default::default(), + }, + senate_members: SenateMembersConfig { + members: Default::default(), + phantom: Default::default() + } + } +} + + +// Configure initial storage state for FRAME modules. +fn finney_genesis( + wasm_binary: &[u8], + initial_authorities: Vec<(AuraId, GrandpaId)>, + root_key: AccountId, + _endowed_accounts: Vec, + _enable_println: bool, + stakes: Vec<(AccountId, Vec<(AccountId, (u64, u16))>)>, + balances: Vec<(AccountId, u64)>, + balances_issuance: u64 + +) -> GenesisConfig { + GenesisConfig { + system: SystemConfig { + // Add Wasm runtime to storage. + code: wasm_binary.to_vec(), + }, + balances: BalancesConfig { + // Configure endowed accounts with initial balance of 1 << 60. + //balances: balances.iter().cloned().map(|k| k).collect(), + balances: balances.iter().cloned().map(|k| k).collect(), + }, + aura: AuraConfig { + authorities: initial_authorities.iter().map(|x| (x.0.clone())).collect(), + }, + grandpa: GrandpaConfig { + authorities: initial_authorities.iter().map(|x| (x.1.clone(), 1)).collect(), + }, + sudo: SudoConfig { + key: Some(Ss58Codec::from_ss58check("5FCM3DBXWiGcwYYQtT8z4ZD93TqYpYxjaAfgv6aMStV1FTCT").unwrap()), + }, + transaction_payment: Default::default(), + subtensor_module: SubtensorModuleConfig { + stakes: stakes, + balances_issuance: balances_issuance + }, + triumvirate: TriumvirateConfig { // Add initial authorities as collective members + members: Default::default(),//initial_authorities.iter().map(|x| x.0.clone()).collect::>(), + phantom: Default::default(), + }, + triumvirate_members: TriumvirateMembersConfig { + members: Default::default(), + phantom: Default::default() + }, + senate: SenateConfig { + members: Default::default(), + phantom: Default::default(), + }, + senate_members: SenateMembersConfig { + members: Default::default(), + phantom: Default::default() + } + } +} \ No newline at end of file diff --git a/pallets/collective/Cargo.toml b/pallets/collective/Cargo.toml new file mode 100644 index 000000000..34c6dcb63 --- /dev/null +++ b/pallets/collective/Cargo.toml @@ -0,0 +1,47 @@ +[package] +name = "pallet-collective" +version = "4.0.0-dev" +authors = ["Parity Technologies , Opentensor Technologies"] +edition = "2021" +license = "Apache-2.0" +homepage = "https://bittensor.com" +repository = "https://github.com/opentensor/subtensor" +description = "Collective system: Members of a set of account IDs can make their collective feelings known through dispatched calls from one of two specialized origins." +readme = "README.md" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +codec = { package = "parity-scale-codec", version = "3.2.2", default-features = false, features = ["derive"] } +log = { version = "0.4.17", default-features = false } +scale-info = { version = "2.1.1", default-features = false, features = ["derive"] } +frame-benchmarking = { version = "4.0.0-dev", default-features = false, optional = true, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.39" } +frame-support = { version = "4.0.0-dev", default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.39" } +frame-system = { version = "4.0.0-dev", default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.39" } +sp-core = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "polkadot-v0.9.39" } +sp-io = { version = "7.0.0", default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.39" } +sp-runtime = { version = "7.0.0", default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.39" } +sp-std = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "polkadot-v0.9.39" } + +[features] +default = ["std"] +std = [ + "codec/std", + "frame-benchmarking?/std", + "frame-support/std", + "frame-system/std", + "log/std", + "scale-info/std", + "sp-core/std", + "sp-io/std", + "sp-runtime/std", + "sp-std/std", +] +runtime-benchmarks = [ + "frame-benchmarking/runtime-benchmarks", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", +] +try-runtime = ["frame-support/try-runtime"] diff --git a/pallets/collective/README.md b/pallets/collective/README.md new file mode 100644 index 000000000..444927e51 --- /dev/null +++ b/pallets/collective/README.md @@ -0,0 +1,25 @@ +Collective system: Members of a set of account IDs can make their collective feelings known +through dispatched calls from one of two specialized origins. + +The membership can be provided in one of two ways: either directly, using the Root-dispatchable +function `set_members`, or indirectly, through implementing the `ChangeMembers`. +The pallet assumes that the amount of members stays at or below `MaxMembers` for its weight +calculations, but enforces this neither in `set_members` nor in `change_members_sorted`. + +A "prime" member may be set to help determine the default vote behavior based on chain +config. If `PrimeDefaultVote` is used, the prime vote acts as the default vote in case of any +abstentions after the voting period. If `MoreThanMajorityThenPrimeDefaultVote` is used, then +abstentations will first follow the majority of the collective voting, and then the prime +member. + +Voting happens through motions comprising a proposal (i.e. a dispatchable) plus a +number of approvals required for it to pass and be called. Motions are open for members to +vote on for a minimum period given by `MotionDuration`. As soon as the required number of +approvals is given, the motion is closed and executed. If the number of approvals is not reached +during the voting period, then `close` may be called by any account in order to force the end +the motion explicitly. If a prime member is defined, then their vote is used instead of any +abstentions and the proposal is executed if there are enough approvals counting the new votes. + +If there are not, or if no prime member is set, then the motion is dropped without being executed. + +License: Apache-2.0 diff --git a/pallets/collective/src/benchmarking.rs b/pallets/collective/src/benchmarking.rs new file mode 100644 index 000000000..b8dcedabf --- /dev/null +++ b/pallets/collective/src/benchmarking.rs @@ -0,0 +1,611 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Staking pallet benchmarking. + +use super::*; +use crate::Pallet as Collective; + +use sp_runtime::traits::Bounded; +use sp_std::mem::size_of; + +use frame_benchmarking::v1::{account, benchmarks_instance_pallet, whitelisted_caller}; +use frame_system::{Call as SystemCall, Pallet as System, RawOrigin as SystemOrigin}; + +const SEED: u32 = 0; + +const MAX_BYTES: u32 = 1_024; + +fn assert_last_event, I: 'static>(generic_event: >::RuntimeEvent) { + frame_system::Pallet::::assert_last_event(generic_event.into()); +} + +fn id_to_remark_data(id: u32, length: usize) -> Vec { + id.to_le_bytes().into_iter().cycle().take(length).collect() +} + +benchmarks_instance_pallet! { + set_members { + let m in 0 .. T::MaxMembers::get(); + let n in 0 .. T::MaxMembers::get(); + let p in 0 .. T::MaxProposals::get(); + + // Set old members. + // We compute the difference of old and new members, so it should influence timing. + let mut old_members = vec![]; + for i in 0 .. m { + let old_member = account::("old member", i, SEED); + old_members.push(old_member); + } + let old_members_count = old_members.len() as u32; + + Collective::::set_members( + SystemOrigin::Root.into(), + old_members.clone(), + old_members.last().cloned(), + T::MaxMembers::get(), + )?; + + // If there were any old members generate a bunch of proposals. + if m > 0 { + // Set a high threshold for proposals passing so that they stay around. + let threshold = m.max(2); + // Length of the proposals should be irrelevant to `set_members`. + let length = 100; + for i in 0 .. p { + // Proposals should be different so that different proposal hashes are generated + let proposal: T::Proposal = SystemCall::::remark { remark: id_to_remark_data(i, length) }.into(); + Collective::::propose( + SystemOrigin::Signed(old_members.last().unwrap().clone()).into(), + Box::new(proposal.clone()), + MAX_BYTES, + )?; + let hash = T::Hashing::hash_of(&proposal); + // Vote on the proposal to increase state relevant for `set_members`. + // Not voting for last old member because they proposed and not voting for the first member + // to keep the proposal from passing. + for j in 2 .. m - 1 { + let voter = &old_members[j as usize]; + let approve = true; + Collective::::vote( + SystemOrigin::Signed(voter.clone()).into(), + hash, + i, + approve, + )?; + } + } + } + + // Construct `new_members`. + // It should influence timing since it will sort this vector. + let mut new_members = vec![]; + for i in 0 .. n { + let member = account::("member", i, SEED); + new_members.push(member); + } + + }: _(SystemOrigin::Root, new_members.clone(), new_members.last().cloned(), T::MaxMembers::get()) + verify { + new_members.sort(); + assert_eq!(Collective::::members(), new_members); + } + + execute { + let b in 2 .. MAX_BYTES; + let m in 1 .. T::MaxMembers::get(); + + let bytes_in_storage = b + size_of::() as u32; + + // Construct `members`. + let mut members = vec![]; + for i in 0 .. m - 1 { + let member = account::("member", i, SEED); + members.push(member); + } + + let caller: T::AccountId = whitelisted_caller(); + members.push(caller.clone()); + + Collective::::set_members(SystemOrigin::Root.into(), members, None, T::MaxMembers::get())?; + + let proposal: T::Proposal = SystemCall::::remark { remark: id_to_remark_data(1, b as usize) }.into(); + + }: _(SystemOrigin::Signed(caller), Box::new(proposal.clone()), bytes_in_storage) + verify { + let proposal_hash = T::Hashing::hash_of(&proposal); + // Note that execution fails due to mis-matched origin + assert_last_event::( + Event::MemberExecuted { proposal_hash, result: Err(DispatchError::BadOrigin) }.into() + ); + } + + // This tests when proposal is created and queued as "proposed" + propose_proposed { + let b in 2 .. MAX_BYTES; + let m in 2 .. T::MaxMembers::get(); + let p in 1 .. T::MaxProposals::get(); + + let bytes_in_storage = b + size_of::() as u32; + + // Construct `members`. + let mut members = vec![]; + for i in 0 .. m - 1 { + let member = account::("member", i, SEED); + members.push(member); + } + let caller: T::AccountId = whitelisted_caller(); + members.push(caller.clone()); + Collective::::set_members(SystemOrigin::Root.into(), members, None, T::MaxMembers::get())?; + + let threshold = m; + // Add previous proposals. + for i in 0 .. p - 1 { + // Proposals should be different so that different proposal hashes are generated + let proposal: T::Proposal = SystemCall::::remark { remark: id_to_remark_data(i, b as usize) }.into(); + Collective::::propose( + SystemOrigin::Signed(caller.clone()).into(), + Box::new(proposal), + bytes_in_storage, + )?; + } + + assert_eq!(Collective::::proposals().len(), (p - 1) as usize); + + let proposal: T::Proposal = SystemCall::::remark { remark: id_to_remark_data(p, b as usize) }.into(); + + }: propose(SystemOrigin::Signed(caller.clone()), Box::new(proposal.clone()), bytes_in_storage) + verify { + // New proposal is recorded + assert_eq!(Collective::::proposals().len(), p as usize); + let proposal_hash = T::Hashing::hash_of(&proposal); + assert_last_event::(Event::Proposed { account: caller, proposal_index: p - 1, proposal_hash, threshold }.into()); + } + + vote { + // We choose 5 as a minimum so we always trigger a vote in the voting loop (`for j in ...`) + let m in 5 .. T::MaxMembers::get(); + + let p = T::MaxProposals::get(); + let b = MAX_BYTES; + let bytes_in_storage = b + size_of::() as u32; + + // Construct `members`. + let mut members = vec![]; + let proposer: T::AccountId = account::("proposer", 0, SEED); + members.push(proposer.clone()); + for i in 1 .. m - 1 { + let member = account::("member", i, SEED); + members.push(member); + } + let voter: T::AccountId = account::("voter", 0, SEED); + members.push(voter.clone()); + Collective::::set_members(SystemOrigin::Root.into(), members.clone(), None, T::MaxMembers::get())?; + + // Threshold is 1 less than the number of members so that one person can vote nay + let threshold = m - 1; + + // Add previous proposals + let mut last_hash = T::Hash::default(); + for i in 0 .. p { + // Proposals should be different so that different proposal hashes are generated + let proposal: T::Proposal = SystemCall::::remark { remark: id_to_remark_data(i, b as usize) }.into(); + Collective::::propose( + SystemOrigin::Signed(proposer.clone()).into(), + Box::new(proposal.clone()), + bytes_in_storage, + )?; + last_hash = T::Hashing::hash_of(&proposal); + } + + let index = p - 1; + // Have almost everyone vote aye on last proposal, while keeping it from passing. + for j in 0 .. m - 3 { + let voter = &members[j as usize]; + let approve = true; + Collective::::vote( + SystemOrigin::Signed(voter.clone()).into(), + last_hash, + index, + approve, + )?; + } + // Voter votes aye without resolving the vote. + let approve = true; + Collective::::vote( + SystemOrigin::Signed(voter.clone()).into(), + last_hash, + index, + approve, + )?; + + assert_eq!(Collective::::proposals().len(), p as usize); + + // Voter switches vote to nay, but does not kill the vote, just updates + inserts + let approve = false; + + // Whitelist voter account from further DB operations. + let voter_key = frame_system::Account::::hashed_key_for(&voter); + frame_benchmarking::benchmarking::add_to_whitelist(voter_key.into()); + }: _(SystemOrigin::Signed(voter), last_hash, index, approve) + verify { + // All proposals exist and the last proposal has just been updated. + assert_eq!(Collective::::proposals().len(), p as usize); + let voting = Collective::::voting(&last_hash).ok_or("Proposal Missing")?; + assert_eq!(voting.ayes.len(), (m - 3) as usize); + assert_eq!(voting.nays.len(), 1); + } + + close_early_disapproved { + // We choose 4 as a minimum so we always trigger a vote in the voting loop (`for j in ...`) + let m in 4 .. T::MaxMembers::get(); + let p in 1 .. T::MaxProposals::get(); + + let bytes = 100; + let bytes_in_storage = bytes + size_of::() as u32; + + // Construct `members`. + let mut members = vec![]; + let proposer = account::("proposer", 0, SEED); + members.push(proposer.clone()); + for i in 1 .. m - 1 { + let member = account::("member", i, SEED); + members.push(member); + } + let voter = account::("voter", 0, SEED); + members.push(voter.clone()); + Collective::::set_members(SystemOrigin::Root.into(), members.clone(), None, T::MaxMembers::get())?; + + // Threshold is total members so that one nay will disapprove the vote + let threshold = m; + + // Add previous proposals + let mut last_hash = T::Hash::default(); + for i in 0 .. p { + // Proposals should be different so that different proposal hashes are generated + let proposal: T::Proposal = SystemCall::::remark { remark: id_to_remark_data(i, bytes as usize) }.into(); + Collective::::propose( + SystemOrigin::Signed(proposer.clone()).into(), + Box::new(proposal.clone()), + bytes_in_storage, + )?; + last_hash = T::Hashing::hash_of(&proposal); + } + + let index = p - 1; + // Have most everyone vote aye on last proposal, while keeping it from passing. + for j in 0 .. m - 2 { + let voter = &members[j as usize]; + let approve = true; + Collective::::vote( + SystemOrigin::Signed(voter.clone()).into(), + last_hash, + index, + approve, + )?; + } + // Voter votes aye without resolving the vote. + let approve = true; + Collective::::vote( + SystemOrigin::Signed(voter.clone()).into(), + last_hash, + index, + approve, + )?; + + assert_eq!(Collective::::proposals().len(), p as usize); + + // Voter switches vote to nay, which kills the vote + let approve = false; + Collective::::vote( + SystemOrigin::Signed(voter.clone()).into(), + last_hash, + index, + approve, + )?; + + // Whitelist voter account from further DB operations. + let voter_key = frame_system::Account::::hashed_key_for(&voter); + frame_benchmarking::benchmarking::add_to_whitelist(voter_key.into()); + }: close(SystemOrigin::Signed(voter), last_hash, index, Weight::MAX, bytes_in_storage) + verify { + // The last proposal is removed. + assert_eq!(Collective::::proposals().len(), (p - 1) as usize); + assert_last_event::(Event::Disapproved { proposal_hash: last_hash }.into()); + } + + close_early_approved { + let b in 2 .. MAX_BYTES; + // We choose 4 as a minimum so we always trigger a vote in the voting loop (`for j in ...`) + let m in 4 .. T::MaxMembers::get(); + let p in 1 .. T::MaxProposals::get(); + + let bytes_in_storage = b + size_of::() as u32; + + // Construct `members`. + let mut members = vec![]; + for i in 0 .. m - 1 { + let member = account::("member", i, SEED); + members.push(member); + } + let caller: T::AccountId = whitelisted_caller(); + members.push(caller.clone()); + Collective::::set_members(SystemOrigin::Root.into(), members.clone(), None, T::MaxMembers::get())?; + + // Threshold is 2 so any two ayes will approve the vote + let threshold = 2; + + // Add previous proposals + let mut last_hash = T::Hash::default(); + for i in 0 .. p { + // Proposals should be different so that different proposal hashes are generated + let proposal: T::Proposal = SystemCall::::remark { remark: id_to_remark_data(i, b as usize) }.into(); + Collective::::propose( + SystemOrigin::Signed(caller.clone()).into(), + Box::new(proposal.clone()), + bytes_in_storage, + )?; + last_hash = T::Hashing::hash_of(&proposal); + } + + // Caller switches vote to nay on their own proposal, allowing them to be the deciding approval vote + Collective::::vote( + SystemOrigin::Signed(caller.clone()).into(), + last_hash, + p - 1, + false, + )?; + + // Have almost everyone vote nay on last proposal, while keeping it from failing. + for j in 2 .. m - 1 { + let voter = &members[j as usize]; + let approve = false; + Collective::::vote( + SystemOrigin::Signed(voter.clone()).into(), + last_hash, + p - 1, + approve, + )?; + } + + // Member zero is the first aye + Collective::::vote( + SystemOrigin::Signed(members[0].clone()).into(), + last_hash, + p - 1, + true, + )?; + + assert_eq!(Collective::::proposals().len(), p as usize); + + // Caller switches vote to aye, which passes the vote + let index = p - 1; + let approve = true; + Collective::::vote( + SystemOrigin::Signed(caller.clone()).into(), + last_hash, + index, approve, + )?; + + }: close(SystemOrigin::Signed(caller), last_hash, index, Weight::MAX, bytes_in_storage) + verify { + // The last proposal is removed. + assert_eq!(Collective::::proposals().len(), (p - 1) as usize); + assert_last_event::(Event::Executed { proposal_hash: last_hash, result: Err(DispatchError::BadOrigin) }.into()); + } + + close_disapproved { + // We choose 4 as a minimum so we always trigger a vote in the voting loop (`for j in ...`) + let m in 4 .. T::MaxMembers::get(); + let p in 1 .. T::MaxProposals::get(); + + let bytes = 100; + let bytes_in_storage = bytes + size_of::() as u32; + + // Construct `members`. + let mut members = vec![]; + for i in 0 .. m - 1 { + let member = account::("member", i, SEED); + members.push(member); + } + let caller: T::AccountId = whitelisted_caller(); + members.push(caller.clone()); + Collective::::set_members( + SystemOrigin::Root.into(), + members.clone(), + Some(caller.clone()), + T::MaxMembers::get(), + )?; + + // Threshold is one less than total members so that two nays will disapprove the vote + let threshold = m - 1; + + // Add proposals + let mut last_hash = T::Hash::default(); + for i in 0 .. p { + // Proposals should be different so that different proposal hashes are generated + let proposal: T::Proposal = SystemCall::::remark { remark: id_to_remark_data(i, bytes as usize) }.into(); + Collective::::propose( + SystemOrigin::Signed(caller.clone()).into(), + Box::new(proposal.clone()), + bytes_in_storage, + )?; + last_hash = T::Hashing::hash_of(&proposal); + } + + let index = p - 1; + // Have almost everyone vote aye on last proposal, while keeping it from passing. + // A few abstainers will be the nay votes needed to fail the vote. + let mut yes_votes: MemberCount = 0; + for j in 2 .. m - 1 { + let voter = &members[j as usize]; + let approve = true; + yes_votes += 1; + // vote aye till a prime nay vote keeps the proposal disapproved. + if <>::DefaultVote as DefaultVote>::default_vote( + Some(false), + yes_votes, + 0, + m,) { + break; + } + Collective::::vote( + SystemOrigin::Signed(voter.clone()).into(), + last_hash, + index, + approve, + )?; + } + + // caller is prime, prime votes nay + Collective::::vote( + SystemOrigin::Signed(caller.clone()).into(), + last_hash, + index, + false, + )?; + + System::::set_block_number(T::BlockNumber::max_value()); + assert_eq!(Collective::::proposals().len(), p as usize); + + // Prime nay will close it as disapproved + }: close(SystemOrigin::Signed(caller), last_hash, index, Weight::MAX, bytes_in_storage) + verify { + assert_eq!(Collective::::proposals().len(), (p - 1) as usize); + assert_last_event::(Event::Disapproved { proposal_hash: last_hash }.into()); + } + + close_approved { + let b in 2 .. MAX_BYTES; + // We choose 4 as a minimum so we always trigger a vote in the voting loop (`for j in ...`) + let m in 4 .. T::MaxMembers::get(); + let p in 1 .. T::MaxProposals::get(); + + let bytes_in_storage = b + size_of::() as u32; + + // Construct `members`. + let mut members = vec![]; + for i in 0 .. m - 1 { + let member = account::("member", i, SEED); + members.push(member); + } + let caller: T::AccountId = whitelisted_caller(); + members.push(caller.clone()); + Collective::::set_members( + SystemOrigin::Root.into(), + members.clone(), + Some(caller.clone()), + T::MaxMembers::get(), + )?; + + // Threshold is two, so any two ayes will pass the vote + let threshold = 2; + + // Add proposals + let mut last_hash = T::Hash::default(); + for i in 0 .. p { + // Proposals should be different so that different proposal hashes are generated + let proposal: T::Proposal = SystemCall::::remark { remark: id_to_remark_data(i, b as usize) }.into(); + Collective::::propose( + SystemOrigin::Signed(caller.clone()).into(), + Box::new(proposal.clone()), + bytes_in_storage, + )?; + last_hash = T::Hashing::hash_of(&proposal); + } + + // The prime member votes aye, so abstentions default to aye. + Collective::::vote( + SystemOrigin::Signed(caller.clone()).into(), + last_hash, + p - 1, + true // Vote aye. + )?; + + // Have almost everyone vote nay on last proposal, while keeping it from failing. + // A few abstainers will be the aye votes needed to pass the vote. + for j in 2 .. m - 1 { + let voter = &members[j as usize]; + let approve = false; + Collective::::vote( + SystemOrigin::Signed(voter.clone()).into(), + last_hash, + p - 1, + approve + )?; + } + + // caller is prime, prime already votes aye by creating the proposal + System::::set_block_number(T::BlockNumber::max_value()); + assert_eq!(Collective::::proposals().len(), p as usize); + + // Prime aye will close it as approved + }: close(SystemOrigin::Signed(caller), last_hash, p - 1, Weight::MAX, bytes_in_storage) + verify { + assert_eq!(Collective::::proposals().len(), (p - 1) as usize); + assert_last_event::(Event::Executed { proposal_hash: last_hash, result: Err(DispatchError::BadOrigin) }.into()); + } + + disapprove_proposal { + let p in 1 .. T::MaxProposals::get(); + + let m = 3; + let b = MAX_BYTES; + let bytes_in_storage = b + size_of::() as u32; + + // Construct `members`. + let mut members = vec![]; + for i in 0 .. m - 1 { + let member = account::("member", i, SEED); + members.push(member); + } + let caller = account::("caller", 0, SEED); + members.push(caller.clone()); + Collective::::set_members( + SystemOrigin::Root.into(), + members.clone(), + Some(caller.clone()), + T::MaxMembers::get(), + )?; + + // Threshold is one less than total members so that two nays will disapprove the vote + let threshold = m - 1; + + // Add proposals + let mut last_hash = T::Hash::default(); + for i in 0 .. p { + // Proposals should be different so that different proposal hashes are generated + let proposal: T::Proposal = SystemCall::::remark { remark: id_to_remark_data(i, b as usize) }.into(); + Collective::::propose( + SystemOrigin::Signed(caller.clone()).into(), + Box::new(proposal.clone()), + bytes_in_storage, + )?; + last_hash = T::Hashing::hash_of(&proposal); + } + + System::::set_block_number(T::BlockNumber::max_value()); + assert_eq!(Collective::::proposals().len(), p as usize); + + }: _(SystemOrigin::Root, last_hash) + verify { + assert_eq!(Collective::::proposals().len(), (p - 1) as usize); + assert_last_event::(Event::Disapproved { proposal_hash: last_hash }.into()); + } + + impl_benchmark_test_suite!(Collective, crate::tests::new_test_ext(), crate::tests::Test); +} diff --git a/pallets/collective/src/lib.rs b/pallets/collective/src/lib.rs new file mode 100644 index 000000000..858e7cd85 --- /dev/null +++ b/pallets/collective/src/lib.rs @@ -0,0 +1,1198 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Collective system: Members of a set of account IDs can make their collective feelings known +//! through dispatched calls from one of two specialized origins. +//! +//! The membership can be provided in one of two ways: either directly, using the Root-dispatchable +//! function `set_members`, or indirectly, through implementing the `ChangeMembers`. +//! The pallet assumes that the amount of members stays at or below `MaxMembers` for its weight +//! calculations, but enforces this neither in `set_members` nor in `change_members_sorted`. +//! +//! A "prime" member may be set to help determine the default vote behavior based on chain +//! config. If `PrimeDefaultVote` is used, the prime vote acts as the default vote in case of any +//! abstentions after the voting period. If `MoreThanMajorityThenPrimeDefaultVote` is used, then +//! abstentions will first follow the majority of the collective voting, and then the prime +//! member. +//! +//! Voting happens through motions comprising a proposal (i.e. a curried dispatchable) plus a +//! number of approvals required for it to pass and be called. Motions are open for members to +//! vote on for a minimum period given by `MotionDuration`. As soon as the needed number of +//! approvals is given, the motion is closed and executed. If the number of approvals is not reached +//! during the voting period, then `close` may be called by any account in order to force the end +//! the motion explicitly. If a prime member is defined then their vote is used in place of any +//! abstentions and the proposal is executed if there are enough approvals counting the new votes. +//! +//! If there are not, or if no prime is set, then the motion is dropped without being executed. + +#![cfg_attr(not(feature = "std"), no_std)] +#![recursion_limit = "128"] + +use scale_info::TypeInfo; +use sp_io::storage; +use sp_runtime::{traits::Hash, RuntimeDebug}; +use sp_std::{marker::PhantomData, prelude::*, result}; + +use frame_support::{ + codec::{Decode, Encode, MaxEncodedLen}, + dispatch::{ + DispatchError, DispatchResultWithPostInfo, Dispatchable, GetDispatchInfo, Pays, + PostDispatchInfo + }, + ensure, + traits::{ + Backing, ChangeMembers, EnsureOrigin, Get, GetBacking, InitializeMembers, StorageVersion, + }, + weights::{OldWeight, Weight}, +}; + +#[cfg(test)] +mod tests; + +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking; +pub mod weights; + +pub use pallet::*; +pub use weights::WeightInfo; + +const LOG_TARGET: &str = "runtime::collective"; + +/// Simple index type for proposal counting. +pub type ProposalIndex = u32; + +/// A number of members. +/// +/// This also serves as a number of voting members, and since for motions, each member may +/// vote exactly once, therefore also the number of votes for any given motion. +pub type MemberCount = u32; + +/// Default voting strategy when a member is inactive. +pub trait DefaultVote { + /// Get the default voting strategy, given: + /// + /// - Whether the prime member voted Aye. + /// - Raw number of yes votes. + /// - Raw number of no votes. + /// - Total number of member count. + fn default_vote( + prime_vote: Option, + yes_votes: MemberCount, + no_votes: MemberCount, + len: MemberCount, + ) -> bool; +} + +/// Set the prime member's vote as the default vote. +pub struct PrimeDefaultVote; + +impl DefaultVote for PrimeDefaultVote { + fn default_vote( + prime_vote: Option, + _yes_votes: MemberCount, + _no_votes: MemberCount, + _len: MemberCount, + ) -> bool { + prime_vote.unwrap_or(false) + } +} + +/// First see if yes vote are over majority of the whole collective. If so, set the default vote +/// as yes. Otherwise, use the prime member's vote as the default vote. +pub struct MoreThanMajorityThenPrimeDefaultVote; + +impl DefaultVote for MoreThanMajorityThenPrimeDefaultVote { + fn default_vote( + prime_vote: Option, + yes_votes: MemberCount, + _no_votes: MemberCount, + len: MemberCount, + ) -> bool { + let more_than_majority = yes_votes * 2 > len; + more_than_majority || prime_vote.unwrap_or(false) + } +} + +/// Origin for the collective module. +#[derive(PartialEq, Eq, Clone, RuntimeDebug, Encode, Decode, TypeInfo, MaxEncodedLen)] +#[scale_info(skip_type_params(I))] +#[codec(mel_bound(AccountId: MaxEncodedLen))] +pub enum RawOrigin { + /// It has been condoned by a given number of members of the collective from a given total. + Members(MemberCount, MemberCount), + /// It has been condoned by a single member of the collective. + Member(AccountId), + /// Dummy to manage the fact we have instancing. + _Phantom(PhantomData), +} + +impl GetBacking for RawOrigin { + fn get_backing(&self) -> Option { + match self { + RawOrigin::Members(n, d) => Some(Backing { approvals: *n, eligible: *d }), + _ => None, + } + } +} + +/// Info for keeping track of a motion being voted on. +#[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, TypeInfo)] +pub struct Votes { + /// The proposal's unique index. + index: ProposalIndex, + /// The number of approval votes that are needed to pass the motion. + threshold: MemberCount, + /// The current set of voters that approved it. + ayes: Vec, + /// The current set of voters that rejected it. + nays: Vec, + /// The hard end time of this vote. + end: BlockNumber, +} + +#[frame_support::pallet] +pub mod pallet { + use super::*; + use frame_support::pallet_prelude::*; + use frame_system::pallet_prelude::*; + + /// The current storage version. + const STORAGE_VERSION: StorageVersion = StorageVersion::new(4); + + #[pallet::pallet] + #[pallet::generate_store(pub(super) trait Store)] + #[pallet::storage_version(STORAGE_VERSION)] + #[pallet::without_storage_info] + pub struct Pallet(PhantomData<(T, I)>); + + #[pallet::config] + pub trait Config: frame_system::Config { + /// The runtime origin type. + type RuntimeOrigin: From>; + + /// The runtime call dispatch type. + type Proposal: Parameter + + Dispatchable< + RuntimeOrigin = >::RuntimeOrigin, + PostInfo = PostDispatchInfo, + > + From> + + GetDispatchInfo; + + /// The runtime event type. + type RuntimeEvent: From> + + IsType<::RuntimeEvent>; + + /// The time-out for council motions. + type MotionDuration: Get; + + /// Maximum number of proposals allowed to be active in parallel. + type MaxProposals: Get; + + /// The maximum number of members supported by the pallet. Used for weight estimation. + /// + /// NOTE: + /// + Benchmarks will need to be re-run and weights adjusted if this changes. + /// + This pallet assumes that dependents keep to the limit without enforcing it. + type MaxMembers: Get; + + /// Default vote strategy of this collective. + type DefaultVote: DefaultVote; + + /// Weight information for extrinsics in this pallet. + type WeightInfo: WeightInfo; + + /// Origin allowed to set collective members + type SetMembersOrigin: EnsureOrigin<::RuntimeOrigin>; + + /// Origin allowed to propose + type CanPropose: CanPropose; + + /// Origin allowed to vote + type CanVote: CanVote; + + /// Members to expect in a vote + type GetVotingMembers: GetVotingMembers; + } + + #[pallet::genesis_config] + pub struct GenesisConfig, I: 'static = ()> { + pub phantom: PhantomData, + pub members: Vec, + } + + #[cfg(feature = "std")] + impl, I: 'static> Default for GenesisConfig { + fn default() -> Self { + Self { phantom: Default::default(), members: Default::default() } + } + } + + #[pallet::genesis_build] + impl, I: 'static> GenesisBuild for GenesisConfig { + fn build(&self) { + use sp_std::collections::btree_set::BTreeSet; + let members_set: BTreeSet<_> = self.members.iter().collect(); + assert_eq!( + members_set.len(), + self.members.len(), + "Members cannot contain duplicate accounts." + ); + + Pallet::::initialize_members(&self.members) + } + } + + /// Origin for the collective pallet. + #[pallet::origin] + pub type Origin = RawOrigin<::AccountId, I>; + + /// The hashes of the active proposals. + #[pallet::storage] + #[pallet::getter(fn proposals)] + pub type Proposals, I: 'static = ()> = + StorageValue<_, BoundedVec, ValueQuery>; + + /// Actual proposal for a given hash, if it's current. + #[pallet::storage] + #[pallet::getter(fn proposal_of)] + pub type ProposalOf, I: 'static = ()> = + StorageMap<_, Identity, T::Hash, >::Proposal, OptionQuery>; + + /// Votes on a given proposal, if it is ongoing. + #[pallet::storage] + #[pallet::getter(fn voting)] + pub type Voting, I: 'static = ()> = + StorageMap<_, Identity, T::Hash, Votes, OptionQuery>; + + /// Proposals so far. + #[pallet::storage] + #[pallet::getter(fn proposal_count)] + pub type ProposalCount, I: 'static = ()> = StorageValue<_, u32, ValueQuery>; + + /// The current members of the collective. This is stored sorted (just by value). + #[pallet::storage] + #[pallet::getter(fn members)] + pub type Members, I: 'static = ()> = + StorageValue<_, Vec, ValueQuery>; + + /// The prime member that helps determine the default vote behavior in case of absentations. + #[pallet::storage] + #[pallet::getter(fn prime)] + pub type Prime, I: 'static = ()> = StorageValue<_, T::AccountId, OptionQuery>; + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event, I: 'static = ()> { + /// A motion (given hash) has been proposed (by given account) with a threshold (given + /// `MemberCount`). + Proposed { + account: T::AccountId, + proposal_index: ProposalIndex, + proposal_hash: T::Hash, + threshold: MemberCount, + }, + /// A motion (given hash) has been voted on by given account, leaving + /// a tally (yes votes and no votes given respectively as `MemberCount`). + Voted { + account: T::AccountId, + proposal_hash: T::Hash, + voted: bool, + yes: MemberCount, + no: MemberCount, + }, + /// A motion was approved by the required threshold. + Approved { proposal_hash: T::Hash }, + /// A motion was not approved by the required threshold. + Disapproved { proposal_hash: T::Hash }, + /// A motion was executed; result will be `Ok` if it returned without error. + Executed { proposal_hash: T::Hash, result: DispatchResult }, + /// A single member did some action; result will be `Ok` if it returned without error. + MemberExecuted { proposal_hash: T::Hash, result: DispatchResult }, + /// A proposal was closed because its threshold was reached or after its duration was up. + Closed { proposal_hash: T::Hash, yes: MemberCount, no: MemberCount }, + } + + #[pallet::error] + pub enum Error { + /// Account is not a member + NotMember, + /// Duplicate proposals not allowed + DuplicateProposal, + /// Proposal must exist + ProposalMissing, + /// Mismatched index + WrongIndex, + /// Duplicate vote ignored + DuplicateVote, + /// Members are already initialized! + AlreadyInitialized, + /// The close call was made too early, before the end of the voting. + TooEarly, + /// There can only be a maximum of `MaxProposals` active proposals. + TooManyProposals, + /// The given weight bound for the proposal was too low. + WrongProposalWeight, + /// The given length bound for the proposal was too low. + WrongProposalLength, + } + + // Note that councillor operations are assigned to the operational class. + #[pallet::call] + impl, I: 'static> Pallet { + /// Set the collective's membership. + /// + /// - `new_members`: The new member list. Be nice to the chain and provide it sorted. + /// - `prime`: The prime member whose vote sets the default. + /// - `old_count`: The upper bound for the previous number of members in storage. Used for + /// weight estimation. + /// + /// The dispatch of this call must be `SetMembersOrigin`. + /// + /// NOTE: Does not enforce the expected `MaxMembers` limit on the amount of members, but + /// the weight estimations rely on it to estimate dispatchable weight. + /// + /// # WARNING: + /// + /// The `pallet-collective` can also be managed by logic outside of the pallet through the + /// implementation of the trait [`ChangeMembers`]. + /// Any call to `set_members` must be careful that the member set doesn't get out of sync + /// with other logic managing the member set. + /// + /// ## Complexity: + /// - `O(MP + N)` where: + /// - `M` old-members-count (code- and governance-bounded) + /// - `N` new-members-count (code- and governance-bounded) + /// - `P` proposals-count (code-bounded) + #[pallet::call_index(0)] + #[pallet::weight(( + T::WeightInfo::set_members( + *old_count, // M + new_members.len() as u32, // N + T::MaxProposals::get() // P + ), + DispatchClass::Operational + ))] + pub fn set_members( + origin: OriginFor, + new_members: Vec, + prime: Option, + old_count: MemberCount, + ) -> DispatchResultWithPostInfo { + T::SetMembersOrigin::ensure_origin(origin)?; + if new_members.len() > T::MaxMembers::get() as usize { + log::error!( + target: LOG_TARGET, + "New members count ({}) exceeds maximum amount of members expected ({}).", + new_members.len(), + T::MaxMembers::get(), + ); + } + + let old = Members::::get(); + if old.len() > old_count as usize { + log::warn!( + target: LOG_TARGET, + "Wrong count used to estimate set_members weight. expected ({}) vs actual ({})", + old_count, + old.len(), + ); + } + let mut new_members = new_members; + new_members.sort(); + >::set_members_sorted(&new_members, &old); + Prime::::set(prime); + + Ok(Some(T::WeightInfo::set_members( + old.len() as u32, // M + new_members.len() as u32, // N + T::MaxProposals::get(), // P + )) + .into()) + } + + /// Dispatch a proposal from a member using the `Member` origin. + /// + /// Origin must be a member of the collective. + /// + /// ## Complexity: + /// - `O(B + M + P)` where: + /// - `B` is `proposal` size in bytes (length-fee-bounded) + /// - `M` members-count (code-bounded) + /// - `P` complexity of dispatching `proposal` + #[pallet::call_index(1)] + #[pallet::weight(( + T::WeightInfo::execute( + *length_bound, // B + T::MaxMembers::get(), // M + ).saturating_add(proposal.get_dispatch_info().weight), // P + DispatchClass::Operational + ))] + pub fn execute( + origin: OriginFor, + proposal: Box<>::Proposal>, + #[pallet::compact] length_bound: u32, + ) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + let members = Self::members(); + ensure!(members.contains(&who), Error::::NotMember); + + let proposal_len = proposal.encoded_size(); + ensure!(proposal_len <= length_bound as usize, Error::::WrongProposalLength); + + let proposal_hash = T::Hashing::hash_of(&proposal); + let result = proposal.dispatch(RawOrigin::Member(who).into()); + Self::deposit_event(Event::MemberExecuted { + proposal_hash, + result: result.map(|_| ()).map_err(|e| e.error), + }); + + Ok(get_result_weight(result) + .map(|w| { + T::WeightInfo::execute( + proposal_len as u32, // B + members.len() as u32, // M + ) + .saturating_add(w) // P + }) + .into()) + } + + /// Add a new proposal to either be voted on or executed directly. + /// + /// Requires the sender to be member. + /// + /// `threshold` determines whether `proposal` is executed directly (`threshold < 2`) + /// or put up for voting. + /// + /// ## Complexity + /// - `O(B + M + P1)` or `O(B + M + P2)` where: + /// - `B` is `proposal` size in bytes (length-fee-bounded) + /// - `M` is members-count (code- and governance-bounded) + /// - branching is influenced by `threshold` where: + /// - `P1` is proposal execution complexity (`threshold < 2`) + /// - `P2` is proposals-count (code-bounded) (`threshold >= 2`) + #[pallet::call_index(2)] + #[pallet::weight(( + T::WeightInfo::propose_proposed( + *length_bound, // B + T::MaxMembers::get(), // M + T::MaxProposals::get(), // P2 + ), + DispatchClass::Operational + ))] + pub fn propose( + origin: OriginFor, + proposal: Box<>::Proposal>, + #[pallet::compact] length_bound: u32, + ) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin.clone())?; + ensure!(T::CanPropose::can_propose(&who), Error::::NotMember); + + let threshold = (T::GetVotingMembers::get_count() / 2) + 1; + + let members = Self::members(); + let (proposal_len, active_proposals) = + Self::do_propose_proposed(who, threshold, proposal, length_bound)?; + + Ok(Some(T::WeightInfo::propose_proposed( + proposal_len as u32, // B + members.len() as u32, // M + active_proposals, // P2 + )) + .into()) + } + + /// Add an aye or nay vote for the sender to the given proposal. + /// + /// Requires the sender to be a member. + /// + /// Transaction fees will be waived if the member is voting on any particular proposal + /// for the first time and the call is successful. Subsequent vote changes will charge a + /// fee. + /// ## Complexity + /// - `O(M)` where `M` is members-count (code- and governance-bounded) + #[pallet::call_index(3)] + #[pallet::weight((T::WeightInfo::vote(T::MaxMembers::get()), DispatchClass::Operational))] + pub fn vote( + origin: OriginFor, + proposal: T::Hash, + #[pallet::compact] index: ProposalIndex, + approve: bool, + ) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin.clone())?; + ensure!(T::CanVote::can_vote(&who), Error::::NotMember); + + let members = Self::members(); + // Detects first vote of the member in the motion + let is_account_voting_first_time = Self::do_vote(who, proposal, index, approve)?; + + if is_account_voting_first_time { + Ok((Some(T::WeightInfo::vote(members.len() as u32)), Pays::No).into()) + } else { + Ok((Some(T::WeightInfo::vote(members.len() as u32)), Pays::Yes).into()) + } + } + + /// Close a vote that is either approved, disapproved or whose voting period has ended. + /// + /// May be called by any signed account in order to finish voting and close the proposal. + /// + /// If called before the end of the voting period it will only close the vote if it is + /// has enough votes to be approved or disapproved. + /// + /// If called after the end of the voting period abstentions are counted as rejections + /// unless there is a prime member set and the prime member cast an approval. + /// + /// If the close operation completes successfully with disapproval, the transaction fee will + /// be waived. Otherwise execution of the approved operation will be charged to the caller. + /// + /// + `proposal_weight_bound`: The maximum amount of weight consumed by executing the closed + /// proposal. + /// + `length_bound`: The upper bound for the length of the proposal in storage. Checked via + /// `storage::read` so it is `size_of::() == 4` larger than the pure length. + /// + /// ## Complexity + /// - `O(B + M + P1 + P2)` where: + /// - `B` is `proposal` size in bytes (length-fee-bounded) + /// - `M` is members-count (code- and governance-bounded) + /// - `P1` is the complexity of `proposal` preimage. + /// - `P2` is proposal-count (code-bounded) + #[pallet::call_index(4)] + #[pallet::weight(( + { + let b = *length_bound; + let m = T::MaxMembers::get(); + let p1 = *proposal_weight_bound; + let p2 = T::MaxProposals::get(); + T::WeightInfo::close_early_approved(b, m, p2) + .max(T::WeightInfo::close_early_disapproved(m, p2)) + .max(T::WeightInfo::close_approved(b, m, p2)) + .max(T::WeightInfo::close_disapproved(m, p2)) + .saturating_add(p1.into()) + }, + DispatchClass::Operational + ))] + #[allow(deprecated)] + #[deprecated(note = "1D weight is used in this extrinsic, please migrate to `close`")] + pub fn close_old_weight( + origin: OriginFor, + proposal_hash: T::Hash, + #[pallet::compact] index: ProposalIndex, + #[pallet::compact] proposal_weight_bound: OldWeight, + #[pallet::compact] length_bound: u32, + ) -> DispatchResultWithPostInfo { + let proposal_weight_bound: Weight = proposal_weight_bound.into(); + let _ = ensure_signed(origin)?; + + Self::do_close(proposal_hash, index, proposal_weight_bound, length_bound) + } + + /// Disapprove a proposal, close, and remove it from the system, regardless of its current + /// state. + /// + /// Must be called by the Root origin. + /// + /// Parameters: + /// * `proposal_hash`: The hash of the proposal that should be disapproved. + /// + /// ## Complexity + /// O(P) where P is the number of max proposals + #[pallet::call_index(5)] + #[pallet::weight(T::WeightInfo::disapprove_proposal(T::MaxProposals::get()))] + pub fn disapprove_proposal( + origin: OriginFor, + proposal_hash: T::Hash, + ) -> DispatchResultWithPostInfo { + ensure_root(origin)?; + let proposal_count = Self::do_disapprove_proposal(proposal_hash); + Ok(Some(T::WeightInfo::disapprove_proposal(proposal_count)).into()) + } + + /// Close a vote that is either approved, disapproved or whose voting period has ended. + /// + /// May be called by any signed account in order to finish voting and close the proposal. + /// + /// If called before the end of the voting period it will only close the vote if it is + /// has enough votes to be approved or disapproved. + /// + /// If called after the end of the voting period abstentions are counted as rejections + /// unless there is a prime member set and the prime member cast an approval. + /// + /// If the close operation completes successfully with disapproval, the transaction fee will + /// be waived. Otherwise execution of the approved operation will be charged to the caller. + /// + /// + `proposal_weight_bound`: The maximum amount of weight consumed by executing the closed + /// proposal. + /// + `length_bound`: The upper bound for the length of the proposal in storage. Checked via + /// `storage::read` so it is `size_of::() == 4` larger than the pure length. + /// + /// ## Complexity + /// - `O(B + M + P1 + P2)` where: + /// - `B` is `proposal` size in bytes (length-fee-bounded) + /// - `M` is members-count (code- and governance-bounded) + /// - `P1` is the complexity of `proposal` preimage. + /// - `P2` is proposal-count (code-bounded) + #[pallet::call_index(6)] + #[pallet::weight(( + { + let b = *length_bound; + let m = T::MaxMembers::get(); + let p1 = *proposal_weight_bound; + let p2 = T::MaxProposals::get(); + T::WeightInfo::close_early_approved(b, m, p2) + .max(T::WeightInfo::close_early_disapproved(m, p2)) + .max(T::WeightInfo::close_approved(b, m, p2)) + .max(T::WeightInfo::close_disapproved(m, p2)) + .saturating_add(p1) + }, + DispatchClass::Operational + ))] + pub fn close( + origin: OriginFor, + proposal_hash: T::Hash, + #[pallet::compact] index: ProposalIndex, + proposal_weight_bound: Weight, + #[pallet::compact] length_bound: u32, + ) -> DispatchResultWithPostInfo { + let _ = ensure_signed(origin)?; + + Self::do_close(proposal_hash, index, proposal_weight_bound, length_bound) + } + } +} + +/// Return the weight of a dispatch call result as an `Option`. +/// +/// Will return the weight regardless of what the state of the result is. +fn get_result_weight(result: DispatchResultWithPostInfo) -> Option { + match result { + Ok(post_info) => post_info.actual_weight, + Err(err) => err.post_info.actual_weight, + } +} + +impl, I: 'static> Pallet { + /// Check whether `who` is a member of the collective. + pub fn is_member(who: &T::AccountId) -> bool { + // Note: The dispatchables *do not* use this to check membership so make sure + // to update those if this is changed. + Self::members().contains(who) + } + + /// Execute immediately when adding a new proposal. + pub fn do_propose_execute( + proposal: Box<>::Proposal>, + length_bound: MemberCount, + ) -> Result<(u32, DispatchResultWithPostInfo), DispatchError> { + let proposal_len = proposal.encoded_size(); + ensure!(proposal_len <= length_bound as usize, Error::::WrongProposalLength); + + let proposal_hash = T::Hashing::hash_of(&proposal); + ensure!(!>::contains_key(proposal_hash), Error::::DuplicateProposal); + + let seats = Self::members().len() as MemberCount; + let result = proposal.dispatch(RawOrigin::Members(1, seats).into()); + Self::deposit_event(Event::Executed { + proposal_hash, + result: result.map(|_| ()).map_err(|e| e.error), + }); + Ok((proposal_len as u32, result)) + } + + /// Add a new proposal to be voted. + pub fn do_propose_proposed( + who: T::AccountId, + threshold: MemberCount, + proposal: Box<>::Proposal>, + length_bound: MemberCount, + ) -> Result<(u32, u32), DispatchError> { + let proposal_len = proposal.encoded_size(); + ensure!(proposal_len <= length_bound as usize, Error::::WrongProposalLength); + + let proposal_hash = T::Hashing::hash_of(&proposal); + ensure!(!>::contains_key(proposal_hash), Error::::DuplicateProposal); + + let active_proposals = + >::try_mutate(|proposals| -> Result { + proposals.try_push(proposal_hash).map_err(|_| Error::::TooManyProposals)?; + Ok(proposals.len()) + })?; + + let index = Self::proposal_count(); + >::mutate(|i| *i += 1); + >::insert(proposal_hash, proposal); + let votes = { + let end = frame_system::Pallet::::block_number() + T::MotionDuration::get(); + Votes { index, threshold, ayes: vec![], nays: vec![], end } + }; + >::insert(proposal_hash, votes); + + Self::deposit_event(Event::Proposed { + account: who, + proposal_index: index, + proposal_hash, + threshold, + }); + Ok((proposal_len as u32, active_proposals as u32)) + } + + /// Add an aye or nay vote for the member to the given proposal, returns true if it's the first + /// vote of the member in the motion + pub fn do_vote( + who: T::AccountId, + proposal: T::Hash, + index: ProposalIndex, + approve: bool, + ) -> Result { + let mut voting = Self::voting(&proposal).ok_or(Error::::ProposalMissing)?; + ensure!(voting.index == index, Error::::WrongIndex); + + let position_yes = voting.ayes.iter().position(|a| a == &who); + let position_no = voting.nays.iter().position(|a| a == &who); + + // Detects first vote of the member in the motion + let is_account_voting_first_time = position_yes.is_none() && position_no.is_none(); + + if approve { + if position_yes.is_none() { + voting.ayes.push(who.clone()); + } else { + return Err(Error::::DuplicateVote.into()) + } + if let Some(pos) = position_no { + voting.nays.swap_remove(pos); + } + } else { + if position_no.is_none() { + voting.nays.push(who.clone()); + } else { + return Err(Error::::DuplicateVote.into()) + } + if let Some(pos) = position_yes { + voting.ayes.swap_remove(pos); + } + } + + let yes_votes = voting.ayes.len() as MemberCount; + let no_votes = voting.nays.len() as MemberCount; + Self::deposit_event(Event::Voted { + account: who, + proposal_hash: proposal, + voted: approve, + yes: yes_votes, + no: no_votes, + }); + + Voting::::insert(&proposal, voting); + + Ok(is_account_voting_first_time) + } + + /// Close a vote that is either approved, disapproved or whose voting period has ended. + pub fn do_close( + proposal_hash: T::Hash, + index: ProposalIndex, + proposal_weight_bound: Weight, + length_bound: u32, + ) -> DispatchResultWithPostInfo { + let voting = Self::voting(&proposal_hash).ok_or(Error::::ProposalMissing)?; + ensure!(voting.index == index, Error::::WrongIndex); + + let mut no_votes = voting.nays.len() as MemberCount; + let mut yes_votes = voting.ayes.len() as MemberCount; + let seats = T::GetVotingMembers::get_count() as MemberCount; + let approved = yes_votes >= voting.threshold; + let disapproved = seats.saturating_sub(no_votes) < voting.threshold; + // Allow (dis-)approving the proposal as soon as there are enough votes. + if approved { + let (proposal, len) = Self::validate_and_get_proposal( + &proposal_hash, + length_bound, + proposal_weight_bound, + )?; + Self::deposit_event(Event::Closed { proposal_hash, yes: yes_votes, no: no_votes }); + let (proposal_weight, proposal_count) = + Self::do_approve_proposal(seats, yes_votes, proposal_hash, proposal); + return Ok(( + Some( + T::WeightInfo::close_early_approved(len as u32, seats, proposal_count) + .saturating_add(proposal_weight), + ), + Pays::Yes, + ) + .into()) + } else if disapproved { + Self::deposit_event(Event::Closed { proposal_hash, yes: yes_votes, no: no_votes }); + let proposal_count = Self::do_disapprove_proposal(proposal_hash); + return Ok(( + Some(T::WeightInfo::close_early_disapproved(seats, proposal_count)), + Pays::No, + ) + .into()) + } + + // Only allow actual closing of the proposal after the voting period has ended. + ensure!(frame_system::Pallet::::block_number() >= voting.end, Error::::TooEarly); + + let prime_vote = Self::prime().map(|who| voting.ayes.iter().any(|a| a == &who)); + + // default voting strategy. + let default = T::DefaultVote::default_vote(prime_vote, yes_votes, no_votes, seats); + + let abstentions = seats - (yes_votes + no_votes); + match default { + true => yes_votes += abstentions, + false => no_votes += abstentions, + } + let approved = yes_votes >= voting.threshold; + + if approved { + let (proposal, len) = Self::validate_and_get_proposal( + &proposal_hash, + length_bound, + proposal_weight_bound, + )?; + Self::deposit_event(Event::Closed { proposal_hash, yes: yes_votes, no: no_votes }); + let (proposal_weight, proposal_count) = + Self::do_approve_proposal(seats, yes_votes, proposal_hash, proposal); + Ok(( + Some( + T::WeightInfo::close_approved(len as u32, seats, proposal_count) + .saturating_add(proposal_weight), + ), + Pays::Yes, + ) + .into()) + } else { + Self::deposit_event(Event::Closed { proposal_hash, yes: yes_votes, no: no_votes }); + let proposal_count = Self::do_disapprove_proposal(proposal_hash); + Ok((Some(T::WeightInfo::close_disapproved(seats, proposal_count)), Pays::No).into()) + } + } + + /// Ensure that the right proposal bounds were passed and get the proposal from storage. + /// + /// Checks the length in storage via `storage::read` which adds an extra `size_of::() == 4` + /// to the length. + fn validate_and_get_proposal( + hash: &T::Hash, + length_bound: u32, + weight_bound: Weight, + ) -> Result<(>::Proposal, usize), DispatchError> { + let key = ProposalOf::::hashed_key_for(hash); + // read the length of the proposal storage entry directly + let proposal_len = + storage::read(&key, &mut [0; 0], 0).ok_or(Error::::ProposalMissing)?; + ensure!(proposal_len <= length_bound, Error::::WrongProposalLength); + let proposal = ProposalOf::::get(hash).ok_or(Error::::ProposalMissing)?; + let proposal_weight = proposal.get_dispatch_info().weight; + ensure!(proposal_weight.all_lte(weight_bound), Error::::WrongProposalWeight); + Ok((proposal, proposal_len as usize)) + } + + /// Weight: + /// If `approved`: + /// - the weight of `proposal` preimage. + /// - two events deposited. + /// - two removals, one mutation. + /// - computation and i/o `O(P + L)` where: + /// - `P` is number of active proposals, + /// - `L` is the encoded length of `proposal` preimage. + /// + /// If not `approved`: + /// - one event deposited. + /// Two removals, one mutation. + /// Computation and i/o `O(P)` where: + /// - `P` is number of active proposals + fn do_approve_proposal( + seats: MemberCount, + yes_votes: MemberCount, + proposal_hash: T::Hash, + proposal: >::Proposal, + ) -> (Weight, u32) { + Self::deposit_event(Event::Approved { proposal_hash }); + + let dispatch_weight = proposal.get_dispatch_info().weight; + let origin = RawOrigin::Members(yes_votes, seats).into(); + let result = proposal.dispatch(origin); + Self::deposit_event(Event::Executed { + proposal_hash, + result: result.map(|_| ()).map_err(|e| e.error), + }); + // default to the dispatch info weight for safety + let proposal_weight = get_result_weight(result).unwrap_or(dispatch_weight); // P1 + + let proposal_count = Self::remove_proposal(proposal_hash); + (proposal_weight, proposal_count) + } + + /// Removes a proposal from the pallet, and deposit the `Disapproved` event. + pub fn do_disapprove_proposal(proposal_hash: T::Hash) -> u32 { + // disapproved + Self::deposit_event(Event::Disapproved { proposal_hash }); + Self::remove_proposal(proposal_hash) + } + + // Removes a proposal from the pallet, cleaning up votes and the vector of proposals. + fn remove_proposal(proposal_hash: T::Hash) -> u32 { + // remove proposal and vote + ProposalOf::::remove(&proposal_hash); + Voting::::remove(&proposal_hash); + let num_proposals = Proposals::::mutate(|proposals| { + proposals.retain(|h| h != &proposal_hash); + proposals.len() + 1 // calculate weight based on original length + }); + num_proposals as u32 + } + + pub fn remove_votes(who: &T::AccountId) -> Result { + for h in Self::proposals().into_iter() { + >::mutate(h, |v| { + if let Some(mut votes) = v.take() { + votes.ayes = votes + .ayes + .into_iter() + .filter(|i| i != who) + .collect(); + votes.nays = votes + .nays + .into_iter() + .filter(|i| i != who) + .collect(); + *v = Some(votes); + } + }); + } + + Ok(true) + } + + pub fn has_voted(proposal: T::Hash, index: ProposalIndex, who: &T::AccountId) -> Result { + let voting = Self::voting(&proposal).ok_or(Error::::ProposalMissing)?; + ensure!(voting.index == index, Error::::WrongIndex); + + let position_yes = voting.ayes.iter().position(|a| a == who); + let position_no = voting.nays.iter().position(|a| a == who); + + Ok(position_yes.is_some() || position_no.is_some()) + } +} + +impl, I: 'static> ChangeMembers for Pallet { + /// Update the members of the collective. Votes are updated and the prime is reset. + /// + /// NOTE: Does not enforce the expected `MaxMembers` limit on the amount of members, but + /// the weight estimations rely on it to estimate dispatchable weight. + /// + /// ## Complexity + /// - `O(MP + N)` + /// - where `M` old-members-count (governance-bounded) + /// - where `N` new-members-count (governance-bounded) + /// - where `P` proposals-count + fn change_members_sorted( + _incoming: &[T::AccountId], + outgoing: &[T::AccountId], + new: &[T::AccountId], + ) { + if new.len() > T::MaxMembers::get() as usize { + log::error!( + target: LOG_TARGET, + "New members count ({}) exceeds maximum amount of members expected ({}).", + new.len(), + T::MaxMembers::get(), + ); + } + // remove accounts from all current voting in motions. + let mut outgoing = outgoing.to_vec(); + outgoing.sort(); + for h in Self::proposals().into_iter() { + >::mutate(h, |v| { + if let Some(mut votes) = v.take() { + votes.ayes = votes + .ayes + .into_iter() + .filter(|i| outgoing.binary_search(i).is_err()) + .collect(); + votes.nays = votes + .nays + .into_iter() + .filter(|i| outgoing.binary_search(i).is_err()) + .collect(); + *v = Some(votes); + } + }); + } + Members::::put(new); + Prime::::kill(); + } + + fn set_prime(prime: Option) { + Prime::::set(prime); + } + + fn get_prime() -> Option { + Prime::::get() + } +} + +impl, I: 'static> InitializeMembers for Pallet { + fn initialize_members(members: &[T::AccountId]) { + if !members.is_empty() { + assert!(>::get().is_empty(), "Members are already initialized!"); + >::put(members); + } + } +} + +/// Ensure that the origin `o` represents at least `n` members. Returns `Ok` or an `Err` +/// otherwise. +pub fn ensure_members( + o: OuterOrigin, + n: MemberCount, +) -> result::Result +where + OuterOrigin: Into, OuterOrigin>>, +{ + match o.into() { + Ok(RawOrigin::Members(x, _)) if x >= n => Ok(n), + _ => Err("bad origin: expected to be a threshold number of members"), + } +} + +pub struct EnsureMember(PhantomData<(AccountId, I)>); +impl< + O: Into, O>> + From>, + I, + AccountId: Decode, + > EnsureOrigin for EnsureMember +{ + type Success = AccountId; + fn try_origin(o: O) -> Result { + o.into().and_then(|o| match o { + RawOrigin::Member(id) => Ok(id), + r => Err(O::from(r)), + }) + } + + #[cfg(feature = "runtime-benchmarks")] + fn try_successful_origin() -> Result { + let zero_account_id = + AccountId::decode(&mut sp_runtime::traits::TrailingZeroInput::zeroes()) + .expect("infinite length input; no invalid inputs for type; qed"); + Ok(O::from(RawOrigin::Member(zero_account_id))) + } +} + +pub struct EnsureMembers(PhantomData<(AccountId, I)>); +impl< + O: Into, O>> + From>, + AccountId, + I, + const N: u32, + > EnsureOrigin for EnsureMembers +{ + type Success = (MemberCount, MemberCount); + fn try_origin(o: O) -> Result { + o.into().and_then(|o| match o { + RawOrigin::Members(n, m) if n >= N => Ok((n, m)), + r => Err(O::from(r)), + }) + } + + #[cfg(feature = "runtime-benchmarks")] + fn try_successful_origin() -> Result { + Ok(O::from(RawOrigin::Members(N, N))) + } +} + +pub struct EnsureProportionMoreThan( + PhantomData<(AccountId, I)>, +); +impl< + O: Into, O>> + From>, + AccountId, + I, + const N: u32, + const D: u32, + > EnsureOrigin for EnsureProportionMoreThan +{ + type Success = (); + fn try_origin(o: O) -> Result { + o.into().and_then(|o| match o { + RawOrigin::Members(n, m) if n * D > N * m => Ok(()), + r => Err(O::from(r)), + }) + } + + #[cfg(feature = "runtime-benchmarks")] + fn try_successful_origin() -> Result { + Ok(O::from(RawOrigin::Members(1u32, 0u32))) + } +} + +pub struct EnsureProportionAtLeast( + PhantomData<(AccountId, I)>, +); +impl< + O: Into, O>> + From>, + AccountId, + I, + const N: u32, + const D: u32, + > EnsureOrigin for EnsureProportionAtLeast +{ + type Success = (); + fn try_origin(o: O) -> Result { + o.into().and_then(|o| match o { + RawOrigin::Members(n, m) if n * D >= N * m => Ok(()), + r => Err(O::from(r)), + }) + } + + #[cfg(feature = "runtime-benchmarks")] + fn try_successful_origin() -> Result { + Ok(O::from(RawOrigin::Members(0u32, 0u32))) + } +} + +/// CanPropose +pub trait CanPropose { + /// Check whether or not the passed AccountId can propose a new motion + fn can_propose(account: &AccountId) -> bool; +} + +impl CanPropose for () { + fn can_propose(_: &T) -> bool {false} +} + +/// CanVote +pub trait CanVote { + /// Check whether or not the passed AccountId can vote on a motion + fn can_vote(account: &AccountId) -> bool; +} + +impl CanVote for () { + fn can_vote(_: &T) -> bool {false} +} + +pub trait GetVotingMembers { + fn get_count() -> MemberCount; +} + +impl GetVotingMembers for () { + fn get_count() -> MemberCount {0} +} \ No newline at end of file diff --git a/pallets/collective/src/tests.rs b/pallets/collective/src/tests.rs new file mode 100644 index 000000000..3f186543d --- /dev/null +++ b/pallets/collective/src/tests.rs @@ -0,0 +1,1409 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::{Event as CollectiveEvent, *}; +use crate as pallet_collective; +use frame_support::{ + assert_noop, assert_ok, + dispatch::Pays, + parameter_types, + traits::{ConstU32, ConstU64, GenesisBuild, StorageVersion}, + Hashable, +}; +use frame_system::{EnsureRoot, EventRecord, Phase}; +use sp_core::H256; +use sp_runtime::{ + testing::Header, + traits::{BlakeTwo256, IdentityLookup}, + BuildStorage, +}; + +pub type Block = sp_runtime::generic::Block; +pub type UncheckedExtrinsic = sp_runtime::generic::UncheckedExtrinsic; + +frame_support::construct_runtime!( + pub enum Test where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic + { + System: frame_system::{Pallet, Call, Event}, + Collective: pallet_collective::::{Pallet, Call, Event, Origin, Config}, + CollectiveMajority: pallet_collective::::{Pallet, Call, Event, Origin, Config}, + DefaultCollective: pallet_collective::{Pallet, Call, Event, Origin, Config}, + Democracy: mock_democracy::{Pallet, Call, Event}, + } +); + +mod mock_democracy { + pub use pallet::*; + #[frame_support::pallet] + pub mod pallet { + use frame_support::pallet_prelude::*; + use frame_system::pallet_prelude::*; + + #[pallet::pallet] + #[pallet::generate_store(pub(super) trait Store)] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: frame_system::Config + Sized { + type RuntimeEvent: From> + + IsType<::RuntimeEvent>; + type ExternalMajorityOrigin: EnsureOrigin; + } + + #[pallet::call] + impl Pallet { + #[pallet::call_index(0)] + #[pallet::weight(0)] + pub fn external_propose_majority(origin: OriginFor) -> DispatchResult { + T::ExternalMajorityOrigin::ensure_origin(origin)?; + Self::deposit_event(Event::::ExternalProposed); + Ok(()) + } + } + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + ExternalProposed, + } + } +} + +pub type MaxMembers = ConstU32<100>; + +parameter_types! { + pub const MotionDuration: u64 = 3; + pub const MaxProposals: u32 = 257; +} +impl frame_system::Config for Test { + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = (); + type BlockLength = (); + type DbWeight = (); + type RuntimeOrigin = RuntimeOrigin; + type Index = u64; + type BlockNumber = u64; + type RuntimeCall = RuntimeCall; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = u64; + type Lookup = IdentityLookup; + type Header = Header; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = ConstU64<250>; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = (); + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = ConstU32<16>; +} + +pub struct CanProposeCollective; +impl CanPropose<::AccountId> for CanProposeCollective { + fn can_propose(who: &::AccountId) -> bool { + Collective::is_member(who) + } +} + +pub struct CanVoteCollective; +impl CanVote<::AccountId> for CanVoteCollective { + fn can_vote(who: &::AccountId) -> bool { + Collective::is_member(who) + } +} + +pub struct GetCollectiveCount; +impl GetVotingMembers for GetCollectiveCount { + fn get_count() -> MemberCount {Collective::members().len() as u32} +} +impl Get for GetCollectiveCount { + fn get() -> MemberCount {MaxMembers::get()} +} + +impl Config for Test { + type RuntimeOrigin = RuntimeOrigin; + type Proposal = RuntimeCall; + type RuntimeEvent = RuntimeEvent; + type MotionDuration = ConstU64<3>; + type MaxProposals = MaxProposals; + type MaxMembers = MaxMembers; + type DefaultVote = PrimeDefaultVote; + type WeightInfo = (); + type SetMembersOrigin = EnsureRoot; + type CanPropose = CanProposeCollective; + type CanVote = CanVoteCollective; + type GetVotingMembers = GetCollectiveCount; +} + +pub struct CanProposeCollectiveMajority; +impl CanPropose<::AccountId> for CanProposeCollectiveMajority { + fn can_propose(who: &::AccountId) -> bool { + CollectiveMajority::is_member(who) + } +} + +pub struct CanVoteCollectiveMajority; +impl CanVote<::AccountId> for CanVoteCollectiveMajority { + fn can_vote(who: &::AccountId) -> bool { + CollectiveMajority::is_member(who) + } +} + +pub struct GetCollectiveMajorityCount; +impl GetVotingMembers for GetCollectiveMajorityCount { + fn get_count() -> MemberCount {CollectiveMajority::members().len() as u32} +} +impl Get for GetCollectiveMajorityCount { + fn get() -> MemberCount {MaxMembers::get()} +} + +impl Config for Test { + type RuntimeOrigin = RuntimeOrigin; + type Proposal = RuntimeCall; + type RuntimeEvent = RuntimeEvent; + type MotionDuration = ConstU64<3>; + type MaxProposals = MaxProposals; + type MaxMembers = MaxMembers; + type DefaultVote = MoreThanMajorityThenPrimeDefaultVote; + type WeightInfo = (); + type SetMembersOrigin = EnsureRoot; + type CanPropose = CanProposeCollectiveMajority; + type CanVote = CanVoteCollectiveMajority; + type GetVotingMembers = GetCollectiveMajorityCount; +} +impl mock_democracy::Config for Test { + type RuntimeEvent = RuntimeEvent; + type ExternalMajorityOrigin = EnsureProportionAtLeast; +} + +pub struct CanProposeDefaultCollective; +impl CanPropose<::AccountId> for CanProposeDefaultCollective { + fn can_propose(who: &::AccountId) -> bool { + DefaultCollective::is_member(who) + } +} + +pub struct CanVoteDefaultCollective; +impl CanVote<::AccountId> for CanVoteDefaultCollective { + fn can_vote(who: &::AccountId) -> bool { + DefaultCollective::is_member(who) + } +} + +pub struct GetDefaultCollectiveCount; +impl GetVotingMembers for GetDefaultCollectiveCount { + fn get_count() -> MemberCount {DefaultCollective::members().len() as u32} +} +impl Get for GetDefaultCollectiveCount { + fn get() -> MemberCount {MaxMembers::get()} +} + +impl Config for Test { + type RuntimeOrigin = RuntimeOrigin; + type Proposal = RuntimeCall; + type RuntimeEvent = RuntimeEvent; + type MotionDuration = ConstU64<3>; + type MaxProposals = MaxProposals; + type MaxMembers = MaxMembers; + type DefaultVote = PrimeDefaultVote; + type WeightInfo = (); + type SetMembersOrigin = EnsureRoot; + type CanPropose = CanProposeDefaultCollective; + type CanVote = CanVoteDefaultCollective; + type GetVotingMembers = GetDefaultCollectiveCount; +} + +pub fn new_test_ext() -> sp_io::TestExternalities { + let mut ext: sp_io::TestExternalities = GenesisConfig { + collective: pallet_collective::GenesisConfig { + members: vec![1, 2, 3], + phantom: Default::default(), + }, + collective_majority: pallet_collective::GenesisConfig { + members: vec![1, 2, 3, 4, 5], + phantom: Default::default(), + }, + default_collective: Default::default(), + } + .build_storage() + .unwrap() + .into(); + ext.execute_with(|| System::set_block_number(1)); + ext +} + +fn make_proposal(value: u64) -> RuntimeCall { + RuntimeCall::System(frame_system::Call::remark_with_event { + remark: value.to_be_bytes().to_vec(), + }) +} + +fn record(event: RuntimeEvent) -> EventRecord { + EventRecord { phase: Phase::Initialization, event, topics: vec![] } +} + +#[test] +fn motions_basic_environment_works() { + new_test_ext().execute_with(|| { + assert_eq!(Collective::members(), vec![1, 2, 3]); + assert_eq!(*Collective::proposals(), Vec::::new()); + }); +} + +#[test] +fn close_works() { + new_test_ext().execute_with(|| { + let proposal = make_proposal(42); + let proposal_len: u32 = proposal.using_encoded(|p| p.len() as u32); + let proposal_weight = proposal.get_dispatch_info().weight; + let hash = BlakeTwo256::hash_of(&proposal); + + assert_ok!(Collective::propose( + RuntimeOrigin::signed(1), + Box::new(proposal.clone()), + proposal_len + )); + assert_ok!(Collective::vote(RuntimeOrigin::signed(1), hash, 0, true)); + + System::set_block_number(3); + assert_noop!( + Collective::close(RuntimeOrigin::signed(4), hash, 0, proposal_weight, proposal_len), + Error::::TooEarly + ); + + System::set_block_number(4); + assert_ok!(Collective::close( + RuntimeOrigin::signed(4), + hash, + 0, + proposal_weight, + proposal_len + )); + + assert_eq!( + System::events(), + vec![ + record(RuntimeEvent::Collective(CollectiveEvent::Proposed { + account: 1, + proposal_index: 0, + proposal_hash: hash, + threshold: 2 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Voted { + account: 1, + proposal_hash: hash, + voted: true, + yes: 1, + no: 0 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Closed { + proposal_hash: hash, + yes: 1, + no: 2 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Disapproved { + proposal_hash: hash + })) + ] + ); + }); +} + +#[test] +fn proposal_weight_limit_works_on_approve() { + new_test_ext().execute_with(|| { + let proposal = RuntimeCall::Collective(crate::Call::set_members { + new_members: vec![1, 2, 3], + prime: None, + old_count: MaxMembers::get(), + }); + let proposal_len: u32 = proposal.using_encoded(|p| p.len() as u32); + let proposal_weight = proposal.get_dispatch_info().weight; + let hash = BlakeTwo256::hash_of(&proposal); + // Set 1 as prime voter + Prime::::set(Some(1)); + assert_ok!(Collective::propose( + RuntimeOrigin::signed(1), + Box::new(proposal.clone()), + proposal_len + )); + assert_ok!(Collective::vote(RuntimeOrigin::signed(1), hash, 0, true)); + // With 1's prime vote, this should pass + System::set_block_number(4); + assert_noop!( + Collective::close( + RuntimeOrigin::signed(4), + hash, + 0, + proposal_weight - Weight::from_ref_time(100), + proposal_len + ), + Error::::WrongProposalWeight + ); + assert_ok!(Collective::close( + RuntimeOrigin::signed(4), + hash, + 0, + proposal_weight, + proposal_len + )); + }) +} + +#[test] +fn proposal_weight_limit_ignored_on_disapprove() { + new_test_ext().execute_with(|| { + let proposal = RuntimeCall::Collective(crate::Call::set_members { + new_members: vec![1, 2, 3], + prime: None, + old_count: MaxMembers::get(), + }); + let proposal_len: u32 = proposal.using_encoded(|p| p.len() as u32); + let proposal_weight = proposal.get_dispatch_info().weight; + let hash = BlakeTwo256::hash_of(&proposal); + + assert_ok!(Collective::propose( + RuntimeOrigin::signed(1), + Box::new(proposal.clone()), + proposal_len + )); + // No votes, this proposal wont pass + System::set_block_number(4); + assert_ok!(Collective::close( + RuntimeOrigin::signed(4), + hash, + 0, + proposal_weight - Weight::from_ref_time(100), + proposal_len + )); + }) +} + +#[test] +fn close_with_prime_works() { + new_test_ext().execute_with(|| { + let proposal = make_proposal(42); + let proposal_len: u32 = proposal.using_encoded(|p| p.len() as u32); + let proposal_weight = proposal.get_dispatch_info().weight; + let hash = BlakeTwo256::hash_of(&proposal); + assert_ok!(Collective::set_members( + RuntimeOrigin::root(), + vec![1, 2, 3], + Some(3), + MaxMembers::get() + )); + + assert_ok!(Collective::propose( + RuntimeOrigin::signed(1), + Box::new(proposal.clone()), + proposal_len + )); + assert_ok!(Collective::vote(RuntimeOrigin::signed(1), hash, 0, true)); + + System::set_block_number(4); + assert_ok!(Collective::close( + RuntimeOrigin::signed(4), + hash, + 0, + proposal_weight, + proposal_len + )); + + assert_eq!( + System::events(), + vec![ + record(RuntimeEvent::Collective(CollectiveEvent::Proposed { + account: 1, + proposal_index: 0, + proposal_hash: hash, + threshold: 2 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Voted { + account: 1, + proposal_hash: hash, + voted: true, + yes: 1, + no: 0 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Closed { + proposal_hash: hash, + yes: 1, + no: 2 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Disapproved { + proposal_hash: hash + })) + ] + ); + }); +} + +#[test] +fn close_with_voting_prime_works() { + new_test_ext().execute_with(|| { + let proposal = make_proposal(42); + let proposal_len: u32 = proposal.using_encoded(|p| p.len() as u32); + let proposal_weight = proposal.get_dispatch_info().weight; + let hash = BlakeTwo256::hash_of(&proposal); + assert_ok!(Collective::set_members( + RuntimeOrigin::root(), + vec![1, 2, 3], + Some(1), + MaxMembers::get() + )); + + assert_ok!(Collective::propose( + RuntimeOrigin::signed(1), + Box::new(proposal.clone()), + proposal_len + )); + assert_ok!(Collective::vote(RuntimeOrigin::signed(1), hash, 0, true)); + + System::set_block_number(4); + assert_ok!(Collective::close( + RuntimeOrigin::signed(4), + hash, + 0, + proposal_weight, + proposal_len + )); + + assert_eq!( + System::events(), + vec![ + record(RuntimeEvent::Collective(CollectiveEvent::Proposed { + account: 1, + proposal_index: 0, + proposal_hash: hash, + threshold: 2 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Voted { + account: 1, + proposal_hash: hash, + voted: true, + yes: 1, + no: 0 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Closed { + proposal_hash: hash, + yes: 3, + no: 0 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Approved { proposal_hash: hash })), + record(RuntimeEvent::Collective(CollectiveEvent::Executed { + proposal_hash: hash, + result: Err(DispatchError::BadOrigin) + })) + ] + ); + }); +} + +#[test] +fn close_with_no_prime_but_majority_works() { + new_test_ext().execute_with(|| { + let proposal = make_proposal(42); + let proposal_len: u32 = proposal.using_encoded(|p| p.len() as u32); + let proposal_weight = proposal.get_dispatch_info().weight; + let hash = BlakeTwo256::hash_of(&proposal); + assert_ok!(CollectiveMajority::set_members( + RuntimeOrigin::root(), + vec![1, 2, 3, 4, 5], + Some(5), + MaxMembers::get() + )); + + assert_ok!(CollectiveMajority::propose( + RuntimeOrigin::signed(1), + Box::new(proposal.clone()), + proposal_len + )); + assert_ok!(CollectiveMajority::vote(RuntimeOrigin::signed(1), hash, 0, true)); + assert_ok!(CollectiveMajority::vote(RuntimeOrigin::signed(2), hash, 0, true)); + assert_ok!(CollectiveMajority::vote(RuntimeOrigin::signed(3), hash, 0, true)); + + System::set_block_number(4); + assert_ok!(CollectiveMajority::close( + RuntimeOrigin::signed(4), + hash, + 0, + proposal_weight, + proposal_len + )); + + assert_eq!( + System::events(), + vec![ + record(RuntimeEvent::CollectiveMajority(CollectiveEvent::Proposed { + account: 1, + proposal_index: 0, + proposal_hash: hash, + threshold: 3 + })), + record(RuntimeEvent::CollectiveMajority(CollectiveEvent::Voted { + account: 1, + proposal_hash: hash, + voted: true, + yes: 1, + no: 0 + })), + record(RuntimeEvent::CollectiveMajority(CollectiveEvent::Voted { + account: 2, + proposal_hash: hash, + voted: true, + yes: 2, + no: 0 + })), + record(RuntimeEvent::CollectiveMajority(CollectiveEvent::Voted { + account: 3, + proposal_hash: hash, + voted: true, + yes: 3, + no: 0 + })), + record(RuntimeEvent::CollectiveMajority(CollectiveEvent::Closed { + proposal_hash: hash, + yes: 3, + no: 0 + })), + record(RuntimeEvent::CollectiveMajority(CollectiveEvent::Approved { + proposal_hash: hash + })), + record(RuntimeEvent::CollectiveMajority(CollectiveEvent::Executed { + proposal_hash: hash, + result: Err(DispatchError::BadOrigin) + })) + ] + ); + }); +} + +#[test] +fn removal_of_old_voters_votes_works() { + new_test_ext().execute_with(|| { + let proposal = make_proposal(42); + let proposal_len: u32 = proposal.using_encoded(|p| p.len() as u32); + let hash = BlakeTwo256::hash_of(&proposal); + let end = 4; + assert_ok!(Collective::propose( + RuntimeOrigin::signed(1), + Box::new(proposal.clone()), + proposal_len + )); + assert_ok!(Collective::vote(RuntimeOrigin::signed(1), hash, 0, true)); + assert_ok!(Collective::vote(RuntimeOrigin::signed(2), hash, 0, true)); + assert_eq!( + Collective::voting(&hash), + Some(Votes { index: 0, threshold: 2, ayes: vec![1, 2], nays: vec![], end }) + ); + Collective::change_members_sorted(&[4], &[1], &[2, 3, 4]); + assert_eq!( + Collective::voting(&hash), + Some(Votes { index: 0, threshold: 2, ayes: vec![2], nays: vec![], end }) + ); + + let proposal = make_proposal(69); + let proposal_len: u32 = proposal.using_encoded(|p| p.len() as u32); + let hash = BlakeTwo256::hash_of(&proposal); + assert_ok!(Collective::propose( + RuntimeOrigin::signed(2), + Box::new(proposal.clone()), + proposal_len + )); + assert_ok!(Collective::vote(RuntimeOrigin::signed(2), hash, 1, true)); + assert_ok!(Collective::vote(RuntimeOrigin::signed(3), hash, 1, false)); + assert_eq!( + Collective::voting(&hash), + Some(Votes { index: 1, threshold: 2, ayes: vec![2], nays: vec![3], end }) + ); + Collective::change_members_sorted(&[], &[3], &[2, 4]); + assert_eq!( + Collective::voting(&hash), + Some(Votes { index: 1, threshold: 2, ayes: vec![2], nays: vec![], end }) + ); + }); +} + +#[test] +fn removal_of_old_voters_votes_works_with_set_members() { + new_test_ext().execute_with(|| { + let proposal = make_proposal(42); + let proposal_len: u32 = proposal.using_encoded(|p| p.len() as u32); + let hash = BlakeTwo256::hash_of(&proposal); + let end = 4; + assert_ok!(Collective::propose( + RuntimeOrigin::signed(1), + Box::new(proposal.clone()), + proposal_len + )); + assert_ok!(Collective::vote(RuntimeOrigin::signed(1), hash, 0, true)); + assert_ok!(Collective::vote(RuntimeOrigin::signed(2), hash, 0, true)); + assert_eq!( + Collective::voting(&hash), + Some(Votes { index: 0, threshold: 2, ayes: vec![1, 2], nays: vec![], end }) + ); + assert_ok!(Collective::set_members( + RuntimeOrigin::root(), + vec![2, 3, 4], + None, + MaxMembers::get() + )); + assert_eq!( + Collective::voting(&hash), + Some(Votes { index: 0, threshold: 2, ayes: vec![2], nays: vec![], end }) + ); + + let proposal = make_proposal(69); + let proposal_len: u32 = proposal.using_encoded(|p| p.len() as u32); + let hash = BlakeTwo256::hash_of(&proposal); + assert_ok!(Collective::propose( + RuntimeOrigin::signed(2), + Box::new(proposal.clone()), + proposal_len + )); + assert_ok!(Collective::vote(RuntimeOrigin::signed(2), hash, 1, true)); + assert_ok!(Collective::vote(RuntimeOrigin::signed(3), hash, 1, false)); + assert_eq!( + Collective::voting(&hash), + Some(Votes { index: 1, threshold: 2, ayes: vec![2], nays: vec![3], end }) + ); + assert_ok!(Collective::set_members( + RuntimeOrigin::root(), + vec![2, 4], + None, + MaxMembers::get() + )); + assert_eq!( + Collective::voting(&hash), + Some(Votes { index: 1, threshold: 2, ayes: vec![2], nays: vec![], end }) + ); + }); +} + +#[test] +fn propose_works() { + new_test_ext().execute_with(|| { + let proposal = make_proposal(42); + let proposal_len: u32 = proposal.using_encoded(|p| p.len() as u32); + let hash = proposal.blake2_256().into(); + let end = 4; + assert_ok!(Collective::propose( + RuntimeOrigin::signed(1), + Box::new(proposal.clone()), + proposal_len + )); + assert_eq!(*Collective::proposals(), vec![hash]); + assert_eq!(Collective::proposal_of(&hash), Some(proposal)); + assert_eq!( + Collective::voting(&hash), + Some(Votes { index: 0, threshold: 2, ayes: vec![], nays: vec![], end }) + ); + + assert_eq!( + System::events(), + vec![record(RuntimeEvent::Collective(CollectiveEvent::Proposed { + account: 1, + proposal_index: 0, + proposal_hash: hash, + threshold: 2 + }))] + ); + }); +} + +#[test] +fn limit_active_proposals() { + new_test_ext().execute_with(|| { + for i in 0..MaxProposals::get() { + let proposal = make_proposal(i as u64); + let proposal_len: u32 = proposal.using_encoded(|p| p.len() as u32); + assert_ok!(Collective::propose( + RuntimeOrigin::signed(1), + Box::new(proposal.clone()), + proposal_len + )); + } + let proposal = make_proposal(MaxProposals::get() as u64 + 1); + let proposal_len: u32 = proposal.using_encoded(|p| p.len() as u32); + assert_noop!( + Collective::propose( + RuntimeOrigin::signed(1), + Box::new(proposal.clone()), + proposal_len + ), + Error::::TooManyProposals + ); + }) +} + +#[test] +fn correct_validate_and_get_proposal() { + new_test_ext().execute_with(|| { + let proposal = RuntimeCall::Collective(crate::Call::set_members { + new_members: vec![1, 2, 3], + prime: None, + old_count: MaxMembers::get(), + }); + let length = proposal.encode().len() as u32; + assert_ok!(Collective::propose( + RuntimeOrigin::signed(1), + Box::new(proposal.clone()), + length + )); + + let hash = BlakeTwo256::hash_of(&proposal); + let weight = proposal.get_dispatch_info().weight; + assert_noop!( + Collective::validate_and_get_proposal( + &BlakeTwo256::hash_of(&vec![3; 4]), + length, + weight + ), + Error::::ProposalMissing + ); + assert_noop!( + Collective::validate_and_get_proposal(&hash, length - 2, weight), + Error::::WrongProposalLength + ); + assert_noop!( + Collective::validate_and_get_proposal( + &hash, + length, + weight - Weight::from_ref_time(10) + ), + Error::::WrongProposalWeight + ); + let res = Collective::validate_and_get_proposal(&hash, length, weight); + assert_ok!(res.clone()); + let (retrieved_proposal, len) = res.unwrap(); + assert_eq!(length as usize, len); + assert_eq!(proposal, retrieved_proposal); + }) +} + +#[test] +fn motions_ignoring_non_collective_proposals_works() { + new_test_ext().execute_with(|| { + let proposal = make_proposal(42); + let proposal_len: u32 = proposal.using_encoded(|p| p.len() as u32); + assert_noop!( + Collective::propose( + RuntimeOrigin::signed(42), + Box::new(proposal.clone()), + proposal_len + ), + Error::::NotMember + ); + }); +} + +#[test] +fn motions_ignoring_non_collective_votes_works() { + new_test_ext().execute_with(|| { + let proposal = make_proposal(42); + let proposal_len: u32 = proposal.using_encoded(|p| p.len() as u32); + let hash: H256 = proposal.blake2_256().into(); + assert_ok!(Collective::propose( + RuntimeOrigin::signed(1), + Box::new(proposal.clone()), + proposal_len + )); + assert_noop!( + Collective::vote(RuntimeOrigin::signed(42), hash, 0, true), + Error::::NotMember, + ); + }); +} + +#[test] +fn motions_ignoring_bad_index_collective_vote_works() { + new_test_ext().execute_with(|| { + System::set_block_number(3); + let proposal = make_proposal(42); + let proposal_len: u32 = proposal.using_encoded(|p| p.len() as u32); + let hash: H256 = proposal.blake2_256().into(); + assert_ok!(Collective::propose( + RuntimeOrigin::signed(1), + Box::new(proposal.clone()), + proposal_len + )); + assert_noop!( + Collective::vote(RuntimeOrigin::signed(2), hash, 1, true), + Error::::WrongIndex, + ); + }); +} + +#[test] +fn motions_vote_after_works() { + new_test_ext().execute_with(|| { + let proposal = make_proposal(42); + let proposal_len: u32 = proposal.using_encoded(|p| p.len() as u32); + let hash: H256 = proposal.blake2_256().into(); + let end = 4; + assert_ok!(Collective::propose( + RuntimeOrigin::signed(1), + Box::new(proposal.clone()), + proposal_len + )); + // Initially there a no votes when the motion is proposed. + assert_eq!( + Collective::voting(&hash), + Some(Votes { index: 0, threshold: 2, ayes: vec![], nays: vec![], end }) + ); + // Cast first aye vote. + assert_ok!(Collective::vote(RuntimeOrigin::signed(1), hash, 0, true)); + assert_eq!( + Collective::voting(&hash), + Some(Votes { index: 0, threshold: 2, ayes: vec![1], nays: vec![], end }) + ); + // Try to cast a duplicate aye vote. + assert_noop!( + Collective::vote(RuntimeOrigin::signed(1), hash, 0, true), + Error::::DuplicateVote, + ); + // Cast a nay vote. + assert_ok!(Collective::vote(RuntimeOrigin::signed(1), hash, 0, false)); + assert_eq!( + Collective::voting(&hash), + Some(Votes { index: 0, threshold: 2, ayes: vec![], nays: vec![1], end }) + ); + // Try to cast a duplicate nay vote. + assert_noop!( + Collective::vote(RuntimeOrigin::signed(1), hash, 0, false), + Error::::DuplicateVote, + ); + + assert_eq!( + System::events(), + vec![ + record(RuntimeEvent::Collective(CollectiveEvent::Proposed { + account: 1, + proposal_index: 0, + proposal_hash: hash, + threshold: 2 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Voted { + account: 1, + proposal_hash: hash, + voted: true, + yes: 1, + no: 0 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Voted { + account: 1, + proposal_hash: hash, + voted: false, + yes: 0, + no: 1 + })), + ] + ); + }); +} + +#[test] +fn motions_all_first_vote_free_works() { + new_test_ext().execute_with(|| { + let proposal = make_proposal(42); + let proposal_len: u32 = proposal.using_encoded(|p| p.len() as u32); + let hash: H256 = proposal.blake2_256().into(); + let end = 4; + assert_ok!(Collective::propose( + RuntimeOrigin::signed(1), + Box::new(proposal.clone()), + proposal_len, + )); + assert_eq!( + Collective::voting(&hash), + Some(Votes { index: 0, threshold: 2, ayes: vec![], nays: vec![], end }) + ); + + // For the motion, acc 2's first vote, expecting Ok with Pays::No. + let vote_rval: DispatchResultWithPostInfo = + Collective::vote(RuntimeOrigin::signed(2), hash, 0, true); + assert_eq!(vote_rval.unwrap().pays_fee, Pays::No); + + // Duplicate vote, expecting error with Pays::Yes. + let vote_rval: DispatchResultWithPostInfo = + Collective::vote(RuntimeOrigin::signed(2), hash, 0, true); + assert_eq!(vote_rval.unwrap_err().post_info.pays_fee, Pays::Yes); + + // Modifying vote, expecting ok with Pays::Yes. + let vote_rval: DispatchResultWithPostInfo = + Collective::vote(RuntimeOrigin::signed(2), hash, 0, false); + assert_eq!(vote_rval.unwrap().pays_fee, Pays::Yes); + + // For the motion, acc 3's first vote, expecting Ok with Pays::No. + let vote_rval: DispatchResultWithPostInfo = + Collective::vote(RuntimeOrigin::signed(3), hash, 0, true); + assert_eq!(vote_rval.unwrap().pays_fee, Pays::No); + + // acc 3 modify the vote, expecting Ok with Pays::Yes. + let vote_rval: DispatchResultWithPostInfo = + Collective::vote(RuntimeOrigin::signed(3), hash, 0, false); + assert_eq!(vote_rval.unwrap().pays_fee, Pays::Yes); + + // Test close() Extrincis | Check DispatchResultWithPostInfo with Pay Info + + let proposal_weight = proposal.get_dispatch_info().weight; + let close_rval: DispatchResultWithPostInfo = + Collective::close(RuntimeOrigin::signed(2), hash, 0, proposal_weight, proposal_len); + assert_eq!(close_rval.unwrap().pays_fee, Pays::No); + + // trying to close the proposal, which is already closed. + // Expecting error "ProposalMissing" with Pays::Yes + let close_rval: DispatchResultWithPostInfo = + Collective::close(RuntimeOrigin::signed(2), hash, 0, proposal_weight, proposal_len); + assert_eq!(close_rval.unwrap_err().post_info.pays_fee, Pays::Yes); + }); +} + +#[test] +fn motions_reproposing_disapproved_works() { + new_test_ext().execute_with(|| { + let proposal = make_proposal(42); + let proposal_len: u32 = proposal.using_encoded(|p| p.len() as u32); + let proposal_weight = proposal.get_dispatch_info().weight; + let hash: H256 = proposal.blake2_256().into(); + assert_ok!(Collective::propose( + RuntimeOrigin::signed(1), + Box::new(proposal.clone()), + proposal_len + )); + + assert_ok!(Collective::vote(RuntimeOrigin::signed(1), hash, 0, false)); + assert_ok!(Collective::vote(RuntimeOrigin::signed(2), hash, 0, false)); + + assert_ok!(Collective::close( + RuntimeOrigin::signed(2), + hash, + 0, + proposal_weight, + proposal_len + )); + assert_eq!(*Collective::proposals(), vec![]); + assert_ok!(Collective::propose( + RuntimeOrigin::signed(1), + Box::new(proposal.clone()), + proposal_len + )); + assert_eq!(*Collective::proposals(), vec![hash]); + }); +} + +#[test] +fn motions_approval_with_enough_votes_and_lower_voting_threshold_works() { + new_test_ext().execute_with(|| { + let proposal = RuntimeCall::Democracy(mock_democracy::Call::external_propose_majority {}); + let proposal_len: u32 = proposal.using_encoded(|p| p.len() as u32); + let proposal_weight = proposal.get_dispatch_info().weight; + let hash: H256 = proposal.blake2_256().into(); + // The voting threshold is 2, but the required votes for `ExternalMajorityOrigin` is 3. + // The proposal will be executed regardless of the voting threshold + // as long as we have enough yes votes. + // + // Failed to execute with only 2 yes votes. + assert_ok!(Collective::propose( + RuntimeOrigin::signed(1), + Box::new(proposal.clone()), + proposal_len + )); + assert_ok!(Collective::vote(RuntimeOrigin::signed(1), hash, 0, true)); + assert_ok!(Collective::vote(RuntimeOrigin::signed(2), hash, 0, true)); + assert_ok!(Collective::close( + RuntimeOrigin::signed(2), + hash, + 0, + proposal_weight, + proposal_len + )); + assert_eq!( + System::events(), + vec![ + record(RuntimeEvent::Collective(CollectiveEvent::Proposed { + account: 1, + proposal_index: 0, + proposal_hash: hash, + threshold: 2 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Voted { + account: 1, + proposal_hash: hash, + voted: true, + yes: 1, + no: 0 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Voted { + account: 2, + proposal_hash: hash, + voted: true, + yes: 2, + no: 0 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Closed { + proposal_hash: hash, + yes: 2, + no: 0 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Approved { proposal_hash: hash })), + record(RuntimeEvent::Collective(CollectiveEvent::Executed { + proposal_hash: hash, + result: Err(DispatchError::BadOrigin) + })), + ] + ); + + System::reset_events(); + + // Executed with 3 yes votes. + assert_ok!(Collective::propose( + RuntimeOrigin::signed(1), + Box::new(proposal.clone()), + proposal_len + )); + assert_ok!(Collective::vote(RuntimeOrigin::signed(1), hash, 1, true)); + assert_ok!(Collective::vote(RuntimeOrigin::signed(2), hash, 1, true)); + assert_ok!(Collective::vote(RuntimeOrigin::signed(3), hash, 1, true)); + assert_ok!(Collective::close( + RuntimeOrigin::signed(2), + hash, + 1, + proposal_weight, + proposal_len + )); + assert_eq!( + System::events(), + vec![ + record(RuntimeEvent::Collective(CollectiveEvent::Proposed { + account: 1, + proposal_index: 1, + proposal_hash: hash, + threshold: 2 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Voted { + account: 1, + proposal_hash: hash, + voted: true, + yes: 1, + no: 0 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Voted { + account: 2, + proposal_hash: hash, + voted: true, + yes: 2, + no: 0 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Voted { + account: 3, + proposal_hash: hash, + voted: true, + yes: 3, + no: 0 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Closed { + proposal_hash: hash, + yes: 3, + no: 0 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Approved { proposal_hash: hash })), + record(RuntimeEvent::Democracy( + mock_democracy::pallet::Event::::ExternalProposed + )), + record(RuntimeEvent::Collective(CollectiveEvent::Executed { + proposal_hash: hash, + result: Ok(()) + })), + ] + ); + }); +} + +#[test] +fn motions_disapproval_works() { + new_test_ext().execute_with(|| { + let proposal = make_proposal(42); + let proposal_len: u32 = proposal.using_encoded(|p| p.len() as u32); + let proposal_weight = proposal.get_dispatch_info().weight; + let hash: H256 = proposal.blake2_256().into(); + assert_ok!(Collective::propose( + RuntimeOrigin::signed(1), + Box::new(proposal.clone()), + proposal_len + )); + assert_ok!(Collective::vote(RuntimeOrigin::signed(1), hash, 0, false)); + assert_ok!(Collective::vote(RuntimeOrigin::signed(2), hash, 0, false)); + assert_ok!(Collective::close( + RuntimeOrigin::signed(2), + hash, + 0, + proposal_weight, + proposal_len + )); + + assert_eq!( + System::events(), + vec![ + record(RuntimeEvent::Collective(CollectiveEvent::Proposed { + account: 1, + proposal_index: 0, + proposal_hash: hash, + threshold: 2 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Voted { + account: 1, + proposal_hash: hash, + voted: false, + yes: 0, + no: 1 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Voted { + account: 2, + proposal_hash: hash, + voted: false, + yes: 0, + no: 2 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Closed { + proposal_hash: hash, + yes: 0, + no: 2 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Disapproved { + proposal_hash: hash + })), + ] + ); + }); +} + +#[test] +fn motions_approval_works() { + new_test_ext().execute_with(|| { + let proposal = make_proposal(42); + let proposal_len: u32 = proposal.using_encoded(|p| p.len() as u32); + let proposal_weight = proposal.get_dispatch_info().weight; + let hash: H256 = proposal.blake2_256().into(); + assert_ok!(Collective::propose( + RuntimeOrigin::signed(1), + Box::new(proposal.clone()), + proposal_len + )); + assert_ok!(Collective::vote(RuntimeOrigin::signed(1), hash, 0, true)); + assert_ok!(Collective::vote(RuntimeOrigin::signed(2), hash, 0, true)); + assert_ok!(Collective::close( + RuntimeOrigin::signed(2), + hash, + 0, + proposal_weight, + proposal_len + )); + + assert_eq!( + System::events(), + vec![ + record(RuntimeEvent::Collective(CollectiveEvent::Proposed { + account: 1, + proposal_index: 0, + proposal_hash: hash, + threshold: 2 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Voted { + account: 1, + proposal_hash: hash, + voted: true, + yes: 1, + no: 0 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Voted { + account: 2, + proposal_hash: hash, + voted: true, + yes: 2, + no: 0 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Closed { + proposal_hash: hash, + yes: 2, + no: 0 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Approved { proposal_hash: hash })), + record(RuntimeEvent::Collective(CollectiveEvent::Executed { + proposal_hash: hash, + result: Err(DispatchError::BadOrigin) + })), + ] + ); + }); +} + +#[test] +fn motion_with_no_votes_closes_with_disapproval() { + new_test_ext().execute_with(|| { + let proposal = make_proposal(42); + let proposal_len: u32 = proposal.using_encoded(|p| p.len() as u32); + let proposal_weight = proposal.get_dispatch_info().weight; + let hash: H256 = proposal.blake2_256().into(); + assert_ok!(Collective::propose( + RuntimeOrigin::signed(1), + Box::new(proposal.clone()), + proposal_len + )); + assert_eq!( + System::events()[0], + record(RuntimeEvent::Collective(CollectiveEvent::Proposed { + account: 1, + proposal_index: 0, + proposal_hash: hash, + threshold: 2 + })) + ); + + // Closing the motion too early is not possible because it has neither + // an approving or disapproving simple majority due to the lack of votes. + assert_noop!( + Collective::close(RuntimeOrigin::signed(2), hash, 0, proposal_weight, proposal_len), + Error::::TooEarly + ); + + // Once the motion duration passes, + let closing_block = System::block_number() + MotionDuration::get(); + System::set_block_number(closing_block); + // we can successfully close the motion. + assert_ok!(Collective::close( + RuntimeOrigin::signed(2), + hash, + 0, + proposal_weight, + proposal_len + )); + + // Events show that the close ended in a disapproval. + assert_eq!( + System::events()[1], + record(RuntimeEvent::Collective(CollectiveEvent::Closed { + proposal_hash: hash, + yes: 0, + no: 3 + })) + ); + assert_eq!( + System::events()[2], + record(RuntimeEvent::Collective(CollectiveEvent::Disapproved { proposal_hash: hash })) + ); + }) +} + +#[test] +fn close_disapprove_does_not_care_about_weight_or_len() { + // This test confirms that if you close a proposal that would be disapproved, + // we do not care about the proposal length or proposal weight since it will + // not be read from storage or executed. + new_test_ext().execute_with(|| { + let proposal = make_proposal(42); + let proposal_len: u32 = proposal.using_encoded(|p| p.len() as u32); + let hash: H256 = proposal.blake2_256().into(); + assert_ok!(Collective::propose( + RuntimeOrigin::signed(1), + Box::new(proposal.clone()), + proposal_len + )); + // First we make the proposal succeed + assert_ok!(Collective::vote(RuntimeOrigin::signed(1), hash, 0, true)); + assert_ok!(Collective::vote(RuntimeOrigin::signed(2), hash, 0, true)); + // It will not close with bad weight/len information + assert_noop!( + Collective::close(RuntimeOrigin::signed(2), hash, 0, Weight::zero(), 0), + Error::::WrongProposalLength, + ); + assert_noop!( + Collective::close(RuntimeOrigin::signed(2), hash, 0, Weight::zero(), proposal_len), + Error::::WrongProposalWeight, + ); + // Now we make the proposal fail + assert_ok!(Collective::vote(RuntimeOrigin::signed(1), hash, 0, false)); + assert_ok!(Collective::vote(RuntimeOrigin::signed(2), hash, 0, false)); + // It can close even if the weight/len information is bad + assert_ok!(Collective::close(RuntimeOrigin::signed(2), hash, 0, Weight::zero(), 0)); + }) +} + +#[test] +fn disapprove_proposal_works() { + new_test_ext().execute_with(|| { + let proposal = make_proposal(42); + let proposal_len: u32 = proposal.using_encoded(|p| p.len() as u32); + let hash: H256 = proposal.blake2_256().into(); + assert_ok!(Collective::propose( + RuntimeOrigin::signed(1), + Box::new(proposal.clone()), + proposal_len + )); + // Proposal would normally succeed + assert_ok!(Collective::vote(RuntimeOrigin::signed(1), hash, 0, true)); + assert_ok!(Collective::vote(RuntimeOrigin::signed(2), hash, 0, true)); + // But Root can disapprove and remove it anyway + assert_ok!(Collective::disapprove_proposal(RuntimeOrigin::root(), hash)); + assert_eq!( + System::events(), + vec![ + record(RuntimeEvent::Collective(CollectiveEvent::Proposed { + account: 1, + proposal_index: 0, + proposal_hash: hash, + threshold: 2 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Voted { + account: 1, + proposal_hash: hash, + voted: true, + yes: 1, + no: 0 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Voted { + account: 2, + proposal_hash: hash, + voted: true, + yes: 2, + no: 0 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Disapproved { + proposal_hash: hash + })), + ] + ); + }) +} + +#[test] +#[should_panic(expected = "Members cannot contain duplicate accounts.")] +fn genesis_build_panics_with_duplicate_members() { + pallet_collective::GenesisConfig:: { + members: vec![1, 2, 3, 1], + phantom: Default::default(), + } + .build_storage() + .unwrap(); +} diff --git a/pallets/collective/src/weights.rs b/pallets/collective/src/weights.rs new file mode 100644 index 000000000..d8a713118 --- /dev/null +++ b/pallets/collective/src/weights.rs @@ -0,0 +1,554 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Autogenerated weights for pallet_collective +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2023-01-24, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `bm2`, CPU: `Intel(R) Core(TM) i7-7700K CPU @ 4.20GHz` +//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024 + +// Executed Command: +// ./target/production/substrate +// benchmark +// pallet +// --chain=dev +// --steps=50 +// --repeat=20 +// --pallet=pallet_collective +// --extrinsic=* +// --execution=wasm +// --wasm-execution=compiled +// --heap-pages=4096 +// --output=./frame/collective/src/weights.rs +// --header=./HEADER-APACHE2 +// --template=./.maintain/frame-weight-template.hbs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use sp_std::marker::PhantomData; + +/// Weight functions needed for pallet_collective. +pub trait WeightInfo { + fn set_members(m: u32, n: u32, p: u32, ) -> Weight; + fn execute(b: u32, m: u32, ) -> Weight; + fn propose_execute(b: u32, m: u32, ) -> Weight; + fn propose_proposed(b: u32, m: u32, p: u32, ) -> Weight; + fn vote(m: u32, ) -> Weight; + fn close_early_disapproved(m: u32, p: u32, ) -> Weight; + fn close_early_approved(b: u32, m: u32, p: u32, ) -> Weight; + fn close_disapproved(m: u32, p: u32, ) -> Weight; + fn close_approved(b: u32, m: u32, p: u32, ) -> Weight; + fn disapprove_proposal(p: u32, ) -> Weight; +} + +/// Weights for pallet_collective using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + /// Storage: Council Members (r:1 w:1) + /// Proof Skipped: Council Members (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council Proposals (r:1 w:0) + /// Proof Skipped: Council Proposals (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council Voting (r:100 w:100) + /// Proof Skipped: Council Voting (max_values: None, max_size: None, mode: Measured) + /// Storage: Council Prime (r:0 w:1) + /// Proof Skipped: Council Prime (max_values: Some(1), max_size: None, mode: Measured) + /// The range of component `m` is `[0, 100]`. + /// The range of component `n` is `[0, 100]`. + /// The range of component `p` is `[0, 100]`. + fn set_members(m: u32, _n: u32, p: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0 + m * (3233 ±0) + p * (3223 ±0)` + // Estimated: `16586 + m * (7809 ±24) + p * (10238 ±24)` + // Minimum execution time: 17_093 nanoseconds. + Weight::from_parts(17_284_000, 16586) + // Standard Error: 64_700 + .saturating_add(Weight::from_ref_time(5_143_145).saturating_mul(m.into())) + // Standard Error: 64_700 + .saturating_add(Weight::from_ref_time(7_480_941).saturating_mul(p.into())) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(p.into()))) + .saturating_add(T::DbWeight::get().writes(2_u64)) + .saturating_add(T::DbWeight::get().writes((1_u64).saturating_mul(p.into()))) + .saturating_add(Weight::from_proof_size(7809).saturating_mul(m.into())) + .saturating_add(Weight::from_proof_size(10238).saturating_mul(p.into())) + } + /// Storage: Council Members (r:1 w:0) + /// Proof Skipped: Council Members (max_values: Some(1), max_size: None, mode: Measured) + /// The range of component `b` is `[2, 1024]`. + /// The range of component `m` is `[1, 100]`. + fn execute(b: u32, m: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `234 + m * (32 ±0)` + // Estimated: `730 + m * (32 ±0)` + // Minimum execution time: 15_972 nanoseconds. + Weight::from_parts(14_971_445, 730) + // Standard Error: 32 + .saturating_add(Weight::from_ref_time(1_775).saturating_mul(b.into())) + // Standard Error: 334 + .saturating_add(Weight::from_ref_time(17_052).saturating_mul(m.into())) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(Weight::from_proof_size(32).saturating_mul(m.into())) + } + /// Storage: Council Members (r:1 w:0) + /// Proof Skipped: Council Members (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council ProposalOf (r:1 w:0) + /// Proof Skipped: Council ProposalOf (max_values: None, max_size: None, mode: Measured) + /// The range of component `b` is `[2, 1024]`. + /// The range of component `m` is `[1, 100]`. + fn propose_execute(b: u32, m: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `234 + m * (32 ±0)` + // Estimated: `3440 + m * (64 ±0)` + // Minimum execution time: 17_950 nanoseconds. + Weight::from_parts(17_019_558, 3440) + // Standard Error: 41 + .saturating_add(Weight::from_ref_time(1_807).saturating_mul(b.into())) + // Standard Error: 432 + .saturating_add(Weight::from_ref_time(27_986).saturating_mul(m.into())) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(Weight::from_proof_size(64).saturating_mul(m.into())) + } + /// Storage: Council Members (r:1 w:0) + /// Proof Skipped: Council Members (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council ProposalOf (r:1 w:1) + /// Proof Skipped: Council ProposalOf (max_values: None, max_size: None, mode: Measured) + /// Storage: Council Proposals (r:1 w:1) + /// Proof Skipped: Council Proposals (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council ProposalCount (r:1 w:1) + /// Proof Skipped: Council ProposalCount (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council Voting (r:0 w:1) + /// Proof Skipped: Council Voting (max_values: None, max_size: None, mode: Measured) + /// The range of component `b` is `[2, 1024]`. + /// The range of component `m` is `[2, 100]`. + /// The range of component `p` is `[1, 100]`. + fn propose_proposed(b: u32, m: u32, p: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `556 + m * (32 ±0) + p * (36 ±0)` + // Estimated: `6355 + m * (165 ±0) + p * (180 ±0)` + // Minimum execution time: 24_817 nanoseconds. + Weight::from_parts(24_778_955, 6355) + // Standard Error: 73 + .saturating_add(Weight::from_ref_time(2_355).saturating_mul(b.into())) + // Standard Error: 765 + .saturating_add(Weight::from_ref_time(20_518).saturating_mul(m.into())) + // Standard Error: 755 + .saturating_add(Weight::from_ref_time(85_670).saturating_mul(p.into())) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(4_u64)) + .saturating_add(Weight::from_proof_size(165).saturating_mul(m.into())) + .saturating_add(Weight::from_proof_size(180).saturating_mul(p.into())) + } + /// Storage: Council Members (r:1 w:0) + /// Proof Skipped: Council Members (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council Voting (r:1 w:1) + /// Proof Skipped: Council Voting (max_values: None, max_size: None, mode: Measured) + /// The range of component `m` is `[5, 100]`. + fn vote(m: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `1006 + m * (64 ±0)` + // Estimated: `4980 + m * (128 ±0)` + // Minimum execution time: 19_790 nanoseconds. + Weight::from_parts(20_528_275, 4980) + // Standard Error: 651 + .saturating_add(Weight::from_ref_time(48_856).saturating_mul(m.into())) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + .saturating_add(Weight::from_proof_size(128).saturating_mul(m.into())) + } + /// Storage: Council Voting (r:1 w:1) + /// Proof Skipped: Council Voting (max_values: None, max_size: None, mode: Measured) + /// Storage: Council Members (r:1 w:0) + /// Proof Skipped: Council Members (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council Proposals (r:1 w:1) + /// Proof Skipped: Council Proposals (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council ProposalOf (r:0 w:1) + /// Proof Skipped: Council ProposalOf (max_values: None, max_size: None, mode: Measured) + /// The range of component `m` is `[4, 100]`. + /// The range of component `p` is `[1, 100]`. + fn close_early_disapproved(m: u32, p: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `626 + m * (64 ±0) + p * (36 ±0)` + // Estimated: `5893 + m * (260 ±0) + p * (144 ±0)` + // Minimum execution time: 25_564 nanoseconds. + Weight::from_parts(25_535_497, 5893) + // Standard Error: 610 + .saturating_add(Weight::from_ref_time(27_956).saturating_mul(m.into())) + // Standard Error: 595 + .saturating_add(Weight::from_ref_time(84_835).saturating_mul(p.into())) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + .saturating_add(Weight::from_proof_size(260).saturating_mul(m.into())) + .saturating_add(Weight::from_proof_size(144).saturating_mul(p.into())) + } + /// Storage: Council Voting (r:1 w:1) + /// Proof Skipped: Council Voting (max_values: None, max_size: None, mode: Measured) + /// Storage: Council Members (r:1 w:0) + /// Proof Skipped: Council Members (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council ProposalOf (r:1 w:1) + /// Proof Skipped: Council ProposalOf (max_values: None, max_size: None, mode: Measured) + /// Storage: Council Proposals (r:1 w:1) + /// Proof Skipped: Council Proposals (max_values: Some(1), max_size: None, mode: Measured) + /// The range of component `b` is `[2, 1024]`. + /// The range of component `m` is `[4, 100]`. + /// The range of component `p` is `[1, 100]`. + fn close_early_approved(b: u32, m: u32, p: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `962 + b * (1 ±0) + m * (64 ±0) + p * (40 ±0)` + // Estimated: `9164 + b * (4 ±0) + m * (264 ±0) + p * (160 ±0)` + // Minimum execution time: 36_515 nanoseconds. + Weight::from_parts(36_626_648, 9164) + // Standard Error: 98 + .saturating_add(Weight::from_ref_time(2_295).saturating_mul(b.into())) + // Standard Error: 1_036 + .saturating_add(Weight::from_ref_time(22_182).saturating_mul(m.into())) + // Standard Error: 1_010 + .saturating_add(Weight::from_ref_time(100_034).saturating_mul(p.into())) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + .saturating_add(Weight::from_proof_size(4).saturating_mul(b.into())) + .saturating_add(Weight::from_proof_size(264).saturating_mul(m.into())) + .saturating_add(Weight::from_proof_size(160).saturating_mul(p.into())) + } + /// Storage: Council Voting (r:1 w:1) + /// Proof Skipped: Council Voting (max_values: None, max_size: None, mode: Measured) + /// Storage: Council Members (r:1 w:0) + /// Proof Skipped: Council Members (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council Prime (r:1 w:0) + /// Proof Skipped: Council Prime (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council Proposals (r:1 w:1) + /// Proof Skipped: Council Proposals (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council ProposalOf (r:0 w:1) + /// Proof Skipped: Council ProposalOf (max_values: None, max_size: None, mode: Measured) + /// The range of component `m` is `[4, 100]`. + /// The range of component `p` is `[1, 100]`. + fn close_disapproved(m: u32, p: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `646 + m * (64 ±0) + p * (36 ±0)` + // Estimated: `7095 + m * (325 ±0) + p * (180 ±0)` + // Minimum execution time: 28_858 nanoseconds. + Weight::from_parts(28_050_047, 7095) + // Standard Error: 614 + .saturating_add(Weight::from_ref_time(34_031).saturating_mul(m.into())) + // Standard Error: 599 + .saturating_add(Weight::from_ref_time(85_744).saturating_mul(p.into())) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + .saturating_add(Weight::from_proof_size(325).saturating_mul(m.into())) + .saturating_add(Weight::from_proof_size(180).saturating_mul(p.into())) + } + /// Storage: Council Voting (r:1 w:1) + /// Proof Skipped: Council Voting (max_values: None, max_size: None, mode: Measured) + /// Storage: Council Members (r:1 w:0) + /// Proof Skipped: Council Members (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council Prime (r:1 w:0) + /// Proof Skipped: Council Prime (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council ProposalOf (r:1 w:1) + /// Proof Skipped: Council ProposalOf (max_values: None, max_size: None, mode: Measured) + /// Storage: Council Proposals (r:1 w:1) + /// Proof Skipped: Council Proposals (max_values: Some(1), max_size: None, mode: Measured) + /// The range of component `b` is `[2, 1024]`. + /// The range of component `m` is `[4, 100]`. + /// The range of component `p` is `[1, 100]`. + fn close_approved(b: u32, m: u32, p: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `982 + b * (1 ±0) + m * (64 ±0) + p * (40 ±0)` + // Estimated: `10565 + b * (5 ±0) + m * (330 ±0) + p * (200 ±0)` + // Minimum execution time: 38_608 nanoseconds. + Weight::from_parts(39_948_329, 10565) + // Standard Error: 84 + .saturating_add(Weight::from_ref_time(2_045).saturating_mul(b.into())) + // Standard Error: 895 + .saturating_add(Weight::from_ref_time(22_669).saturating_mul(m.into())) + // Standard Error: 872 + .saturating_add(Weight::from_ref_time(95_525).saturating_mul(p.into())) + .saturating_add(T::DbWeight::get().reads(5_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + .saturating_add(Weight::from_proof_size(5).saturating_mul(b.into())) + .saturating_add(Weight::from_proof_size(330).saturating_mul(m.into())) + .saturating_add(Weight::from_proof_size(200).saturating_mul(p.into())) + } + /// Storage: Council Proposals (r:1 w:1) + /// Proof Skipped: Council Proposals (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council Voting (r:0 w:1) + /// Proof Skipped: Council Voting (max_values: None, max_size: None, mode: Measured) + /// Storage: Council ProposalOf (r:0 w:1) + /// Proof Skipped: Council ProposalOf (max_values: None, max_size: None, mode: Measured) + /// The range of component `p` is `[1, 100]`. + fn disapprove_proposal(p: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `391 + p * (32 ±0)` + // Estimated: `1668 + p * (96 ±0)` + // Minimum execution time: 14_785 nanoseconds. + Weight::from_parts(16_393_818, 1668) + // Standard Error: 612 + .saturating_add(Weight::from_ref_time(76_786).saturating_mul(p.into())) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + .saturating_add(Weight::from_proof_size(96).saturating_mul(p.into())) + } +} + +// For backwards compatibility and tests +impl WeightInfo for () { + /// Storage: Council Members (r:1 w:1) + /// Proof Skipped: Council Members (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council Proposals (r:1 w:0) + /// Proof Skipped: Council Proposals (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council Voting (r:100 w:100) + /// Proof Skipped: Council Voting (max_values: None, max_size: None, mode: Measured) + /// Storage: Council Prime (r:0 w:1) + /// Proof Skipped: Council Prime (max_values: Some(1), max_size: None, mode: Measured) + /// The range of component `m` is `[0, 100]`. + /// The range of component `n` is `[0, 100]`. + /// The range of component `p` is `[0, 100]`. + fn set_members(m: u32, _n: u32, p: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0 + m * (3233 ±0) + p * (3223 ±0)` + // Estimated: `16586 + m * (7809 ±24) + p * (10238 ±24)` + // Minimum execution time: 17_093 nanoseconds. + Weight::from_parts(17_284_000, 16586) + // Standard Error: 64_700 + .saturating_add(Weight::from_ref_time(5_143_145).saturating_mul(m.into())) + // Standard Error: 64_700 + .saturating_add(Weight::from_ref_time(7_480_941).saturating_mul(p.into())) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(p.into()))) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + .saturating_add(RocksDbWeight::get().writes((1_u64).saturating_mul(p.into()))) + .saturating_add(Weight::from_proof_size(7809).saturating_mul(m.into())) + .saturating_add(Weight::from_proof_size(10238).saturating_mul(p.into())) + } + /// Storage: Council Members (r:1 w:0) + /// Proof Skipped: Council Members (max_values: Some(1), max_size: None, mode: Measured) + /// The range of component `b` is `[2, 1024]`. + /// The range of component `m` is `[1, 100]`. + fn execute(b: u32, m: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `234 + m * (32 ±0)` + // Estimated: `730 + m * (32 ±0)` + // Minimum execution time: 15_972 nanoseconds. + Weight::from_parts(14_971_445, 730) + // Standard Error: 32 + .saturating_add(Weight::from_ref_time(1_775).saturating_mul(b.into())) + // Standard Error: 334 + .saturating_add(Weight::from_ref_time(17_052).saturating_mul(m.into())) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(Weight::from_proof_size(32).saturating_mul(m.into())) + } + /// Storage: Council Members (r:1 w:0) + /// Proof Skipped: Council Members (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council ProposalOf (r:1 w:0) + /// Proof Skipped: Council ProposalOf (max_values: None, max_size: None, mode: Measured) + /// The range of component `b` is `[2, 1024]`. + /// The range of component `m` is `[1, 100]`. + fn propose_execute(b: u32, m: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `234 + m * (32 ±0)` + // Estimated: `3440 + m * (64 ±0)` + // Minimum execution time: 17_950 nanoseconds. + Weight::from_parts(17_019_558, 3440) + // Standard Error: 41 + .saturating_add(Weight::from_ref_time(1_807).saturating_mul(b.into())) + // Standard Error: 432 + .saturating_add(Weight::from_ref_time(27_986).saturating_mul(m.into())) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(Weight::from_proof_size(64).saturating_mul(m.into())) + } + /// Storage: Council Members (r:1 w:0) + /// Proof Skipped: Council Members (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council ProposalOf (r:1 w:1) + /// Proof Skipped: Council ProposalOf (max_values: None, max_size: None, mode: Measured) + /// Storage: Council Proposals (r:1 w:1) + /// Proof Skipped: Council Proposals (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council ProposalCount (r:1 w:1) + /// Proof Skipped: Council ProposalCount (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council Voting (r:0 w:1) + /// Proof Skipped: Council Voting (max_values: None, max_size: None, mode: Measured) + /// The range of component `b` is `[2, 1024]`. + /// The range of component `m` is `[2, 100]`. + /// The range of component `p` is `[1, 100]`. + fn propose_proposed(b: u32, m: u32, p: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `556 + m * (32 ±0) + p * (36 ±0)` + // Estimated: `6355 + m * (165 ±0) + p * (180 ±0)` + // Minimum execution time: 24_817 nanoseconds. + Weight::from_parts(24_778_955, 6355) + // Standard Error: 73 + .saturating_add(Weight::from_ref_time(2_355).saturating_mul(b.into())) + // Standard Error: 765 + .saturating_add(Weight::from_ref_time(20_518).saturating_mul(m.into())) + // Standard Error: 755 + .saturating_add(Weight::from_ref_time(85_670).saturating_mul(p.into())) + .saturating_add(RocksDbWeight::get().reads(4_u64)) + .saturating_add(RocksDbWeight::get().writes(4_u64)) + .saturating_add(Weight::from_proof_size(165).saturating_mul(m.into())) + .saturating_add(Weight::from_proof_size(180).saturating_mul(p.into())) + } + /// Storage: Council Members (r:1 w:0) + /// Proof Skipped: Council Members (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council Voting (r:1 w:1) + /// Proof Skipped: Council Voting (max_values: None, max_size: None, mode: Measured) + /// The range of component `m` is `[5, 100]`. + fn vote(m: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `1006 + m * (64 ±0)` + // Estimated: `4980 + m * (128 ±0)` + // Minimum execution time: 19_790 nanoseconds. + Weight::from_parts(20_528_275, 4980) + // Standard Error: 651 + .saturating_add(Weight::from_ref_time(48_856).saturating_mul(m.into())) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + .saturating_add(Weight::from_proof_size(128).saturating_mul(m.into())) + } + /// Storage: Council Voting (r:1 w:1) + /// Proof Skipped: Council Voting (max_values: None, max_size: None, mode: Measured) + /// Storage: Council Members (r:1 w:0) + /// Proof Skipped: Council Members (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council Proposals (r:1 w:1) + /// Proof Skipped: Council Proposals (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council ProposalOf (r:0 w:1) + /// Proof Skipped: Council ProposalOf (max_values: None, max_size: None, mode: Measured) + /// The range of component `m` is `[4, 100]`. + /// The range of component `p` is `[1, 100]`. + fn close_early_disapproved(m: u32, p: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `626 + m * (64 ±0) + p * (36 ±0)` + // Estimated: `5893 + m * (260 ±0) + p * (144 ±0)` + // Minimum execution time: 25_564 nanoseconds. + Weight::from_parts(25_535_497, 5893) + // Standard Error: 610 + .saturating_add(Weight::from_ref_time(27_956).saturating_mul(m.into())) + // Standard Error: 595 + .saturating_add(Weight::from_ref_time(84_835).saturating_mul(p.into())) + .saturating_add(RocksDbWeight::get().reads(3_u64)) + .saturating_add(RocksDbWeight::get().writes(3_u64)) + .saturating_add(Weight::from_proof_size(260).saturating_mul(m.into())) + .saturating_add(Weight::from_proof_size(144).saturating_mul(p.into())) + } + /// Storage: Council Voting (r:1 w:1) + /// Proof Skipped: Council Voting (max_values: None, max_size: None, mode: Measured) + /// Storage: Council Members (r:1 w:0) + /// Proof Skipped: Council Members (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council ProposalOf (r:1 w:1) + /// Proof Skipped: Council ProposalOf (max_values: None, max_size: None, mode: Measured) + /// Storage: Council Proposals (r:1 w:1) + /// Proof Skipped: Council Proposals (max_values: Some(1), max_size: None, mode: Measured) + /// The range of component `b` is `[2, 1024]`. + /// The range of component `m` is `[4, 100]`. + /// The range of component `p` is `[1, 100]`. + fn close_early_approved(b: u32, m: u32, p: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `962 + b * (1 ±0) + m * (64 ±0) + p * (40 ±0)` + // Estimated: `9164 + b * (4 ±0) + m * (264 ±0) + p * (160 ±0)` + // Minimum execution time: 36_515 nanoseconds. + Weight::from_parts(36_626_648, 9164) + // Standard Error: 98 + .saturating_add(Weight::from_ref_time(2_295).saturating_mul(b.into())) + // Standard Error: 1_036 + .saturating_add(Weight::from_ref_time(22_182).saturating_mul(m.into())) + // Standard Error: 1_010 + .saturating_add(Weight::from_ref_time(100_034).saturating_mul(p.into())) + .saturating_add(RocksDbWeight::get().reads(4_u64)) + .saturating_add(RocksDbWeight::get().writes(3_u64)) + .saturating_add(Weight::from_proof_size(4).saturating_mul(b.into())) + .saturating_add(Weight::from_proof_size(264).saturating_mul(m.into())) + .saturating_add(Weight::from_proof_size(160).saturating_mul(p.into())) + } + /// Storage: Council Voting (r:1 w:1) + /// Proof Skipped: Council Voting (max_values: None, max_size: None, mode: Measured) + /// Storage: Council Members (r:1 w:0) + /// Proof Skipped: Council Members (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council Prime (r:1 w:0) + /// Proof Skipped: Council Prime (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council Proposals (r:1 w:1) + /// Proof Skipped: Council Proposals (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council ProposalOf (r:0 w:1) + /// Proof Skipped: Council ProposalOf (max_values: None, max_size: None, mode: Measured) + /// The range of component `m` is `[4, 100]`. + /// The range of component `p` is `[1, 100]`. + fn close_disapproved(m: u32, p: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `646 + m * (64 ±0) + p * (36 ±0)` + // Estimated: `7095 + m * (325 ±0) + p * (180 ±0)` + // Minimum execution time: 28_858 nanoseconds. + Weight::from_parts(28_050_047, 7095) + // Standard Error: 614 + .saturating_add(Weight::from_ref_time(34_031).saturating_mul(m.into())) + // Standard Error: 599 + .saturating_add(Weight::from_ref_time(85_744).saturating_mul(p.into())) + .saturating_add(RocksDbWeight::get().reads(4_u64)) + .saturating_add(RocksDbWeight::get().writes(3_u64)) + .saturating_add(Weight::from_proof_size(325).saturating_mul(m.into())) + .saturating_add(Weight::from_proof_size(180).saturating_mul(p.into())) + } + /// Storage: Council Voting (r:1 w:1) + /// Proof Skipped: Council Voting (max_values: None, max_size: None, mode: Measured) + /// Storage: Council Members (r:1 w:0) + /// Proof Skipped: Council Members (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council Prime (r:1 w:0) + /// Proof Skipped: Council Prime (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council ProposalOf (r:1 w:1) + /// Proof Skipped: Council ProposalOf (max_values: None, max_size: None, mode: Measured) + /// Storage: Council Proposals (r:1 w:1) + /// Proof Skipped: Council Proposals (max_values: Some(1), max_size: None, mode: Measured) + /// The range of component `b` is `[2, 1024]`. + /// The range of component `m` is `[4, 100]`. + /// The range of component `p` is `[1, 100]`. + fn close_approved(b: u32, m: u32, p: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `982 + b * (1 ±0) + m * (64 ±0) + p * (40 ±0)` + // Estimated: `10565 + b * (5 ±0) + m * (330 ±0) + p * (200 ±0)` + // Minimum execution time: 38_608 nanoseconds. + Weight::from_parts(39_948_329, 10565) + // Standard Error: 84 + .saturating_add(Weight::from_ref_time(2_045).saturating_mul(b.into())) + // Standard Error: 895 + .saturating_add(Weight::from_ref_time(22_669).saturating_mul(m.into())) + // Standard Error: 872 + .saturating_add(Weight::from_ref_time(95_525).saturating_mul(p.into())) + .saturating_add(RocksDbWeight::get().reads(5_u64)) + .saturating_add(RocksDbWeight::get().writes(3_u64)) + .saturating_add(Weight::from_proof_size(5).saturating_mul(b.into())) + .saturating_add(Weight::from_proof_size(330).saturating_mul(m.into())) + .saturating_add(Weight::from_proof_size(200).saturating_mul(p.into())) + } + /// Storage: Council Proposals (r:1 w:1) + /// Proof Skipped: Council Proposals (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council Voting (r:0 w:1) + /// Proof Skipped: Council Voting (max_values: None, max_size: None, mode: Measured) + /// Storage: Council ProposalOf (r:0 w:1) + /// Proof Skipped: Council ProposalOf (max_values: None, max_size: None, mode: Measured) + /// The range of component `p` is `[1, 100]`. + fn disapprove_proposal(p: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `391 + p * (32 ±0)` + // Estimated: `1668 + p * (96 ±0)` + // Minimum execution time: 14_785 nanoseconds. + Weight::from_parts(16_393_818, 1668) + // Standard Error: 612 + .saturating_add(Weight::from_ref_time(76_786).saturating_mul(p.into())) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(3_u64)) + .saturating_add(Weight::from_proof_size(96).saturating_mul(p.into())) + } +} diff --git a/pallets/subtensor/Cargo.toml b/pallets/subtensor/Cargo.toml index e26bfe660..79acf8202 100644 --- a/pallets/subtensor/Cargo.toml +++ b/pallets/subtensor/Cargo.toml @@ -35,6 +35,10 @@ pallet-transaction-payment = { git = "https://github.com/paritytech/substrate", pallet-utility = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "polkadot-v0.9.39" } ndarray = { version = "0.15.0", default-features = false } +# Used for sudo decentralization +pallet-collective = { version = "4.0.0-dev", default-features = false, path = "../collective" } +pallet-membership = {version = "4.0.0-dev", default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.39" } + [dev-dependencies] pallet-balances = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "polkadot-v0.9.39", features = ["std"] } sp-version = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "polkadot-v0.9.39" } @@ -52,6 +56,8 @@ std = [ "frame-support/std", "frame-system/std", "scale-info/std", + "pallet-collective/std", + "pallet-membership/std", ] runtime-benchmarks = ["frame-benchmarking/runtime-benchmarks"] try-runtime = ["frame-support/try-runtime"] diff --git a/pallets/subtensor/src/benchmarks.rs b/pallets/subtensor/src/benchmarks.rs index fb3a4064a..95897be00 100644 --- a/pallets/subtensor/src/benchmarks.rs +++ b/pallets/subtensor/src/benchmarks.rs @@ -18,10 +18,6 @@ benchmarks! { // Add individual benchmarks here benchmark_register { - - // This is a whitelisted caller who can make transaction without weights. - let caller: T::AccountId = whitelisted_caller::>(); - // Lets create a single network. let n: u16 = 10; let netuid: u16 = 1; //11 is the benchmark network. @@ -31,21 +27,19 @@ benchmarks! { let block_number: u64 = Subtensor::::get_current_block_as_u64(); let start_nonce: u64 = (39420842u64 + 100u64*netuid as u64).into(); - let (nonce, work): (u64, Vec) = Subtensor::::create_work_for_block_number( netuid, block_number, start_nonce, &caller); + let hotkey: T::AccountId = account("Alice", 0, seed); + let (nonce, work): (u64, Vec) = Subtensor::::create_work_for_block_number( netuid, block_number, start_nonce, &hotkey); assert_ok!(Subtensor::::do_add_network( RawOrigin::Root.into(), netuid.try_into().unwrap(), tempo.into(), modality.into())); - assert_ok!(Subtensor::::do_sudo_set_network_registration_allowed( RawOrigin::Root.into(), netuid.try_into().unwrap(), true.into())); + assert_ok!(Subtensor::::do_sudo_set_network_registration_allowed( RawOrigin::Root.into(), netuid.try_into().unwrap(), true.into())); let block_number: u64 = Subtensor::::get_current_block_as_u64(); let coldkey: T::AccountId = account("Test", 0, seed); - - }: register( RawOrigin::Signed( caller.clone() ), netuid, block_number, nonce, work, caller.clone(), coldkey ) + }: register( RawOrigin::Signed( hotkey.clone() ), netuid, block_number, nonce, work, hotkey.clone(), coldkey.clone() ) benchmark_set_weights { // This is a whitelisted caller who can make transaction without weights. - let caller: T::AccountId = whitelisted_caller::>(); - let caller_origin = ::RuntimeOrigin::from(RawOrigin::Signed(caller.clone())); let netuid: u16 = 1; let version_key: u64 = 1; let tempo: u16 = 1; @@ -64,17 +58,15 @@ benchmarks! { let signer : T::AccountId = account("Alice", 0, seed); for id in 0..4096 as u16 { - let block_number: u64 = Subtensor::::get_current_block_as_u64(); - let start_nonce: u64 = (39420842u64 + 100u64*id as u64).into(); - let hotkey: T::AccountId = account("Alice", 0, seed); let coldkey: T::AccountId = account("Test", 0, seed); - let (nonce, work): (u64, Vec) = Subtensor::::create_work_for_block_number( id, block_number, start_nonce, &hotkey); seed = seed +1; - let block_number: u64 = Subtensor::::get_current_block_as_u64(); - - assert_ok!( Subtensor::::do_registration(RawOrigin::Signed( hotkey.clone() ).into(), netuid.try_into().unwrap(), block_number, nonce, work, hotkey.clone(), coldkey )); + Subtensor::::set_burn(netuid, 1); + let amoun_to_be_staked = Subtensor::::u64_to_balance( 1000000 ); + Subtensor::::add_balance_to_coldkey_account(&coldkey.clone(), amoun_to_be_staked.unwrap()); + + Subtensor::::do_burned_registration(RawOrigin::Signed(coldkey.clone()).into(), netuid, hotkey.clone())?; let uid = Subtensor::::get_uid_for_net_and_hotkey(netuid, &hotkey.clone()).unwrap(); Subtensor::::set_validator_permit_for_uid(netuid, uid.clone(), true); @@ -96,20 +88,20 @@ benchmarks! { let seed : u32 = 1; assert_ok!( Subtensor::::do_add_network( RawOrigin::Root.into(), netuid.try_into().unwrap(), tempo.into(), modality.into())); + Subtensor::::set_burn(netuid, 1); Subtensor::::set_max_allowed_uids( netuid, 4096 ); - assert_ok!(Subtensor::::do_sudo_set_network_registration_allowed( RawOrigin::Root.into(), netuid.try_into().unwrap(), true.into())); + assert_ok!(Subtensor::::do_sudo_set_network_registration_allowed( RawOrigin::Root.into(), netuid.try_into().unwrap(), true.into())); assert_eq!(Subtensor::::get_max_allowed_uids(netuid), 4096); - let block_number: u64 = Subtensor::::get_current_block_as_u64(); - let start_nonce: u64 = (39420842u64 + 100u64*netuid as u64).into(); let coldkey: T::AccountId = account("Test", 0, seed); - let (nonce, work): (u64, Vec) = Subtensor::::create_work_for_block_number( netuid, block_number, start_nonce, &caller); - - assert_ok!( Subtensor::::do_registration(caller_origin.clone(), netuid.try_into().unwrap(), block_number, nonce, work, caller.clone(), coldkey.clone() )); + let hotkey: T::AccountId = account("Alice", 0, seed); - }: become_delegate(RawOrigin::Signed( coldkey.clone() ), caller.clone()) + let amoun_to_be_staked = Subtensor::::u64_to_balance( 1000000000); + Subtensor::::add_balance_to_coldkey_account(&coldkey.clone(), amoun_to_be_staked.unwrap()); + assert_ok!(Subtensor::::do_burned_registration(RawOrigin::Signed(coldkey.clone()).into(), netuid, hotkey.clone())); + }: become_delegate(RawOrigin::Signed( coldkey.clone() ), hotkey.clone()) benchmark_add_stake { let caller: T::AccountId = whitelisted_caller::>(); @@ -121,24 +113,22 @@ benchmarks! { let seed : u32 = 1; assert_ok!( Subtensor::::do_add_network( RawOrigin::Root.into(), netuid.try_into().unwrap(), tempo.into(), modality.into())); - assert_ok!(Subtensor::::do_sudo_set_network_registration_allowed( RawOrigin::Root.into(), netuid.try_into().unwrap(), true.into())); + + Subtensor::::set_burn(netuid, 1); + assert_ok!(Subtensor::::do_sudo_set_network_registration_allowed( RawOrigin::Root.into(), netuid.try_into().unwrap(), true.into())); Subtensor::::set_max_allowed_uids( netuid, 4096 ); assert_eq!(Subtensor::::get_max_allowed_uids(netuid), 4096); - let block_number: u64 = Subtensor::::get_current_block_as_u64(); - let start_nonce: u64 = (39420842u64 + 100u64*netuid as u64).into(); let coldkey: T::AccountId = account("Test", 0, seed); - let (nonce, work): (u64, Vec) = Subtensor::::create_work_for_block_number( netuid, block_number, start_nonce, &caller); - - assert_ok!( Subtensor::::do_registration(caller_origin.clone(), netuid.try_into().unwrap(), block_number, nonce, work, caller.clone(), coldkey.clone() )); + let hotkey: T::AccountId = account("Alice", 0, seed); let amount: u64 = 1; let amoun_to_be_staked = Subtensor::::u64_to_balance( 1000000000); - Subtensor::::add_balance_to_coldkey_account(&coldkey.clone(), amoun_to_be_staked.unwrap()); - }: add_stake(RawOrigin::Signed( coldkey.clone() ), caller, amount) + assert_ok!(Subtensor::::do_burned_registration(RawOrigin::Signed(coldkey.clone()).into(), netuid, hotkey.clone())); + }: add_stake(RawOrigin::Signed( coldkey.clone() ), hotkey, amount) benchmark_remove_stake{ let caller: T::AccountId = whitelisted_caller::>(); @@ -149,32 +139,43 @@ benchmarks! { let modality: u16 = 0; let seed : u32 = 1; + // Set our total stake to 1000 TAO + Subtensor::::increase_total_stake(1_000_000_000_000); + assert_ok!( Subtensor::::do_add_network( RawOrigin::Root.into(), netuid.try_into().unwrap(), tempo.into(), modality.into())); - assert_ok!(Subtensor::::do_sudo_set_network_registration_allowed( RawOrigin::Root.into(), netuid.try_into().unwrap(), true.into())); + assert_ok!(Subtensor::::do_sudo_set_network_registration_allowed( RawOrigin::Root.into(), netuid.try_into().unwrap(), true.into())); Subtensor::::set_max_allowed_uids( netuid, 4096 ); assert_eq!(Subtensor::::get_max_allowed_uids(netuid), 4096); - let block_number: u64 = Subtensor::::get_current_block_as_u64(); - let start_nonce: u64 = (39420842u64 + 100u64*netuid as u64).into(); let coldkey: T::AccountId = account("Test", 0, seed); - let (nonce, work): (u64, Vec) = Subtensor::::create_work_for_block_number( netuid, block_number, start_nonce, &caller); + let hotkey: T::AccountId = account("Alice", 0, seed); + Subtensor::::set_burn(netuid, 1); - assert_ok!( Subtensor::::do_registration(caller_origin.clone(), netuid.try_into().unwrap(), block_number, nonce, work, caller.clone(), coldkey.clone() )); + let wallet_bal = Subtensor::::u64_to_balance(1000000); + Subtensor::::add_balance_to_coldkey_account(&coldkey.clone(), wallet_bal.unwrap()); - let amoun_to_be_staked = Subtensor::::u64_to_balance( 1000000000); - Subtensor::::add_balance_to_coldkey_account(&coldkey.clone(), amoun_to_be_staked.unwrap()); + assert_ok!(Subtensor::::do_burned_registration(RawOrigin::Signed(coldkey.clone()).into(), netuid, hotkey.clone())); + assert_ok!(Subtensor::::do_become_delegate(RawOrigin::Signed(coldkey.clone()).into(), hotkey.clone(), Subtensor::::get_default_take())); - assert_ok!( Subtensor::::add_stake(RawOrigin::Signed( coldkey.clone() ).into() , caller.clone(), 1000)); + // Stake 10% of our current total staked TAO + let u64_staked_amt = 100_000_000_000; + let amount_to_be_staked = Subtensor::::u64_to_balance(u64_staked_amt); + Subtensor::::add_balance_to_coldkey_account(&coldkey.clone(), amount_to_be_staked.unwrap()); - let amount_unstaked: u64 = 1; + assert_ok!( Subtensor::::add_stake(RawOrigin::Signed( coldkey.clone() ).into() , hotkey.clone(), u64_staked_amt)); - }: remove_stake(RawOrigin::Signed( coldkey.clone() ), caller.clone(), amount_unstaked) + let amount_unstaked: u64 = u64_staked_amt - 1; + assert_ok!(Subtensor::::do_join_senate(RawOrigin::Signed(coldkey.clone()).into(), &hotkey)); + }: remove_stake(RawOrigin::Signed( coldkey.clone() ), hotkey.clone(), amount_unstaked) benchmark_serve_axon{ let caller: T::AccountId = whitelisted_caller::>(); let caller_origin = ::RuntimeOrigin::from(RawOrigin::Signed(caller.clone())); let netuid: u16 = 1; + let tempo: u16 = 1; + let modality: u16 = 0; + let version: u32 = 2; let ip: u128 = 1676056785; let port: u16 = 128; @@ -183,6 +184,16 @@ benchmarks! { let placeholder1: u8 = 0; let placeholder2: u8 = 0; + assert_ok!(Subtensor::::do_add_network(RawOrigin::Root.into(), netuid.try_into().unwrap(), tempo.into(), modality.into())); + Subtensor::::set_max_allowed_uids( netuid, 4096 ); + assert_eq!(Subtensor::::get_max_allowed_uids(netuid), 4096); + + Subtensor::::set_burn(netuid, 1); + let amoun_to_be_staked = Subtensor::::u64_to_balance( 1000000 ); + Subtensor::::add_balance_to_coldkey_account(&caller.clone(), amoun_to_be_staked.unwrap()); + + assert_ok!(Subtensor::::do_burned_registration(caller_origin.clone(), netuid, caller.clone())); + Subtensor::::set_serving_rate_limit(netuid, 0); }: serve_axon(RawOrigin::Signed( caller.clone() ), netuid, version, ip, port, ip_type, protocol, placeholder1, placeholder2) @@ -191,12 +202,23 @@ benchmarks! { let caller: T::AccountId = whitelisted_caller::>(); let caller_origin = ::RuntimeOrigin::from(RawOrigin::Signed(caller.clone())); let netuid: u16 = 1; + let tempo: u16 = 1; + let modality: u16 = 0; + let version: u32 = 2; let ip: u128 = 1676056785; let port: u16 = 128; let ip_type: u8 = 4; - + assert_ok!(Subtensor::::do_add_network(RawOrigin::Root.into(), netuid.try_into().unwrap(), tempo.into(), modality.into())); + Subtensor::::set_max_allowed_uids( netuid, 4096 ); + assert_eq!(Subtensor::::get_max_allowed_uids(netuid), 4096); + + Subtensor::::set_burn(netuid, 1); + let amoun_to_be_staked = Subtensor::::u64_to_balance( 1000000 ); + Subtensor::::add_balance_to_coldkey_account(&caller.clone(), amoun_to_be_staked.unwrap()); + + assert_ok!(Subtensor::::do_burned_registration(caller_origin.clone(), netuid, caller.clone())); Subtensor::::set_serving_rate_limit(netuid, 0); }: serve_prometheus(RawOrigin::Signed( caller.clone() ), netuid, version, ip, port, ip_type) @@ -417,7 +439,7 @@ benchmarks! { let netuid: u16 = 1; let tempo: u16 = 1; let modality: u16 = 0; - let max_allowed_uids: u16 = 4096; + let max_allowed_uids: u16 = 4097; assert_ok!( Subtensor::::do_add_network( RawOrigin::Root.into(), netuid.try_into().unwrap(), tempo.into(), modality.into())); @@ -550,7 +572,9 @@ benchmarks! { let coldkey: T::AccountId = account("Test", 0, seed); let modality: u16 = 0; let tempo: u16 = 1; + assert_ok!( Subtensor::::do_add_network( RawOrigin::Root.into(), netuid.try_into().unwrap(), tempo.into(), modality.into())); + Subtensor::::set_burn(netuid, 1); let amoun_to_be_staked = Subtensor::::u64_to_balance( 1000000); Subtensor::::add_balance_to_coldkey_account(&coldkey.clone(), amoun_to_be_staked.unwrap()); @@ -597,6 +621,88 @@ benchmarks! { }: sudo_set_min_burn(RawOrigin::>::Root, netuid, min_burn) + benchmark_join_senate { + let netuid: u16 = 1; + let mut seed : u32 = 1; + let hotkey: T::AccountId = account("Alice", 0, seed); + let coldkey: T::AccountId = account("Test", 0, seed); + let modality: u16 = 0; + let tempo: u16 = 1; + + assert_ok!( Subtensor::::do_add_network( RawOrigin::Root.into(), netuid.try_into().unwrap(), tempo.into(), modality.into())); + Subtensor::::set_max_allowed_uids( netuid, 4096 ); + + assert_ok!(Subtensor::::do_sudo_set_max_registrations_per_block(RawOrigin::Root.into(), netuid.try_into().unwrap(), 4096 )); + Subtensor::::set_target_registrations_per_interval(netuid.try_into().unwrap(), 4096); + + for id in 0..10 as u16 { + seed = seed + 1; + + let hotkey: T::AccountId = account("Alice", 0, seed); + let coldkey: T::AccountId = account("Test", 0, seed); + + Subtensor::::set_burn(netuid, 1); + let amoun_to_be_staked = Subtensor::::u64_to_balance( 1000000 ); + Subtensor::::add_balance_to_coldkey_account(&coldkey.clone(), amoun_to_be_staked.unwrap()); + + assert_ok!(Subtensor::::do_burned_registration(RawOrigin::Signed(coldkey.clone()).into(), netuid, hotkey.clone())); + assert_ok!(Subtensor::::do_become_delegate(RawOrigin::Signed( coldkey.clone() ).into(), hotkey.clone(), Subtensor::::get_default_take())); + + Subtensor::::increase_stake_on_hotkey_account(&hotkey.clone(), 1000000000); + assert_ok!(Subtensor::::do_join_senate(RawOrigin::Signed(coldkey.clone()).into(), &hotkey)); + } + + let amoun_to_be_staked = Subtensor::::u64_to_balance( 1000000 ); + Subtensor::::add_balance_to_coldkey_account(&coldkey.clone(), amoun_to_be_staked.unwrap()); + + assert_ok!(Subtensor::::do_burned_registration(RawOrigin::Signed(coldkey.clone()).into(), netuid, hotkey.clone())); + assert_ok!(Subtensor::::do_become_delegate(RawOrigin::Signed( coldkey.clone() ).into(), hotkey.clone(), Subtensor::::get_default_take())); + + Subtensor::::increase_stake_on_hotkey_account(&hotkey.clone(), 10000000000); + }: join_senate(RawOrigin::Signed( coldkey.clone() ), hotkey.clone()) + + benchmark_leave_senate { + let netuid: u16 = 1; + let mut seed : u32 = 1; + let hotkey: T::AccountId = account("Alice", 0, seed); + let coldkey: T::AccountId = account("Test", 0, seed); + let modality: u16 = 0; + let tempo: u16 = 1; + + assert_ok!( Subtensor::::do_add_network( RawOrigin::Root.into(), netuid.try_into().unwrap(), tempo.into(), modality.into())); + Subtensor::::set_max_allowed_uids( netuid, 4096 ); + + assert_ok!(Subtensor::::do_sudo_set_max_registrations_per_block(RawOrigin::Root.into(), netuid.try_into().unwrap(), 4096 )); + Subtensor::::set_target_registrations_per_interval(netuid.try_into().unwrap(), 4096); + + for id in 0..10 as u16 { + seed = seed + 1; + + let hotkey: T::AccountId = account("Alice", 0, seed); + let coldkey: T::AccountId = account("Test", 0, seed); + + Subtensor::::set_burn(netuid, 1); + let amoun_to_be_staked = Subtensor::::u64_to_balance( 1000000 ); + Subtensor::::add_balance_to_coldkey_account(&coldkey.clone(), amoun_to_be_staked.unwrap()); + + assert_ok!(Subtensor::::do_burned_registration(RawOrigin::Signed(coldkey.clone()).into(), netuid, hotkey.clone())); + assert_ok!(Subtensor::::do_become_delegate(RawOrigin::Signed( coldkey.clone() ).into(), hotkey.clone(), Subtensor::::get_default_take())); + + Subtensor::::increase_stake_on_hotkey_account(&hotkey.clone(), 1000000000); + assert_ok!(Subtensor::::do_join_senate(RawOrigin::Signed(coldkey.clone()).into(), &hotkey)); + } + + let amoun_to_be_staked = Subtensor::::u64_to_balance( 1000000 ); + Subtensor::::add_balance_to_coldkey_account(&coldkey.clone(), amoun_to_be_staked.unwrap()); + + assert_ok!(Subtensor::::do_burned_registration(RawOrigin::Signed(coldkey.clone()).into(), netuid, hotkey.clone())); + assert_ok!(Subtensor::::do_become_delegate(RawOrigin::Signed( coldkey.clone() ).into(), hotkey.clone(), Subtensor::::get_default_take())); + + Subtensor::::increase_stake_on_hotkey_account(&hotkey.clone(), 10000000000); + + assert_ok!(Subtensor::::do_join_senate(RawOrigin::Signed(coldkey.clone()).into(), &hotkey)); + }: leave_senate(RawOrigin::Signed(coldkey.clone()), hotkey.clone()) + benchmark_sudo_set_registration_allowed { let netuid: u16 = 1; let tempo: u16 = 1; @@ -615,4 +721,3 @@ benchmarks! { }: sudo_set_tempo(RawOrigin::>::Root, netuid, tempo) } - diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index 42c3634c1..0e2b08f43 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -14,7 +14,9 @@ use frame_support::{ dispatch, dispatch::{ DispatchInfo, - PostDispatchInfo + PostDispatchInfo, + DispatchResult, + DispatchError }, ensure, traits::{ Currency, @@ -23,7 +25,7 @@ use frame_support::{ WithdrawReasons }, IsSubType, - } + } }; use sp_std::marker::PhantomData; @@ -33,7 +35,7 @@ use sp_runtime::{ Dispatchable, DispatchInfoOf, SignedExtension, - PostDispatchInfoOf + PostDispatchInfoOf, }, transaction_validity::{ TransactionValidity, @@ -63,21 +65,32 @@ mod staking; mod utils; mod uids; mod weights; +mod senate; pub mod delegate_info; pub mod neuron_info; pub mod subnet_info; +// apparently this is stabilized since rust 1.36 +extern crate alloc; mod migration; #[frame_support::pallet] pub mod pallet { - use frame_support::pallet_prelude::*; + use frame_support::{ + dispatch::GetDispatchInfo, + pallet_prelude::{*, StorageMap, DispatchResult} + }; use frame_system::pallet_prelude::*; - use frame_support::traits::Currency; + use frame_support::traits::{Currency, UnfilteredDispatchable}; use frame_support::sp_std::vec; use frame_support::inherent::Vec; + #[cfg(not(feature = "std"))] + use alloc::boxed::Box; + #[cfg(feature = "std")] + use sp_std::prelude::Box; + // Tracks version for migrations. Should be monotonic with respect to the // order of migrations. (i.e. always increasing) const STORAGE_VERSION: StorageVersion = StorageVersion::new(1); @@ -94,9 +107,21 @@ pub mod pallet { // Because this pallet emits events, it depends on the runtime's definition of an event. type RuntimeEvent: From> + IsType<::RuntimeEvent>; + /// A sudo-able call. + type SudoRuntimeCall: Parameter + + UnfilteredDispatchable + + GetDispatchInfo; + + /// Origin checking for council majority + type CouncilOrigin: EnsureOrigin; + // --- Currency type that will be used to place deposits on neurons type Currency: Currency + Send + Sync; + type SenateMembers: crate::MemberManagement; + + type TriumvirateInterface: crate::CollectiveInterface; + // ================================= // ==== Initial Value Constants ==== // ================================= @@ -172,10 +197,19 @@ pub mod pallet { type InitialServingRateLimit: Get; #[pallet::constant] // Initial transaction rate limit. type InitialTxRateLimit: Get; + #[pallet::constant] // Initial percentage of total stake required to join senate. + type InitialSenateRequiredStakePercentage: Get; } pub type AccountIdOf = ::AccountId; + // Senate requirements + #[pallet::type_value] + pub fn DefaultSenateRequiredStakePercentage() -> u64 { T::InitialSenateRequiredStakePercentage::get() } + + #[pallet::storage] // --- ITEM ( tx_rate_limit ) + pub(super) type SenateRequiredStakePercentage = StorageValue<_, u64, ValueQuery, DefaultSenateRequiredStakePercentage>; + // ============================ // ==== Staking + Accounts ==== // ============================ @@ -314,7 +348,7 @@ pub mod pallet { pub type BlocksSinceLastStep = StorageMap<_, Identity, u16, u64, ValueQuery, DefaultBlocksSinceLastStep>; #[pallet::storage] // --- MAP ( netuid ) --> last_mechanism_step_block pub type LastMechansimStepBlock = StorageMap<_, Identity, u16, u64, ValueQuery, DefaultLastMechansimStepBlock >; - + // ================================= // ==== Axon / Promo Endpoints ===== // ================================= @@ -577,6 +611,7 @@ pub mod pallet { MaxBurnSet( u16, u64 ), // --- Event created when setting max burn on a network. MinBurnSet( u16, u64 ), // --- Event created when setting min burn on a network. TxRateLimitSet( u64 ), // --- Event created when setting the transaction rate limit. + Sudid ( DispatchResult ), // --- Event created when a sudo call is done. RegistrationAllowed( u16, bool ), // --- Event created when registration is allowed/disallowed for a subnet. TempoSet(u16, u16), // --- Event created when setting tempo on a network RAORecycledForRegistrationSet( u16, u64 ), // Event created when setting the RAO recycled for registration. @@ -629,6 +664,11 @@ pub mod pallet { TooManyRegistrationsThisInterval, // --- Thrown when registration attempt exceeds allowed in interval BenchmarkingOnly, // --- Thrown when a function is only available for benchmarking HotkeyOriginMismatch, // --- Thrown when the hotkey passed is not the origin, but it should be + // Senate errors + NotSenateMember, // --- Thrown when a hotkey attempts to do something only senate members can do + AlreadySenateMember, // --- Thrown when a hotkey attempts to join the senate while already being a member + BelowStakeThreshold, // --- Thrown when a hotkey attempts to join the senate without enough stake + NotDelegate, // --- Thrown when a hotkey attempts to join the senate without being a delegate first IncorrectNetuidsLength, // --- Thrown when an incorrect amount of Netuids are passed as input } @@ -961,9 +1001,10 @@ pub mod pallet { // // #[pallet::call_index(3)] - #[pallet::weight((Weight::from_ref_time(66_000_000) - .saturating_add(T::DbWeight::get().reads(8)) - .saturating_add(T::DbWeight::get().writes(6)), DispatchClass::Normal, Pays::No))] + #[pallet::weight((Weight::from_ref_time(63_000_000) + .saturating_add(Weight::from_proof_size(43991)) + .saturating_add(T::DbWeight::get().reads(14)) + .saturating_add(T::DbWeight::get().writes(9)), DispatchClass::Normal, Pays::No))] pub fn remove_stake( origin: OriginFor, hotkey: T::AccountId, @@ -1567,6 +1608,92 @@ pub mod pallet { #[pallet::weight((0, DispatchClass::Operational, Pays::No))] pub fn sudo_set_rao_recycled(origin: OriginFor, netuid: u16, rao_recycled: u64 ) -> DispatchResult { Self::do_set_rao_recycled(origin, netuid, rao_recycled) + } + + /// Authenticates a council proposal and dispatches a function call with `Root` origin. + /// + /// The dispatch origin for this call must be a council majority. + /// + /// ## Complexity + /// - O(1). + #[pallet::call_index(51)] + #[pallet::weight((Weight::from_ref_time(0), DispatchClass::Operational, Pays::No))] + pub fn sudo( + origin: OriginFor, + call: Box, + ) -> DispatchResultWithPostInfo { + // This is a public call, so we ensure that the origin is a council majority. + T::CouncilOrigin::ensure_origin(origin)?; + + let result = call.dispatch_bypass_filter(frame_system::RawOrigin::Root.into()); + let error = result.map(|_| ()).map_err(|e| e.error); + Self::deposit_event(Event::Sudid(error)); + + return result + } + + /// Authenticates a council proposal and dispatches a function call with `Root` origin. + /// This function does not check the weight of the call, and instead allows the + /// user to specify the weight of the call. + /// + /// The dispatch origin for this call must be a council majority. + /// + /// ## Complexity + /// - O(1). + #[pallet::call_index(52)] + #[pallet::weight((*_weight, call.get_dispatch_info().class, Pays::No))] + pub fn sudo_unchecked_weight( + origin: OriginFor, + call: Box, + _weight: Weight, + ) -> DispatchResultWithPostInfo { + // This is a public call, so we ensure that the origin is a council majority. + T::CouncilOrigin::ensure_origin(origin)?; + + let result = call.dispatch_bypass_filter(frame_system::RawOrigin::Root.into()); + let error = result.map(|_| ()).map_err(|e| e.error); + Self::deposit_event(Event::Sudid(error)); + + return result + } + + #[pallet::call_index(53)] + #[pallet::weight((Weight::from_ref_time(67_000_000) + .saturating_add(Weight::from_proof_size(61173)) + .saturating_add(T::DbWeight::get().reads(20)) + .saturating_add(T::DbWeight::get().writes(3)), DispatchClass::Normal, Pays::No))] + pub fn join_senate( + origin: OriginFor, + hotkey: T::AccountId + ) -> DispatchResult { + Self::do_join_senate(origin, &hotkey) + } + + #[pallet::call_index(54)] + #[pallet::weight((Weight::from_ref_time(20_000_000) + .saturating_add(Weight::from_proof_size(4748)) + .saturating_add(T::DbWeight::get().reads(4)) + .saturating_add(T::DbWeight::get().writes(3)), DispatchClass::Normal, Pays::No))] + pub fn leave_senate( + origin: OriginFor, + hotkey: T::AccountId + ) -> DispatchResult { + Self::do_leave_senate(origin, &hotkey) + } + + #[pallet::call_index(55)] + #[pallet::weight((Weight::from_ref_time(0) + .saturating_add(Weight::from_proof_size(0)) + .saturating_add(T::DbWeight::get().reads(0)) + .saturating_add(T::DbWeight::get().writes(0)), DispatchClass::Operational))] + pub fn vote( + origin: OriginFor, + hotkey: T::AccountId, + proposal: T::Hash, + #[pallet::compact] index: u32, + approve: bool, + ) -> DispatchResultWithPostInfo { + Self::do_vote_senate(origin, &hotkey, proposal, index, approve) } // Sudo call for setting registration allowed @@ -1820,3 +1947,59 @@ impl SignedExtension for SubtensorSignedExte } } + +use frame_support::{inherent::Vec, sp_std::vec}; + +/// Trait for managing a membership pallet instance in the runtime +pub trait MemberManagement { + /// Add member + fn add_member(account: &AccountId) -> DispatchResult; + + /// Remove a member + fn remove_member(account: &AccountId) -> DispatchResult; + + /// Swap member + fn swap_member(remove: &AccountId, add: &AccountId) -> DispatchResult; + + /// Get all members + fn members() -> Vec; + + /// Check if an account is apart of the set + fn is_member(account: &AccountId) -> bool; + + /// Get our maximum member count + fn max_members() -> u32; +} + +impl MemberManagement for () { + /// Add member + fn add_member(_: &T) -> DispatchResult {Ok(())} + + // Remove a member + fn remove_member(_: &T) -> DispatchResult {Ok(())} + + // Swap member + fn swap_member(_: &T, _: &T) -> DispatchResult {Ok(())} + + // Get all members + fn members() -> Vec {vec![]} + + // Check if an account is apart of the set + fn is_member(_: &T) -> bool {false} + + fn max_members() -> u32 {0} +} + +/// Trait for interacting with collective pallets +pub trait CollectiveInterface { + /// Remove vote + fn remove_votes(hotkey: &AccountId) -> Result; + + fn add_vote(hotkey: &AccountId, proposal: Hash, index: ProposalIndex, approve: bool) -> Result; +} + +impl CollectiveInterface for () { + fn remove_votes(_: &T) -> Result {Ok(true)} + + fn add_vote(_: &T, _: H, _: P, _: bool) -> Result {Ok(true)} +} \ No newline at end of file diff --git a/pallets/subtensor/src/senate.rs b/pallets/subtensor/src/senate.rs new file mode 100644 index 000000000..abf686ed3 --- /dev/null +++ b/pallets/subtensor/src/senate.rs @@ -0,0 +1,102 @@ +use super::*; +use frame_support::{dispatch::Pays, pallet_prelude::{DispatchResult, DispatchResultWithPostInfo, Weight}}; +use frame_system::{ensure_signed}; +use sp_core::Get; + +impl Pallet { + /// do_join_senate(RuntimeOrigin hotkey) + /// + /// This extrinsic allows top delegates to join the senate, a group of accounts that votes on proposals from the Triumvirate. + /// In order to join the senate, the hotkey must: + /// + /// 1) Be registered as a delegate + /// 2) Control greater than 2% of the total staked volume. + pub fn do_join_senate( + origin: T::RuntimeOrigin, + hotkey: &T::AccountId + ) -> DispatchResult { + let coldkey = ensure_signed( origin )?; + log::info!("do_join_senate( coldkey: {:?}, hotkey: {:?} )", coldkey, hotkey ); + + // Ensure that the pairing is correct. + ensure!( Self::coldkey_owns_hotkey( &coldkey, &hotkey ), Error::::NonAssociatedColdKey ); + + // Check all our senate requirements + ensure!(Self::is_hotkey_registered_on_any_network(&hotkey), Error::::NotRegistered); + ensure!(!T::SenateMembers::is_member(&hotkey), Error::::AlreadySenateMember); + ensure!(Self::hotkey_is_delegate(&hotkey), Error::::NotDelegate); + + let total_stake = Self::get_total_stake(); + let current_stake = Self::get_total_stake_for_hotkey(&hotkey); + ensure!(total_stake > 0 && current_stake > 0, Error::::BelowStakeThreshold); + + ensure!(current_stake * 100 / total_stake >= SenateRequiredStakePercentage::::get(), Error::::BelowStakeThreshold); + + // If we're full, we'll swap out the lowest stake member. + let members = T::SenateMembers::members(); + if (members.len() as u32) == T::SenateMembers::max_members() { + let mut sorted_members = members.clone(); + sorted_members.sort_by(|a, b| { + let a_stake = Self::get_total_stake_for_hotkey(a); + let b_stake = Self::get_total_stake_for_hotkey(b); + + b_stake.cmp(&a_stake) + }); + + if let Some(last) = sorted_members.last() { + let last_stake = Self::get_total_stake_for_hotkey(last); + + ensure!(last_stake < current_stake, Error::::BelowStakeThreshold); + + return T::SenateMembers::swap_member(last, &hotkey); + } + } + + // Since we're calling another extrinsic, we want to propagate our errors back up the call stack. + T::SenateMembers::add_member(&hotkey) + } + + pub fn do_leave_senate( + origin: T::RuntimeOrigin, + hotkey: &T::AccountId + ) -> DispatchResult { + let coldkey = ensure_signed( origin )?; + log::info!("do_leave_senate( coldkey: {:?} hotkey:{:?} )", coldkey, hotkey ); + + // Ensure that the pairing is correct. + ensure!( Self::coldkey_owns_hotkey( &coldkey, &hotkey ), Error::::NonAssociatedColdKey ); + + // Check all our leave requirements + ensure!(T::SenateMembers::is_member(&hotkey), Error::::NotSenateMember); + + T::TriumvirateInterface::remove_votes(&hotkey); + T::SenateMembers::remove_member(&hotkey) + } + + pub fn do_vote_senate( + origin: T::RuntimeOrigin, + hotkey: &T::AccountId, + proposal: T::Hash, + index: u32, + approve: bool + ) -> DispatchResultWithPostInfo { + let coldkey = ensure_signed(origin.clone())?; + // Ensure that the pairing is correct. + ensure!( Self::coldkey_owns_hotkey( &coldkey, &hotkey ), Error::::NonAssociatedColdKey ); + ensure!(T::SenateMembers::is_member(&hotkey), Error::::NotSenateMember); + + let members = T::SenateMembers::members(); + // Detects first vote of the member in the motion + let is_account_voting_first_time = T::TriumvirateInterface::add_vote(hotkey, proposal, index, approve)?; + + // Calculate extrinsic weight + let member_count = members.len() as u32; + let vote_weight = Weight::from_parts(20_528_275, 4980) + .saturating_add(Weight::from_ref_time(48_856).saturating_mul(member_count.into())) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + .saturating_add(Weight::from_proof_size(128).saturating_mul(member_count.into())); + + Ok((Some(vote_weight), if is_account_voting_first_time { Pays::No } else { Pays::Yes }).into()) + } +} \ No newline at end of file diff --git a/pallets/subtensor/src/staking.rs b/pallets/subtensor/src/staking.rs index c89d20bf0..6360a1e63 100644 --- a/pallets/subtensor/src/staking.rs +++ b/pallets/subtensor/src/staking.rs @@ -209,6 +209,17 @@ impl Pallet { // Set last block for rate limiting Self::set_last_tx_block(&coldkey, block); + // If this hotkey is a senator, check to see if they fall below stake threshold in this withdraw + if T::SenateMembers::is_member(&hotkey) && + Self::get_total_stake_for_hotkey(&hotkey) * 100 / { + if Self::get_total_stake() == 0 {1} else {Self::get_total_stake()} + } < SenateRequiredStakePercentage::::get() + { + // This might cause a panic, but there shouldn't be any reason this will fail with the checks above. + T::TriumvirateInterface::remove_votes(&hotkey); + T::SenateMembers::remove_member(&hotkey)?; + } + // --- 9. Emit the unstaking event. log::info!("StakeRemoved( hotkey:{:?}, stake_to_be_removed:{:?} )", hotkey, stake_to_be_removed ); Self::deposit_event( Event::StakeRemoved( hotkey, stake_to_be_removed ) ); diff --git a/pallets/subtensor/src/utils.rs b/pallets/subtensor/src/utils.rs index f1b0e4a57..a692cec7f 100644 --- a/pallets/subtensor/src/utils.rs +++ b/pallets/subtensor/src/utils.rs @@ -1,12 +1,12 @@ use super::*; -use frame_support::inherent::Vec; +use frame_support::{inherent::Vec}; use sp_core::U256; use frame_support::pallet_prelude::DispatchResult; use crate::system::ensure_root; impl Pallet { - + // ======================== // ==== Global Setters ==== // ======================== @@ -524,7 +524,7 @@ impl Pallet { Self::deposit_event( Event::RAORecycledForRegistrationSet( netuid, rao_recycled ) ); Ok(()) } - } + diff --git a/pallets/subtensor/tests/mock.rs b/pallets/subtensor/tests/mock.rs index 9b454cd47..771ffbc34 100644 --- a/pallets/subtensor/tests/mock.rs +++ b/pallets/subtensor/tests/mock.rs @@ -1,14 +1,17 @@ use frame_support::{assert_ok, parameter_types, traits::{Everything, Hooks}, weights}; -use frame_system::{limits}; -use frame_support::traits::{StorageMapShim}; +use frame_system::{limits, EnsureNever, EnsureRoot, RawOrigin}; +use frame_support::traits::{StorageMapShim, Hash}; use frame_system as system; use frame_system::Config; -use sp_core::{H256, U256}; +use sp_core::{H256, U256, Get}; use sp_runtime::{ testing::Header, traits::{BlakeTwo256, IdentityLookup}, + DispatchResult }; +use pallet_collective::MemberCount; + type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; type Block = frame_system::mocking::MockBlock; @@ -21,6 +24,10 @@ frame_support::construct_runtime!( { System: frame_system::{Pallet, Call, Config, Storage, Event}, Balances: pallet_balances::{Pallet, Call, Config, Storage, Event}, + Triumvirate: pallet_collective::::{Pallet, Call, Storage, Origin, Event, Config}, + TriumvirateMembers: pallet_membership::::{Pallet, Call, Storage, Event, Config}, + Senate: pallet_collective::::{Pallet, Call, Storage, Origin, Event, Config}, + SenateMembers: pallet_membership::::{Pallet, Call, Storage, Event, Config}, SubtensorModule: pallet_subtensor::{Pallet, Call, Storage, Event}, Utility: pallet_utility::{Pallet, Call, Storage, Event}, } @@ -43,6 +50,9 @@ parameter_types! { #[allow(dead_code)] pub type AccountId = U256; +// The address format for describing accounts. +pub type Address = AccountId; + // Balance of an account. #[allow(dead_code)] pub type Balance = u64; @@ -144,11 +154,157 @@ parameter_types! { pub const InitialMaxDifficulty: u64 = u64::MAX; pub const InitialRAORecycledForRegistration: u64 = 0; + pub const InitialSenateRequiredStakePercentage: u64 = 2; // 2 percent of total stake +} + +// Configure collective pallet for council +parameter_types! { + pub const CouncilMotionDuration: BlockNumber = 100; + pub const CouncilMaxProposals: u32 = 10; + pub const CouncilMaxMembers: u32 = 3; +} + +// Configure collective pallet for Senate +parameter_types! { + pub const SenateMaxMembers: u32 = 10; +} + +use pallet_collective::{CanPropose, CanVote, GetVotingMembers}; +pub struct CanProposeToTriumvirate; +impl CanPropose for CanProposeToTriumvirate { + fn can_propose(account: &AccountId) -> bool { + Triumvirate::is_member(account) + } +} + +pub struct CanVoteToTriumvirate; +impl CanVote for CanVoteToTriumvirate { + fn can_vote(_: &AccountId) -> bool { + //Senate::is_member(account) + false // Disable voting from pallet_collective::vote + } +} + +use pallet_subtensor::{MemberManagement, CollectiveInterface}; +pub struct ManageSenateMembers; +impl MemberManagement for ManageSenateMembers { + fn add_member(account: &AccountId) -> DispatchResult { + SenateMembers::add_member(RawOrigin::Root.into(), *account) + } + + fn remove_member(account: &AccountId) -> DispatchResult { + SenateMembers::remove_member(RawOrigin::Root.into(), *account) + } + + fn swap_member(remove: &AccountId, add: &AccountId) -> DispatchResult { + SenateMembers::swap_member(RawOrigin::Root.into(), *remove, *add) + } + + fn is_member(account: &AccountId) -> bool { + Senate::is_member(account) + } + + fn members() -> Vec { + Senate::members() + } + + fn max_members() -> u32 { + SenateMaxMembers::get() + } +} + +pub struct GetSenateMemberCount; +impl GetVotingMembers for GetSenateMemberCount { + fn get_count() -> MemberCount {Senate::members().len() as u32} } +impl Get for GetSenateMemberCount { + fn get() -> MemberCount {SenateMaxMembers::get()} +} + +pub struct TriumvirateVotes; +impl CollectiveInterface for TriumvirateVotes { + fn remove_votes(hotkey: &AccountId) -> Result { + Triumvirate::remove_votes(hotkey) + } + + fn add_vote(hotkey: &AccountId, proposal: Hash, index: u32, approve: bool) -> Result { + Triumvirate::do_vote(hotkey.clone(), proposal, index, approve) + } +} + +// We call pallet_collective TriumvirateCollective +type TriumvirateCollective = pallet_collective::Instance1; +impl pallet_collective::Config for Test { + type RuntimeOrigin = RuntimeOrigin; + type Proposal = RuntimeCall; + type RuntimeEvent = RuntimeEvent; + type MotionDuration = CouncilMotionDuration; + type MaxProposals = CouncilMaxProposals; + type MaxMembers = GetSenateMemberCount; + type DefaultVote = pallet_collective::PrimeDefaultVote; + type WeightInfo = pallet_collective::weights::SubstrateWeight; + type SetMembersOrigin = EnsureNever; + type CanPropose = CanProposeToTriumvirate; + type CanVote = CanVoteToTriumvirate; + type GetVotingMembers = GetSenateMemberCount; +} + +// We call council members Triumvirate +type TriumvirateMembership = pallet_membership::Instance1; +impl pallet_membership::Config for Test { + type RuntimeEvent = RuntimeEvent; + type AddOrigin = EnsureRoot; + type RemoveOrigin = EnsureRoot; + type SwapOrigin = EnsureRoot; + type ResetOrigin = EnsureRoot; + type PrimeOrigin = EnsureRoot; + type MembershipInitialized = Triumvirate; + type MembershipChanged = Triumvirate; + type MaxMembers = CouncilMaxMembers; + type WeightInfo = pallet_membership::weights::SubstrateWeight; +} + +// This is a dummy collective instance for managing senate members +// Probably not the best solution, but fastest implementation +type SenateCollective = pallet_collective::Instance2; +impl pallet_collective::Config for Test { + type RuntimeOrigin = RuntimeOrigin; + type Proposal = RuntimeCall; + type RuntimeEvent = RuntimeEvent; + type MotionDuration = CouncilMotionDuration; + type MaxProposals = CouncilMaxProposals; + type MaxMembers = SenateMaxMembers; + type DefaultVote = pallet_collective::PrimeDefaultVote; + type WeightInfo = pallet_collective::weights::SubstrateWeight; + type SetMembersOrigin = EnsureNever; + type CanPropose = (); + type CanVote = (); + type GetVotingMembers = (); +} + +// We call our top K delegates membership Senate +type SenateMembership = pallet_membership::Instance2; +impl pallet_membership::Config for Test { + type RuntimeEvent = RuntimeEvent; + type AddOrigin = EnsureRoot; + type RemoveOrigin = EnsureRoot; + type SwapOrigin = EnsureRoot; + type ResetOrigin = EnsureRoot; + type PrimeOrigin = EnsureRoot; + type MembershipInitialized = Senate; + type MembershipChanged = Senate; + type MaxMembers = SenateMaxMembers; + type WeightInfo = pallet_membership::weights::SubstrateWeight; +} + impl pallet_subtensor::Config for Test { type RuntimeEvent = RuntimeEvent; type Currency = Balances; type InitialIssuance = InitialIssuance; + type SudoRuntimeCall = TestRuntimeCall; + type CouncilOrigin = frame_system::EnsureSigned; + type SenateMembers = ManageSenateMembers; + type TriumvirateInterface = TriumvirateVotes; type InitialMinAllowedWeights = InitialMinAllowedWeights; type InitialEmissionValue = InitialEmissionValue; @@ -185,6 +341,7 @@ impl pallet_subtensor::Config for Test { type InitialMaxBurn = InitialMaxBurn; type InitialMinBurn = InitialMinBurn; type InitialRAORecycledForRegistration = InitialRAORecycledForRegistration; + type InitialSenateRequiredStakePercentage = InitialSenateRequiredStakePercentage; } impl pallet_utility::Config for Test { diff --git a/pallets/subtensor/tests/senate.rs b/pallets/subtensor/tests/senate.rs new file mode 100644 index 000000000..c77240cbd --- /dev/null +++ b/pallets/subtensor/tests/senate.rs @@ -0,0 +1,492 @@ +mod mock; +use mock::*; + +use sp_core::{H256, U256, bounded_vec}; +use frame_system::{EventRecord, Phase}; +use frame_support::{assert_ok, assert_noop, codec::Encode}; +use sp_runtime::{ + traits::{BlakeTwo256, Hash}, + BuildStorage, +}; + +use frame_system::Config; +use pallet_subtensor::{Error}; +use pallet_collective::{Event as CollectiveEvent}; + +pub fn new_test_ext() -> sp_io::TestExternalities { + sp_tracing::try_init_simple(); + + let mut ext: sp_io::TestExternalities = GenesisConfig { + senate_members: pallet_membership::GenesisConfig:: { + members: bounded_vec![1.into(), 2.into(), 3.into(), 4.into(), 5.into()], + phantom: Default::default() + }, + triumvirate: pallet_collective::GenesisConfig:: { + members: vec![1.into()], + phantom: Default::default(), + }, + ..Default::default() + } + .build_storage() + .unwrap() + .into(); + + ext.execute_with(|| System::set_block_number(1)); + ext +} + +fn make_proposal(value: u64) -> RuntimeCall { + RuntimeCall::System(frame_system::Call::remark_with_event { + remark: value.to_be_bytes().to_vec(), + }) +} + +fn record(event: RuntimeEvent) -> EventRecord { + EventRecord { phase: Phase::Initialization, event, topics: vec![] } +} + +#[test] +fn test_senate_join_works() { + new_test_ext().execute_with( || { + let netuid: u16 = 1; + let tempo: u16 = 13; + let hotkey_account_id = U256::from(6); + let burn_cost = 1000; + let coldkey_account_id = U256::from(667); // Neighbour of the beast, har har + + //add network + SubtensorModule::set_burn( netuid, burn_cost); + add_network(netuid, tempo, 0); + // Give it some $$$ in his coldkey balance + SubtensorModule::add_balance_to_coldkey_account( &coldkey_account_id, 10000 ); + + // Subscribe and check extrinsic output + assert_ok!(SubtensorModule::burned_register(<::RuntimeOrigin>::signed(coldkey_account_id), netuid, hotkey_account_id)); + // Check if balance has decreased to pay for the burn. + assert_eq!(SubtensorModule::get_coldkey_balance(&coldkey_account_id) as u64, 10000 - burn_cost); // funds drained on reg. + // Check if neuron has added to the specified network(netuid) + assert_eq!(SubtensorModule::get_subnetwork_n(netuid), 1); + // Check if hotkey is added to the Hotkeys + assert_eq!(SubtensorModule::get_owning_coldkey_for_hotkey(&hotkey_account_id), coldkey_account_id); + + // Lets make this new key a delegate with a 50% take. + assert_ok!(SubtensorModule::do_become_delegate(<::RuntimeOrigin>::signed(coldkey_account_id), hotkey_account_id, u16::MAX/2)); + + let staker_coldkey = U256::from(7); + SubtensorModule::add_balance_to_coldkey_account(&staker_coldkey, 100_000); + + assert_ok!(SubtensorModule::add_stake(<::RuntimeOrigin>::signed(staker_coldkey), hotkey_account_id, 100_000)); + assert_eq!(SubtensorModule::get_stake_for_coldkey_and_hotkey(&staker_coldkey, &hotkey_account_id), 100_000); + assert_eq!(SubtensorModule::get_total_stake_for_hotkey(&hotkey_account_id), 100_000); + + assert_ok!(SubtensorModule::do_join_senate(<::RuntimeOrigin>::signed(coldkey_account_id), &hotkey_account_id)); + assert_eq!(Senate::is_member(&hotkey_account_id), true); + }); +} + +#[test] +fn test_senate_join_fails_stake_req() { + new_test_ext().execute_with( || { + let netuid: u16 = 1; + let tempo: u16 = 13; + let hotkey_account_id = U256::from(6); + let burn_cost = 1000; + let coldkey_account_id = U256::from(667); // Neighbour of the beast, har har + + //add network + SubtensorModule::set_burn( netuid, burn_cost); + add_network(netuid, tempo, 0); + // Give it some $$$ in his coldkey balance + SubtensorModule::add_balance_to_coldkey_account( &coldkey_account_id, 10000 ); + + // Subscribe and check extrinsic output + assert_ok!(SubtensorModule::burned_register(<::RuntimeOrigin>::signed(coldkey_account_id), netuid, hotkey_account_id)); + // Check if balance has decreased to pay for the burn. + assert_eq!(SubtensorModule::get_coldkey_balance(&coldkey_account_id) as u64, 10000 - burn_cost); // funds drained on reg. + // Check if neuron has added to the specified network(netuid) + assert_eq!(SubtensorModule::get_subnetwork_n(netuid), 1); + // Check if hotkey is added to the Hotkeys + assert_eq!(SubtensorModule::get_owning_coldkey_for_hotkey(&hotkey_account_id), coldkey_account_id); + + // Lets make this new key a delegate with a 50% take. + assert_ok!(SubtensorModule::do_become_delegate(<::RuntimeOrigin>::signed(coldkey_account_id), hotkey_account_id, u16::MAX/2)); + + SubtensorModule::increase_total_stake(100_000); + assert_eq!(SubtensorModule::get_total_stake(), 100_000); + + assert_noop!(SubtensorModule::do_join_senate( + <::RuntimeOrigin>::signed(coldkey_account_id), + &hotkey_account_id + ), Error::::BelowStakeThreshold); + }); +} + +#[test] +fn test_senate_vote_works() { + new_test_ext().execute_with( || { + let netuid: u16 = 1; + let tempo: u16 = 13; + let senate_hotkey = U256::from(1); + let hotkey_account_id = U256::from(6); + let burn_cost = 1000; + let coldkey_account_id = U256::from(667); // Neighbour of the beast, har har + + //add network + SubtensorModule::set_burn( netuid, burn_cost); + add_network(netuid, tempo, 0); + // Give it some $$$ in his coldkey balance + SubtensorModule::add_balance_to_coldkey_account( &coldkey_account_id, 10000 ); + + // Subscribe and check extrinsic output + assert_ok!(SubtensorModule::burned_register(<::RuntimeOrigin>::signed(coldkey_account_id), netuid, hotkey_account_id)); + // Check if balance has decreased to pay for the burn. + assert_eq!(SubtensorModule::get_coldkey_balance(&coldkey_account_id) as u64, 10000 - burn_cost); // funds drained on reg. + // Check if neuron has added to the specified network(netuid) + assert_eq!(SubtensorModule::get_subnetwork_n(netuid), 1); + // Check if hotkey is added to the Hotkeys + assert_eq!(SubtensorModule::get_owning_coldkey_for_hotkey(&hotkey_account_id), coldkey_account_id); + + // Lets make this new key a delegate with a 50% take. + assert_ok!(SubtensorModule::do_become_delegate(<::RuntimeOrigin>::signed(coldkey_account_id), hotkey_account_id, u16::MAX/2)); + + let staker_coldkey = U256::from(7); + SubtensorModule::add_balance_to_coldkey_account(&staker_coldkey, 100_000); + + assert_ok!(SubtensorModule::add_stake(<::RuntimeOrigin>::signed(staker_coldkey), hotkey_account_id, 100_000)); + assert_eq!(SubtensorModule::get_stake_for_coldkey_and_hotkey(&staker_coldkey, &hotkey_account_id), 100_000); + assert_eq!(SubtensorModule::get_total_stake_for_hotkey(&hotkey_account_id), 100_000); + + assert_ok!(SubtensorModule::do_join_senate(<::RuntimeOrigin>::signed(coldkey_account_id), &hotkey_account_id)); + assert_eq!(Senate::is_member(&hotkey_account_id), true); + + System::reset_events(); + + let proposal = make_proposal(42); + let proposal_len: u32 = proposal.using_encoded(|p| p.len() as u32); + let hash = BlakeTwo256::hash_of(&proposal); + assert_ok!(Triumvirate::propose( + RuntimeOrigin::signed(senate_hotkey), + Box::new(proposal.clone()), + proposal_len + )); + + assert_ok!(SubtensorModule::do_vote_senate(<::RuntimeOrigin>::signed(coldkey_account_id), &hotkey_account_id, hash, 0, true)); + assert_eq!( + System::events(), + vec![ + record(RuntimeEvent::Triumvirate(CollectiveEvent::Proposed { + account: senate_hotkey, + proposal_index: 0, + proposal_hash: hash, + threshold: 4 + })), + record(RuntimeEvent::Triumvirate(CollectiveEvent::Voted { + account: hotkey_account_id, + proposal_hash: hash, + voted: true, + yes: 1, + no: 0 + })) + ] + ); + }); +} + +#[test] +fn test_senate_vote_not_member() { + new_test_ext().execute_with( || { + let netuid: u16 = 1; + let tempo: u16 = 13; + let senate_hotkey = U256::from(1); + let hotkey_account_id = U256::from(6); + let burn_cost = 1000; + let coldkey_account_id = U256::from(667); // Neighbour of the beast, har har + + //add network + SubtensorModule::set_burn( netuid, burn_cost); + add_network(netuid, tempo, 0); + // Give it some $$$ in his coldkey balance + SubtensorModule::add_balance_to_coldkey_account( &coldkey_account_id, 10000 ); + + // Subscribe and check extrinsic output + assert_ok!(SubtensorModule::burned_register(<::RuntimeOrigin>::signed(coldkey_account_id), netuid, hotkey_account_id)); + // Check if balance has decreased to pay for the burn. + assert_eq!(SubtensorModule::get_coldkey_balance(&coldkey_account_id) as u64, 10000 - burn_cost); // funds drained on reg. + // Check if neuron has added to the specified network(netuid) + assert_eq!(SubtensorModule::get_subnetwork_n(netuid), 1); + // Check if hotkey is added to the Hotkeys + assert_eq!(SubtensorModule::get_owning_coldkey_for_hotkey(&hotkey_account_id), coldkey_account_id); + + let proposal = make_proposal(42); + let proposal_len: u32 = proposal.using_encoded(|p| p.len() as u32); + let hash = BlakeTwo256::hash_of(&proposal); + assert_ok!(Triumvirate::propose( + RuntimeOrigin::signed(senate_hotkey), + Box::new(proposal.clone()), + proposal_len + )); + + assert_noop!(SubtensorModule::do_vote_senate( + <::RuntimeOrigin>::signed(coldkey_account_id), + &hotkey_account_id, + hash, + 0, + true + ), Error::::NotSenateMember); + }); +} + +#[test] +fn test_senate_leave_works() { + new_test_ext().execute_with( || { + let netuid: u16 = 1; + let tempo: u16 = 13; + let hotkey_account_id = U256::from(6); + let burn_cost = 1000; + let coldkey_account_id = U256::from(667); // Neighbour of the beast, har har + + //add network + SubtensorModule::set_burn( netuid, burn_cost); + add_network(netuid, tempo, 0); + // Give it some $$$ in his coldkey balance + SubtensorModule::add_balance_to_coldkey_account( &coldkey_account_id, 10000 ); + + // Subscribe and check extrinsic output + assert_ok!(SubtensorModule::burned_register(<::RuntimeOrigin>::signed(coldkey_account_id), netuid, hotkey_account_id)); + // Check if balance has decreased to pay for the burn. + assert_eq!(SubtensorModule::get_coldkey_balance(&coldkey_account_id) as u64, 10000 - burn_cost); // funds drained on reg. + // Check if neuron has added to the specified network(netuid) + assert_eq!(SubtensorModule::get_subnetwork_n(netuid), 1); + // Check if hotkey is added to the Hotkeys + assert_eq!(SubtensorModule::get_owning_coldkey_for_hotkey(&hotkey_account_id), coldkey_account_id); + + // Lets make this new key a delegate with a 50% take. + assert_ok!(SubtensorModule::do_become_delegate(<::RuntimeOrigin>::signed(coldkey_account_id), hotkey_account_id, u16::MAX/2)); + + let staker_coldkey = U256::from(7); + SubtensorModule::add_balance_to_coldkey_account(&staker_coldkey, 100_000); + + assert_ok!(SubtensorModule::add_stake(<::RuntimeOrigin>::signed(staker_coldkey), hotkey_account_id, 100_000)); + assert_eq!(SubtensorModule::get_stake_for_coldkey_and_hotkey(&staker_coldkey, &hotkey_account_id), 100_000); + assert_eq!(SubtensorModule::get_total_stake_for_hotkey(&hotkey_account_id), 100_000); + + assert_ok!(SubtensorModule::do_join_senate(<::RuntimeOrigin>::signed(coldkey_account_id), &hotkey_account_id)); + assert_eq!(Senate::is_member(&hotkey_account_id), true); + + assert_ok!(SubtensorModule::do_leave_senate(<::RuntimeOrigin>::signed(coldkey_account_id), &hotkey_account_id)); + assert_eq!(Senate::is_member(&hotkey_account_id), false); + }); +} + +#[test] +fn test_senate_leave_not_member() { + new_test_ext().execute_with( || { + let netuid: u16 = 1; + let tempo: u16 = 13; + let hotkey_account_id = U256::from(6); + let burn_cost = 1000; + let coldkey_account_id = U256::from(667); // Neighbour of the beast, har har + + //add network + SubtensorModule::set_burn( netuid, burn_cost); + add_network(netuid, tempo, 0); + // Give it some $$$ in his coldkey balance + SubtensorModule::add_balance_to_coldkey_account( &coldkey_account_id, 10000 ); + + // Subscribe and check extrinsic output + assert_ok!(SubtensorModule::burned_register(<::RuntimeOrigin>::signed(coldkey_account_id), netuid, hotkey_account_id)); + // Check if balance has decreased to pay for the burn. + assert_eq!(SubtensorModule::get_coldkey_balance(&coldkey_account_id) as u64, 10000 - burn_cost); // funds drained on reg. + // Check if neuron has added to the specified network(netuid) + assert_eq!(SubtensorModule::get_subnetwork_n(netuid), 1); + // Check if hotkey is added to the Hotkeys + assert_eq!(SubtensorModule::get_owning_coldkey_for_hotkey(&hotkey_account_id), coldkey_account_id); + + assert_noop!(SubtensorModule::do_leave_senate( + <::RuntimeOrigin>::signed(coldkey_account_id), + &hotkey_account_id + ), Error::::NotSenateMember); + }); +} + +#[test] +fn test_senate_leave_vote_removal() { + new_test_ext().execute_with( || { + let netuid: u16 = 1; + let tempo: u16 = 13; + let senate_hotkey = U256::from(1); + let hotkey_account_id = U256::from(6); + let burn_cost = 1000; + let coldkey_account_id = U256::from(667); // Neighbour of the beast, har har + let coldkey_origin = <::RuntimeOrigin>::signed(coldkey_account_id); + + //add network + SubtensorModule::set_burn( netuid, burn_cost); + add_network(netuid, tempo, 0); + // Give it some $$$ in his coldkey balance + SubtensorModule::add_balance_to_coldkey_account( &coldkey_account_id, 10000 ); + + // Subscribe and check extrinsic output + assert_ok!(SubtensorModule::burned_register(coldkey_origin.clone(), netuid, hotkey_account_id)); + // Check if balance has decreased to pay for the burn. + assert_eq!(SubtensorModule::get_coldkey_balance(&coldkey_account_id) as u64, 10000 - burn_cost); // funds drained on reg. + // Check if neuron has added to the specified network(netuid) + assert_eq!(SubtensorModule::get_subnetwork_n(netuid), 1); + // Check if hotkey is added to the Hotkeys + assert_eq!(SubtensorModule::get_owning_coldkey_for_hotkey(&hotkey_account_id), coldkey_account_id); + + // Lets make this new key a delegate with a 50% take. + assert_ok!(SubtensorModule::do_become_delegate(coldkey_origin.clone(), hotkey_account_id, u16::MAX/2)); + + let staker_coldkey = U256::from(7); + SubtensorModule::add_balance_to_coldkey_account(&staker_coldkey, 100_000); + + assert_ok!(SubtensorModule::add_stake(<::RuntimeOrigin>::signed(staker_coldkey), hotkey_account_id, 100_000)); + assert_eq!(SubtensorModule::get_stake_for_coldkey_and_hotkey(&staker_coldkey, &hotkey_account_id), 100_000); + assert_eq!(SubtensorModule::get_total_stake_for_hotkey(&hotkey_account_id), 100_000); + + assert_ok!(SubtensorModule::do_join_senate(coldkey_origin.clone(), &hotkey_account_id)); + assert_eq!(Senate::is_member(&hotkey_account_id), true); + + let proposal = make_proposal(42); + let proposal_len: u32 = proposal.using_encoded(|p| p.len() as u32); + let hash = BlakeTwo256::hash_of(&proposal); + assert_ok!(Triumvirate::propose( + RuntimeOrigin::signed(senate_hotkey), + Box::new(proposal.clone()), + proposal_len + )); + + assert_ok!(SubtensorModule::do_vote_senate(coldkey_origin.clone(), &hotkey_account_id, hash, 0, true)); + assert_ok!(SubtensorModule::do_leave_senate(coldkey_origin, &hotkey_account_id)); + + assert_eq!(Triumvirate::has_voted(hash, 0, &hotkey_account_id), Ok(false)); + }); +} + +#[test] +fn test_senate_leave_when_stake_removed() { + new_test_ext().execute_with( || { + let netuid: u16 = 1; + let tempo: u16 = 13; + let hotkey_account_id = U256::from(6); + let burn_cost = 1000; + let coldkey_account_id = U256::from(667); // Neighbour of the beast, har har + + //add network + SubtensorModule::set_burn( netuid, burn_cost); + add_network(netuid, tempo, 0); + // Give it some $$$ in his coldkey balance + SubtensorModule::add_balance_to_coldkey_account( &coldkey_account_id, 10000 ); + + // Subscribe and check extrinsic output + assert_ok!(SubtensorModule::burned_register(<::RuntimeOrigin>::signed(coldkey_account_id), netuid, hotkey_account_id)); + // Check if balance has decreased to pay for the burn. + assert_eq!(SubtensorModule::get_coldkey_balance(&coldkey_account_id) as u64, 10000 - burn_cost); // funds drained on reg. + // Check if neuron has added to the specified network(netuid) + assert_eq!(SubtensorModule::get_subnetwork_n(netuid), 1); + // Check if hotkey is added to the Hotkeys + assert_eq!(SubtensorModule::get_owning_coldkey_for_hotkey(&hotkey_account_id), coldkey_account_id); + + // Lets make this new key a delegate with a 50% take. + assert_ok!(SubtensorModule::do_become_delegate(<::RuntimeOrigin>::signed(coldkey_account_id), hotkey_account_id, u16::MAX/2)); + + let staker_coldkey = U256::from(7); + let stake_amount: u64 = 100_000; + SubtensorModule::add_balance_to_coldkey_account(&staker_coldkey, stake_amount); + + assert_ok!(SubtensorModule::add_stake(<::RuntimeOrigin>::signed(staker_coldkey), hotkey_account_id, stake_amount)); + assert_eq!(SubtensorModule::get_stake_for_coldkey_and_hotkey(&staker_coldkey, &hotkey_account_id), stake_amount); + assert_eq!(SubtensorModule::get_total_stake_for_hotkey(&hotkey_account_id), stake_amount); + + assert_ok!(SubtensorModule::do_join_senate(<::RuntimeOrigin>::signed(coldkey_account_id), &hotkey_account_id)); + assert_eq!(Senate::is_member(&hotkey_account_id), true); + + step_block(100); + + assert_ok!(SubtensorModule::remove_stake(<::RuntimeOrigin>::signed(staker_coldkey), hotkey_account_id, stake_amount)); + assert_eq!(Senate::is_member(&hotkey_account_id), false); + }); +} + +#[test] +fn test_senate_replace_lowest_member() { + new_test_ext().execute_with( || { + let netuid: u16 = 1; + let tempo: u16 = 13; + let burn_cost = 1000; + + //add network + add_network(netuid, tempo, 0); + SubtensorModule::set_max_allowed_uids(netuid, 4096); + SubtensorModule::set_burn( netuid, burn_cost ); + SubtensorModule::set_max_registrations_per_block(netuid, 100); + + for i in 1..11u64 { + let coldkey_account_id = U256::from(100 + i); + let coldkey_origin = <::RuntimeOrigin>::signed(coldkey_account_id); + + let hotkey_account_id = U256::from(5 + i); + + // Give it some $$$ in his coldkey balance + SubtensorModule::add_balance_to_coldkey_account( &coldkey_account_id, 10000 ); + + // Subscribe and check extrinsic output + assert_ok!(SubtensorModule::burned_register(coldkey_origin.clone(), netuid, hotkey_account_id)); + // Check if balance has decreased to pay for the burn. + assert_eq!(SubtensorModule::get_coldkey_balance(&coldkey_account_id) as u64, 10000 - burn_cost); // funds drained on reg. + // Check if neuron has added to the specified network(netuid) + assert_eq!(u64::from(SubtensorModule::get_subnetwork_n(netuid)), i); + // Check if hotkey is added to the Hotkeys + assert_eq!(SubtensorModule::get_owning_coldkey_for_hotkey(&hotkey_account_id), coldkey_account_id); + + // Lets make this new key a delegate with a 50% take. + assert_ok!(SubtensorModule::do_become_delegate(coldkey_origin.clone(), hotkey_account_id, u16::MAX/2)); + + let staker_coldkey = U256::from(200 + i); + let staked_amount = 100_000 * i; + SubtensorModule::add_balance_to_coldkey_account(&staker_coldkey, staked_amount); + + assert_ok!(SubtensorModule::add_stake(<::RuntimeOrigin>::signed(staker_coldkey), hotkey_account_id, staked_amount)); + assert_eq!(SubtensorModule::get_stake_for_coldkey_and_hotkey(&staker_coldkey, &hotkey_account_id), staked_amount); + assert_eq!(SubtensorModule::get_total_stake_for_hotkey(&hotkey_account_id), staked_amount); + + assert_ok!(SubtensorModule::do_join_senate(coldkey_origin, &hotkey_account_id)); + assert_eq!(Senate::is_member(&hotkey_account_id), true); + + SubtensorModule::set_burn( netuid, burn_cost ); + step_block(100); + } + + let hotkey_account_id = U256::from(16); + let coldkey_account_id = U256::from(667); // Neighbour of the beast, har har + let coldkey_origin = <::RuntimeOrigin>::signed(coldkey_account_id); + + // Give it some $$$ in his coldkey balance + SubtensorModule::add_balance_to_coldkey_account( &coldkey_account_id, 10000 ); + + // Subscribe and check extrinsic output + assert_ok!(SubtensorModule::burned_register(coldkey_origin.clone(), netuid, hotkey_account_id)); + // Check if balance has decreased to pay for the burn. + assert_eq!(SubtensorModule::get_coldkey_balance(&coldkey_account_id) as u64, 10000 - burn_cost); // funds drained on reg. + // Check if hotkey is added to the Hotkeys + assert_eq!(SubtensorModule::get_owning_coldkey_for_hotkey(&hotkey_account_id), coldkey_account_id); + + // Lets make this new key a delegate with a 50% take. + assert_ok!(SubtensorModule::do_become_delegate(coldkey_origin.clone(), hotkey_account_id, u16::MAX/2)); + + let staker_coldkey = U256::from(1000); + let stake_amount = 1_000_001; + SubtensorModule::add_balance_to_coldkey_account(&staker_coldkey, stake_amount); + + assert_ok!(SubtensorModule::add_stake(<::RuntimeOrigin>::signed(staker_coldkey), hotkey_account_id, stake_amount)); + assert_eq!(SubtensorModule::get_stake_for_coldkey_and_hotkey(&staker_coldkey, &hotkey_account_id), stake_amount); + assert_eq!(SubtensorModule::get_total_stake_for_hotkey(&hotkey_account_id), stake_amount); + + assert_ok!(SubtensorModule::do_join_senate(coldkey_origin.clone(), &hotkey_account_id)); + assert_eq!(Senate::is_member(&hotkey_account_id), true); + + // Lowest stake amount should get kicked out + assert_eq!(Senate::is_member(&U256::from(6)), false); + }); +} diff --git a/pallets/subtensor/tests/staking.rs b/pallets/subtensor/tests/staking.rs index 899e18ee7..fc0a3824e 100644 --- a/pallets/subtensor/tests/staking.rs +++ b/pallets/subtensor/tests/staking.rs @@ -294,7 +294,7 @@ fn test_remove_stake_dispatch_info_ok() { let amount_unstaked = 5000; let call = RuntimeCall::SubtensorModule(SubtensorCall::remove_stake{hotkey, amount_unstaked}); assert_eq!(call.get_dispatch_info(), DispatchInfo { - weight: frame_support::weights::Weight::from_ref_time(66000000), + weight: frame_support::weights::Weight::from_ref_time(63000000).add_proof_size(43991), class: DispatchClass::Normal, pays_fee: Pays::No }); diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 8111b8848..c10ed1fe4 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -27,7 +27,6 @@ pallet-balances = { version = "4.0.0-dev", default-features = false, git = "http frame-support = { version = "4.0.0-dev", default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.39" } pallet-grandpa = { version = "4.0.0-dev", default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.39" } pallet-insecure-randomness-collective-flip = { version = "4.0.0-dev", default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.39" } -pallet-sudo = { version = "4.0.0-dev", default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.39" } frame-system = { version = "4.0.0-dev", default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.39" } frame-try-runtime = { version = "0.10.0-dev", default-features = false, git = "https://github.com/paritytech/substrate.git", optional = true, branch = "polkadot-v0.9.39" } pallet-timestamp = { version = "4.0.0-dev", default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.39" } @@ -46,6 +45,13 @@ sp-std = { version = "5.0.0", default-features = false, git = "https://github.co sp-transaction-pool = { version = "4.0.0-dev", default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.39" } sp-version = { version = "5.0.0", default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.39" } +# Temporary sudo +pallet-sudo = { version = "4.0.0-dev", default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.39" } + +# Used for sudo decentralization +pallet-collective = { version = "4.0.0-dev", default-features = false, path = "../pallets/collective" } +pallet-membership = {version = "4.0.0-dev", default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.39" } + # Multisig pallet-multisig = { version = "4.0.0-dev", default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.39" } @@ -78,11 +84,11 @@ std = [ "pallet-balances/std", "pallet-grandpa/std", "pallet-insecure-randomness-collective-flip/std", - "pallet-sudo/std", "pallet-timestamp/std", "pallet-transaction-payment-rpc-runtime-api/std", "pallet-transaction-payment/std", "pallet-utility/std", + "pallet-sudo/std", "pallet-multisig/std", "sp-api/std", "sp-block-builder/std", @@ -96,6 +102,8 @@ std = [ "sp-transaction-pool/std", "sp-version/std", "substrate-wasm-builder", + "pallet-collective/std", + "pallet-membership/std", ] runtime-benchmarks = [ "frame-benchmarking/runtime-benchmarks", @@ -108,6 +116,8 @@ runtime-benchmarks = [ "pallet-utility/runtime-benchmarks", "sp-runtime/runtime-benchmarks", "pallet-subtensor/runtime-benchmarks", + "pallet-collective/runtime-benchmarks", + "pallet-membership/runtime-benchmarks", ] try-runtime = [ "frame-try-runtime/try-runtime", @@ -115,13 +125,15 @@ try-runtime = [ "frame-system/try-runtime", "frame-support/try-runtime", "pallet-aura/try-runtime", + "pallet-sudo/try-runtime", "pallet-balances/try-runtime", "pallet-grandpa/try-runtime", "pallet-insecure-randomness-collective-flip/try-runtime", - "pallet-sudo/try-runtime", "pallet-timestamp/try-runtime", "pallet-transaction-payment/try-runtime", "pallet-utility/try-runtime", "pallet-subtensor/try-runtime", + "pallet-collective/try-runtime", + "pallet-membership/try-runtime", "pallet-multisig/try-runtime", ] diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index bd0f1fcfa..335f4f610 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -6,12 +6,14 @@ #[cfg(feature = "std")] include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs")); -use codec::Encode; +use codec::{Encode, Decode}; +use pallet_collective::EnsureMember; use pallet_grandpa::{ fg_primitives, AuthorityId as GrandpaId, AuthorityList as GrandpaAuthorityList, }; -use frame_support::pallet_prelude::Get; +use frame_support::{pallet_prelude::{Get, TypeInfo, MaxEncodedLen, PhantomData, EnsureOrigin, DispatchResult}, traits::{EitherOfDiverse}, RuntimeDebug}; +use frame_system::{EnsureRoot, Config, EnsureNever, RawOrigin}; use smallvec::smallvec; use sp_api::impl_runtime_apis; @@ -75,6 +77,9 @@ pub type Index = u32; // A hash of some data used by the chain. pub type Hash = sp_core::H256; +// Member type for membership +type MemberCount = u32; + // Opaque types. These are used by the CLI to instantiate machinery that don't need to know // the specifics of the runtime. They can then be made to be agnostic over specific formats // of data like extrinsics, allowing for them to continue syncing the network through upgrades @@ -111,7 +116,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { // `spec_version`, and `authoring_version` are the same between Wasm and native. // This value is set to 100 to notify Polkadot-JS App (https://polkadot.js.org/apps) to use // the compatible custom types. - spec_version: 121, + spec_version: 122, impl_version: 1, apis: RUNTIME_API_VERSIONS, transaction_version: 1, @@ -315,6 +320,153 @@ impl pallet_transaction_payment::Config for Runtime { //type FeeMultiplierUpdate = ConstFeeMultiplier; } +// Configure collective pallet for council +parameter_types! { + pub const CouncilMotionDuration: BlockNumber = 100; + pub const CouncilMaxProposals: u32 = 10; + pub const CouncilMaxMembers: u32 = 3; +} + +// Configure collective pallet for Senate +parameter_types! { + pub const SenateMaxMembers: u32 = 10; +} + +use pallet_collective::{CanPropose, CanVote, GetVotingMembers}; +pub struct CanProposeToTriumvirate; +impl CanPropose for CanProposeToTriumvirate { + fn can_propose(account: &AccountId) -> bool { + Triumvirate::is_member(account) + } +} + +pub struct CanVoteToTriumvirate; +impl CanVote for CanVoteToTriumvirate { + fn can_vote(account: &AccountId) -> bool { + //Senate::is_member(account) + false // Disable voting from pallet_collective::vote + } +} + +use pallet_subtensor::{MemberManagement, CollectiveInterface}; +pub struct ManageSenateMembers; +impl MemberManagement for ManageSenateMembers { + fn add_member(account: &AccountId) -> DispatchResult { + let who = Address::Id( account.clone() ); + SenateMembers::add_member(RawOrigin::Root.into(), who) + } + + fn remove_member(account: &AccountId) -> DispatchResult { + let who = Address::Id( account.clone() ); + SenateMembers::remove_member(RawOrigin::Root.into(), who) + } + + fn swap_member(remove: &AccountId, add: &AccountId) -> DispatchResult { + let remove = Address::Id( remove.clone() ); + let add = Address::Id( add.clone() ); + + SenateMembers::swap_member(RawOrigin::Root.into(), remove, add) + } + + fn is_member(account: &AccountId) -> bool { + Senate::is_member(account) + } + + fn members() -> Vec { + Senate::members() + } + + fn max_members() -> u32 { + SenateMaxMembers::get() + } +} + +pub struct GetSenateMemberCount; +impl GetVotingMembers for GetSenateMemberCount { + fn get_count() -> MemberCount {Senate::members().len() as u32} +} +impl Get for GetSenateMemberCount { + fn get() -> MemberCount {SenateMaxMembers::get()} +} + +pub struct TriumvirateVotes; +impl CollectiveInterface for TriumvirateVotes { + fn remove_votes(hotkey: &AccountId) -> Result { + Triumvirate::remove_votes(hotkey) + } + + fn add_vote(hotkey: &AccountId, proposal: Hash, index: u32, approve: bool) -> Result { + Triumvirate::do_vote(hotkey.clone(), proposal, index, approve) + } +} + +type EnsureMajoritySenate = pallet_collective::EnsureProportionMoreThan; + +// We call pallet_collective TriumvirateCollective +type TriumvirateCollective = pallet_collective::Instance1; +impl pallet_collective::Config for Runtime { + type RuntimeOrigin = RuntimeOrigin; + type Proposal = RuntimeCall; + type RuntimeEvent = RuntimeEvent; + type MotionDuration = CouncilMotionDuration; + type MaxProposals = CouncilMaxProposals; + type MaxMembers = GetSenateMemberCount; + type DefaultVote = pallet_collective::PrimeDefaultVote; + type WeightInfo = pallet_collective::weights::SubstrateWeight; + type SetMembersOrigin = EnsureNever; + type CanPropose = CanProposeToTriumvirate; + type CanVote = CanVoteToTriumvirate; + type GetVotingMembers = GetSenateMemberCount; +} + +// We call council members Triumvirate +type TriumvirateMembership = pallet_membership::Instance1; +impl pallet_membership::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type AddOrigin = EnsureRoot; + type RemoveOrigin = EnsureRoot; + type SwapOrigin = EnsureRoot; + type ResetOrigin = EnsureRoot; + type PrimeOrigin = EnsureRoot; + type MembershipInitialized = Triumvirate; + type MembershipChanged = Triumvirate; + type MaxMembers = CouncilMaxMembers; + type WeightInfo = pallet_membership::weights::SubstrateWeight; +} + +// This is a dummy collective instance for managing senate members +// Probably not the best solution, but fastest implementation +type SenateCollective = pallet_collective::Instance2; +impl pallet_collective::Config for Runtime { + type RuntimeOrigin = RuntimeOrigin; + type Proposal = RuntimeCall; + type RuntimeEvent = RuntimeEvent; + type MotionDuration = CouncilMotionDuration; + type MaxProposals = CouncilMaxProposals; + type MaxMembers = SenateMaxMembers; + type DefaultVote = pallet_collective::PrimeDefaultVote; + type WeightInfo = pallet_collective::weights::SubstrateWeight; + type SetMembersOrigin = EnsureNever; + type CanPropose = (); + type CanVote = (); + type GetVotingMembers = (); +} + +// We call our top K delegates membership Senate +type SenateMembership = pallet_membership::Instance2; +impl pallet_membership::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type AddOrigin = EnsureRoot; + type RemoveOrigin = EnsureRoot; + type SwapOrigin = EnsureRoot; + type ResetOrigin = EnsureRoot; + type PrimeOrigin = EnsureRoot; + type MembershipInitialized = Senate; + type MembershipChanged = Senate; + type MaxMembers = SenateMaxMembers; + type WeightInfo = pallet_membership::weights::SubstrateWeight; +} + impl pallet_sudo::Config for Runtime { type RuntimeEvent = RuntimeEvent; type RuntimeCall = RuntimeCall; @@ -376,11 +528,17 @@ parameter_types! { pub const SubtensorInitialMaxBurn: u64 = 100_000_000_000; // 100 tao pub const SubtensorInitialTxRateLimit: u64 = 1000; pub const SubtensorInitialRAORecycledForRegistration: u64 = 0; // 0 rao + pub const SubtensorInitialSenateRequiredStakePercentage: u64 = 2; // 2 percent of total stake } impl pallet_subtensor::Config for Runtime { type RuntimeEvent = RuntimeEvent; + type SudoRuntimeCall = RuntimeCall; type Currency = Balances; + type CouncilOrigin = EnsureMajoritySenate; + type SenateMembers = ManageSenateMembers; + type TriumvirateInterface = TriumvirateVotes; + type InitialRho = SubtensorInitialRho; type InitialKappa = SubtensorInitialKappa; type InitialMaxAllowedUids = SubtensorInitialMaxAllowedUids; @@ -417,6 +575,7 @@ impl pallet_subtensor::Config for Runtime { type InitialMinBurn = SubtensorInitialMinBurn; type InitialTxRateLimit = SubtensorInitialTxRateLimit; type InitialRAORecycledForRegistration = SubtensorInitialRAORecycledForRegistration; + type InitialSenateRequiredStakePercentage = SubtensorInitialSenateRequiredStakePercentage; } // Create the runtime by composing the FRAME pallets that were previously configured. @@ -434,9 +593,13 @@ construct_runtime!( Grandpa: pallet_grandpa, Balances: pallet_balances, TransactionPayment: pallet_transaction_payment, - Sudo: pallet_sudo, SubtensorModule: pallet_subtensor, + Triumvirate: pallet_collective::::{Pallet, Call, Storage, Origin, Event, Config}, + TriumvirateMembers: pallet_membership::::{Pallet, Call, Storage, Event, Config}, + Senate: pallet_collective::::{Pallet, Call, Storage, Origin, Event, Config}, + SenateMembers: pallet_membership::::{Pallet, Call, Storage, Event, Config}, Utility: pallet_utility, + Sudo: pallet_sudo, Multisig: pallet_multisig } );