diff --git a/substrate/bin/node/runtime/src/lib.rs b/substrate/bin/node/runtime/src/lib.rs index 1cc0a2b725..ae0de952e4 100644 --- a/substrate/bin/node/runtime/src/lib.rs +++ b/substrate/bin/node/runtime/src/lib.rs @@ -68,6 +68,7 @@ use impls::{CurrencyToVoteHandler, Author, LinearWeightToFee, TargetedFeeAdjustm /// Constant values used within the runtime. pub mod constants; use constants::{time::*, currency::*}; +use frame_system::Trait; // Make the WASM binary available. #[cfg(feature = "std")] @@ -330,11 +331,16 @@ impl pallet_democracy::Trait for Runtime { type Slash = Treasury; } +parameter_types! { + pub const CouncilMotionDuration: BlockNumber = 5 * DAYS; +} + type CouncilCollective = pallet_collective::Instance1; impl pallet_collective::Trait for Runtime { type Origin = Origin; type Proposal = Call; type Event = Event; + type MotionDuration = CouncilMotionDuration; } parameter_types! { @@ -360,11 +366,16 @@ impl pallet_elections_phragmen::Trait for Runtime { type TermDuration = TermDuration; } +parameter_types! { + pub const TechnicalMotionDuration: BlockNumber = 5 * DAYS; +} + type TechnicalCollective = pallet_collective::Instance2; impl pallet_collective::Trait for Runtime { type Origin = Origin; type Proposal = Call; type Event = Event; + type MotionDuration = TechnicalMotionDuration; } impl pallet_membership::Trait for Runtime { @@ -373,6 +384,7 @@ impl pallet_membership::Trait for Runtime { type RemoveOrigin = pallet_collective::EnsureProportionMoreThan<_1, _2, AccountId, CouncilCollective>; type SwapOrigin = pallet_collective::EnsureProportionMoreThan<_1, _2, AccountId, CouncilCollective>; type ResetOrigin = pallet_collective::EnsureProportionMoreThan<_1, _2, AccountId, CouncilCollective>; + type PrimeOrigin = pallet_collective::EnsureProportionMoreThan<_1, _2, AccountId, CouncilCollective>; type MembershipInitialized = TechnicalCommittee; type MembershipChanged = TechnicalCommittee; } diff --git a/substrate/frame/collective/src/lib.rs b/substrate/frame/collective/src/lib.rs index 605cda3cb7..b5620e3406 100644 --- a/substrate/frame/collective/src/lib.rs +++ b/substrate/frame/collective/src/lib.rs @@ -18,7 +18,20 @@ //! 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` +//! function `set_members`, or indirectly, through implementing the `ChangeMembers`. +//! +//! A "prime" member may be set allowing their vote to act as the default vote in case of any +//! abstentions after the voting period. +//! +//! 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"] @@ -30,7 +43,7 @@ use sp_runtime::traits::{Hash, EnsureOrigin}; use frame_support::weights::SimpleDispatchInfo; use frame_support::{ dispatch::{Dispatchable, Parameter}, codec::{Encode, Decode}, - traits::{ChangeMembers, InitializeMembers}, decl_module, decl_event, + traits::{Get, ChangeMembers, InitializeMembers}, decl_module, decl_event, decl_storage, decl_error, ensure, }; use frame_system::{self as system, ensure_signed, ensure_root}; @@ -53,6 +66,9 @@ pub trait Trait: frame_system::Trait { /// The outer event type. type Event: From> + Into<::Event>; + + /// The time-out for council motions. + type MotionDuration: Get; } /// Origin for the collective module. @@ -71,7 +87,7 @@ pub type Origin = RawOrigin<::Ac #[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug)] /// Info for keeping track of a motion being voted on. -pub struct Votes { +pub struct Votes { /// The proposal's unique index. index: ProposalIndex, /// The number of approval votes that are needed to pass the motion. @@ -80,6 +96,8 @@ pub struct Votes { ayes: Vec, /// The current set of voters that rejected it. nays: Vec, + /// The hard end time of this vote. + end: BlockNumber, } decl_storage! { @@ -91,11 +109,14 @@ decl_storage! { map hasher(blake2_256) T::Hash => Option<>::Proposal>; /// Votes on a given proposal, if it is ongoing. pub Voting get(fn voting): - map hasher(blake2_256) T::Hash => Option>; + map hasher(blake2_256) T::Hash => Option>; /// Proposals so far. pub ProposalCount get(fn proposal_count): u32; /// The current members of the collective. This is stored sorted (just by value). pub Members get(fn members): Vec; + /// The member who provides the default vote for any other members that do not vote before + /// the timeout. If None, then no member has that privilege. + pub Prime get(fn prime): Option; } add_extra_genesis { config(phantom): sp_std::marker::PhantomData; @@ -123,6 +144,8 @@ decl_event! { Executed(Hash, bool), /// A single member did some action; `bool` is true if returned without error. MemberExecuted(Hash, bool), + /// A proposal was closed after its duration was up. + Closed(Hash, MemberCount, MemberCount), } } @@ -140,6 +163,8 @@ decl_error! { DuplicateVote, /// Members are already initialized! AlreadyInitialized, + /// The close call is made too early, before the end of the voting. + TooEarly, } } @@ -152,19 +177,21 @@ decl_module! { fn deposit_event() = default; - /// Set the collective's membership manually to `new_members`. Be nice to the chain and - /// provide it pre-sorted. + /// 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. /// /// Requires root origin. #[weight = SimpleDispatchInfo::FixedOperational(100_000)] - fn set_members(origin, new_members: Vec) { + fn set_members(origin, new_members: Vec, prime: Option) { ensure_root(origin)?; let mut new_members = new_members; new_members.sort(); - >::mutate(|m| { - >::set_members_sorted(&new_members[..], m); - *m = new_members; - }); + let old = Members::::get(); + >::set_members_sorted(&new_members[..], &old); + Prime::::set(prime); } /// Dispatch a proposal from a member using the `Member` origin. @@ -202,7 +229,8 @@ decl_module! { >::mutate(|i| *i += 1); >::mutate(|proposals| proposals.push(proposal_hash)); >::insert(proposal_hash, *proposal); - let votes = Votes { index, threshold, ayes: vec![who.clone()], nays: vec![] }; + let end = system::Module::::block_number() + T::MotionDuration::get(); + let votes = Votes { index, threshold, ayes: vec![who.clone()], nays: vec![], end }; >::insert(proposal_hash, votes); Self::deposit_event(RawEvent::Proposed(who, index, proposal_hash, threshold)); @@ -249,32 +277,55 @@ decl_module! { Self::deposit_event(RawEvent::Voted(who, proposal, approve, yes_votes, no_votes)); let seats = Self::members().len() as MemberCount; + let approved = yes_votes >= voting.threshold; let disapproved = seats.saturating_sub(no_votes) < voting.threshold; if approved || disapproved { - if approved { - Self::deposit_event(RawEvent::Approved(proposal)); - - // execute motion, assuming it exists. - if let Some(p) = >::take(&proposal) { - let origin = RawOrigin::Members(voting.threshold, seats).into(); - let ok = p.dispatch(origin).is_ok(); - Self::deposit_event(RawEvent::Executed(proposal, ok)); - } - } else { - // disapproved - >::remove(&proposal); - Self::deposit_event(RawEvent::Disapproved(proposal)); - } - - // remove vote - >::remove(&proposal); - >::mutate(|proposals| proposals.retain(|h| h != &proposal)); + Self::finalize_proposal(approved, seats, voting, proposal); } else { - // update voting - >::insert(&proposal, voting); + Voting::::insert(&proposal, voting); } } + + /// May be called by any signed account after the voting duration has ended in order to + /// finish voting and close the proposal. + /// + /// Abstentions are counted as rejections unless there is a prime member set and the prime + /// member cast an approval. + /// + /// - the weight of `proposal` preimage. + /// - up to three events deposited. + /// - one read, two removals, one mutation. (plus three static reads.) + /// - computation and i/o `O(P + L + M)` where: + /// - `M` is number of members, + /// - `P` is number of active proposals, + /// - `L` is the encoded length of `proposal` preimage. + #[weight = SimpleDispatchInfo::FixedOperational(200_000)] + fn close(origin, proposal: T::Hash, #[compact] index: ProposalIndex) { + let _ = ensure_signed(origin)?; + + let voting = Self::voting(&proposal).ok_or(Error::::ProposalMissing)?; + ensure!(voting.index == index, Error::::WrongIndex); + ensure!(system::Module::::block_number() >= voting.end, Error::::TooEarly); + + // default to true only if there's a prime and they voted in favour. + let default = Self::prime().map_or( + false, + |who| voting.ayes.iter().any(|a| a == &who), + ); + + let mut no_votes = voting.nays.len() as MemberCount; + let mut yes_votes = voting.ayes.len() as MemberCount; + let seats = Self::members().len() as MemberCount; + let abstentions = seats - (yes_votes + no_votes); + match default { + true => yes_votes += abstentions, + false => no_votes += abstentions, + } + + Self::deposit_event(RawEvent::Closed(proposal, yes_votes, no_votes)); + Self::finalize_proposal(yes_votes >= voting.threshold, seats, voting, proposal); + } } } @@ -282,10 +333,54 @@ impl, I: Instance> Module { pub fn is_member(who: &T::AccountId) -> bool { Self::members().contains(who) } + + /// 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 finalize_proposal( + approved: bool, + seats: MemberCount, + voting: Votes, + proposal: T::Hash, + ) { + if approved { + Self::deposit_event(RawEvent::Approved(proposal)); + + // execute motion, assuming it exists. + if let Some(p) = ProposalOf::::take(&proposal) { + let origin = RawOrigin::Members(voting.threshold, seats).into(); + let ok = p.dispatch(origin).is_ok(); + Self::deposit_event(RawEvent::Executed(proposal, ok)); + } + } else { + // disapproved + ProposalOf::::remove(&proposal); + Self::deposit_event(RawEvent::Disapproved(proposal)); + } + + // remove vote + Voting::::remove(&proposal); + Proposals::::mutate(|proposals| proposals.retain(|h| h != &proposal)); + } } impl, I: Instance> ChangeMembers for Module { - fn change_members_sorted(_incoming: &[T::AccountId], outgoing: &[T::AccountId], new: &[T::AccountId]) { + fn change_members_sorted( + _incoming: &[T::AccountId], + outgoing: &[T::AccountId], + new: &[T::AccountId], + ) { // remove accounts from all current voting in motions. let mut outgoing = outgoing.to_vec(); outgoing.sort_unstable(); @@ -302,7 +397,12 @@ impl, I: Instance> ChangeMembers for Module { } ); } - >::put(new); + Members::::put(new); + Prime::::kill(); + } + + fn set_prime(prime: Option) { + Prime::::set(prime); } } @@ -415,6 +515,7 @@ mod tests { pub const MaximumBlockWeight: Weight = 1024; pub const MaximumBlockLength: u32 = 2 * 1024; pub const AvailableBlockRatio: Perbill = Perbill::one(); + pub const MotionDuration: u64 = 3; } impl frame_system::Trait for Test { type Origin = Origin; @@ -441,11 +542,13 @@ mod tests { type Origin = Origin; type Proposal = Call; type Event = Event; + type MotionDuration = MotionDuration; } impl Trait for Test { type Origin = Origin; type Proposal = Call; type Event = Event; + type MotionDuration = MotionDuration; } pub type Block = sp_runtime::generic::Block; @@ -486,22 +589,101 @@ mod tests { Call::System(frame_system::Call::remark(value.encode())) } + #[test] + fn close_works() { + make_ext().execute_with(|| { + System::set_block_number(1); + let proposal = make_proposal(42); + let hash = BlakeTwo256::hash_of(&proposal); + + assert_ok!(Collective::propose(Origin::signed(1), 3, Box::new(proposal.clone()))); + assert_ok!(Collective::vote(Origin::signed(2), hash.clone(), 0, true)); + + System::set_block_number(3); + assert_noop!( + Collective::close(Origin::signed(4), hash.clone(), 0), + Error::::TooEarly + ); + + System::set_block_number(4); + assert_ok!(Collective::close(Origin::signed(4), hash.clone(), 0)); + + let record = |event| EventRecord { phase: Phase::Finalization, event, topics: vec![] }; + assert_eq!(System::events(), vec![ + record(Event::collective_Instance1(RawEvent::Proposed(1, 0, hash.clone(), 3))), + record(Event::collective_Instance1(RawEvent::Voted(2, hash.clone(), true, 2, 0))), + record(Event::collective_Instance1(RawEvent::Closed(hash.clone(), 2, 1))), + record(Event::collective_Instance1(RawEvent::Disapproved(hash.clone()))) + ]); + }); + } + + #[test] + fn close_with_prime_works() { + make_ext().execute_with(|| { + System::set_block_number(1); + let proposal = make_proposal(42); + let hash = BlakeTwo256::hash_of(&proposal); + assert_ok!(Collective::set_members(Origin::ROOT, vec![1, 2, 3], Some(3))); + + assert_ok!(Collective::propose(Origin::signed(1), 3, Box::new(proposal.clone()))); + assert_ok!(Collective::vote(Origin::signed(2), hash.clone(), 0, true)); + + System::set_block_number(4); + assert_ok!(Collective::close(Origin::signed(4), hash.clone(), 0)); + + let record = |event| EventRecord { phase: Phase::Finalization, event, topics: vec![] }; + assert_eq!(System::events(), vec![ + record(Event::collective_Instance1(RawEvent::Proposed(1, 0, hash.clone(), 3))), + record(Event::collective_Instance1(RawEvent::Voted(2, hash.clone(), true, 2, 0))), + record(Event::collective_Instance1(RawEvent::Closed(hash.clone(), 2, 1))), + record(Event::collective_Instance1(RawEvent::Disapproved(hash.clone()))) + ]); + }); + } + + #[test] + fn close_with_voting_prime_works() { + make_ext().execute_with(|| { + System::set_block_number(1); + let proposal = make_proposal(42); + let hash = BlakeTwo256::hash_of(&proposal); + assert_ok!(Collective::set_members(Origin::ROOT, vec![1, 2, 3], Some(1))); + + assert_ok!(Collective::propose(Origin::signed(1), 3, Box::new(proposal.clone()))); + assert_ok!(Collective::vote(Origin::signed(2), hash.clone(), 0, true)); + + System::set_block_number(4); + assert_ok!(Collective::close(Origin::signed(4), hash.clone(), 0)); + + let record = |event| EventRecord { phase: Phase::Finalization, event, topics: vec![] }; + assert_eq!(System::events(), vec![ + record(Event::collective_Instance1(RawEvent::Proposed(1, 0, hash.clone(), 3))), + record(Event::collective_Instance1(RawEvent::Voted(2, hash.clone(), true, 2, 0))), + record(Event::collective_Instance1(RawEvent::Closed(hash.clone(), 3, 0))), + record(Event::collective_Instance1(RawEvent::Approved(hash.clone()))), + record(Event::collective_Instance1(RawEvent::Executed(hash.clone(), false))) + ]); + }); + } + #[test] fn removal_of_old_voters_votes_works() { make_ext().execute_with(|| { System::set_block_number(1); let proposal = make_proposal(42); let hash = BlakeTwo256::hash_of(&proposal); + let end = 4; assert_ok!(Collective::propose(Origin::signed(1), 3, Box::new(proposal.clone()))); assert_ok!(Collective::vote(Origin::signed(2), hash.clone(), 0, true)); assert_eq!( Collective::voting(&hash), - Some(Votes { index: 0, threshold: 3, ayes: vec![1, 2], nays: vec![] }) + Some(Votes { index: 0, threshold: 3, 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: 3, ayes: vec![2], nays: vec![] }) + Some(Votes { index: 0, threshold: 3, ayes: vec![2], nays: vec![], end }) ); let proposal = make_proposal(69); @@ -510,12 +692,12 @@ mod tests { assert_ok!(Collective::vote(Origin::signed(3), hash.clone(), 1, false)); assert_eq!( Collective::voting(&hash), - Some(Votes { index: 1, threshold: 2, ayes: vec![2], nays: vec![3] }) + 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![] }) + Some(Votes { index: 1, threshold: 2, ayes: vec![2], nays: vec![], end }) ); }); } @@ -526,16 +708,17 @@ mod tests { System::set_block_number(1); let proposal = make_proposal(42); let hash = BlakeTwo256::hash_of(&proposal); + let end = 4; assert_ok!(Collective::propose(Origin::signed(1), 3, Box::new(proposal.clone()))); assert_ok!(Collective::vote(Origin::signed(2), hash.clone(), 0, true)); assert_eq!( Collective::voting(&hash), - Some(Votes { index: 0, threshold: 3, ayes: vec![1, 2], nays: vec![] }) + Some(Votes { index: 0, threshold: 3, ayes: vec![1, 2], nays: vec![], end }) ); - assert_ok!(Collective::set_members(Origin::ROOT, vec![2, 3, 4])); + assert_ok!(Collective::set_members(Origin::ROOT, vec![2, 3, 4], None)); assert_eq!( Collective::voting(&hash), - Some(Votes { index: 0, threshold: 3, ayes: vec![2], nays: vec![] }) + Some(Votes { index: 0, threshold: 3, ayes: vec![2], nays: vec![], end }) ); let proposal = make_proposal(69); @@ -544,12 +727,12 @@ mod tests { assert_ok!(Collective::vote(Origin::signed(3), hash.clone(), 1, false)); assert_eq!( Collective::voting(&hash), - Some(Votes { index: 1, threshold: 2, ayes: vec![2], nays: vec![3] }) + Some(Votes { index: 1, threshold: 2, ayes: vec![2], nays: vec![3], end }) ); - assert_ok!(Collective::set_members(Origin::ROOT, vec![2, 4])); + assert_ok!(Collective::set_members(Origin::ROOT, vec![2, 4], None)); assert_eq!( Collective::voting(&hash), - Some(Votes { index: 1, threshold: 2, ayes: vec![2], nays: vec![] }) + Some(Votes { index: 1, threshold: 2, ayes: vec![2], nays: vec![], end }) ); }); } @@ -560,12 +743,13 @@ mod tests { System::set_block_number(1); let proposal = make_proposal(42); let hash = proposal.blake2_256().into(); + let end = 4; assert_ok!(Collective::propose(Origin::signed(1), 3, Box::new(proposal.clone()))); assert_eq!(Collective::proposals(), vec![hash]); assert_eq!(Collective::proposal_of(&hash), Some(proposal)); assert_eq!( Collective::voting(&hash), - Some(Votes { index: 0, threshold: 3, ayes: vec![1], nays: vec![] }) + Some(Votes { index: 0, threshold: 3, ayes: vec![1], nays: vec![], end }) ); assert_eq!(System::events(), vec![ @@ -629,10 +813,11 @@ mod tests { System::set_block_number(1); let proposal = make_proposal(42); let hash: H256 = proposal.blake2_256().into(); + let end = 4; assert_ok!(Collective::propose(Origin::signed(1), 2, Box::new(proposal.clone()))); assert_eq!( Collective::voting(&hash), - Some(Votes { index: 0, threshold: 2, ayes: vec![1], nays: vec![] }) + Some(Votes { index: 0, threshold: 2, ayes: vec![1], nays: vec![], end }) ); assert_noop!( Collective::vote(Origin::signed(1), hash.clone(), 0, true), @@ -641,7 +826,7 @@ mod tests { assert_ok!(Collective::vote(Origin::signed(1), hash.clone(), 0, false)); assert_eq!( Collective::voting(&hash), - Some(Votes { index: 0, threshold: 2, ayes: vec![], nays: vec![1] }) + Some(Votes { index: 0, threshold: 2, ayes: vec![], nays: vec![1], end }) ); assert_noop!( Collective::vote(Origin::signed(1), hash.clone(), 0, false), diff --git a/substrate/frame/elections-phragmen/src/lib.rs b/substrate/frame/elections-phragmen/src/lib.rs index a9474ae844..55b7b3f128 100644 --- a/substrate/frame/elections-phragmen/src/lib.rs +++ b/substrate/frame/elections-phragmen/src/lib.rs @@ -690,6 +690,7 @@ impl Module { // split new set into winners and runners up. let split_point = desired_seats.min(new_set_with_stake.len()); let mut new_members = (&new_set_with_stake[..split_point]).to_vec(); + let most_popular = new_members.first().map(|x| x.0.clone()); // save the runners up as-is. They are sorted based on desirability. // sort and save the members. @@ -722,6 +723,7 @@ impl Module { &outgoing.clone(), &new_members_ids, ); + T::ChangeMembers::set_prime(most_popular); // outgoing candidates lose their bond. let mut to_burn_bond = outgoing.to_vec(); @@ -864,6 +866,7 @@ mod tests { thread_local! { pub static MEMBERS: RefCell> = RefCell::new(vec![]); + pub static PRIME: RefCell> = RefCell::new(None); } pub struct TestChangeMembers; @@ -898,6 +901,11 @@ mod tests { assert_eq!(old_plus_incoming, new_plus_outgoing); MEMBERS.with(|m| *m.borrow_mut() = new.to_vec()); + PRIME.with(|p| *p.borrow_mut() = None); + } + + fn set_prime(who: Option) { + PRIME.with(|p| *p.borrow_mut() = who); } } @@ -1250,6 +1258,30 @@ mod tests { }); } + #[test] + fn prime_works() { + ExtBuilder::default().build().execute_with(|| { + assert_ok!(Elections::submit_candidacy(Origin::signed(3))); + assert_ok!(Elections::submit_candidacy(Origin::signed(4))); + assert_ok!(Elections::submit_candidacy(Origin::signed(5))); + + assert_ok!(Elections::vote(Origin::signed(1), vec![4, 3], 10)); + assert_ok!(Elections::vote(Origin::signed(2), vec![4], 20)); + assert_ok!(Elections::vote(Origin::signed(3), vec![3], 30)); + assert_ok!(Elections::vote(Origin::signed(4), vec![4], 40)); + assert_ok!(Elections::vote(Origin::signed(5), vec![5], 50)); + + System::set_block_number(5); + assert_ok!(Elections::end_block(System::block_number())); + + assert_eq!(Elections::members_ids(), vec![4, 5]); + assert_eq!(Elections::candidates(), vec![]); + + assert_ok!(Elections::vote(Origin::signed(3), vec![4, 5], 10)); + assert_eq!(PRIME.with(|p| *p.borrow()), Some(4)); + }); + } + #[test] fn cannot_vote_for_more_than_candidates() { ExtBuilder::default().build().execute_with(|| { diff --git a/substrate/frame/membership/src/lib.rs b/substrate/frame/membership/src/lib.rs index c39055c1bc..129f3c4003 100644 --- a/substrate/frame/membership/src/lib.rs +++ b/substrate/frame/membership/src/lib.rs @@ -17,7 +17,7 @@ //! # Membership Module //! //! Allows control of membership of a set of `AccountId`s, useful for managing membership of of a -//! collective. +//! collective. A prime member may be set. // Ensure we're `no_std` when compiling for Wasm. #![cfg_attr(not(feature = "std"), no_std)] @@ -47,6 +47,9 @@ pub trait Trait: frame_system::Trait { /// Required origin for resetting membership. type ResetOrigin: EnsureOrigin; + /// Required origin for setting or resetting the prime member. + type PrimeOrigin: EnsureOrigin; + /// The receiver of the signal for when the membership has been initialized. This happens pre- /// genesis and will usually be the same as `MembershipChanged`. If you need to do something /// different on initialization, then you can change this accordingly. @@ -60,6 +63,9 @@ decl_storage! { trait Store for Module, I: Instance=DefaultInstance> as Membership { /// The current membership, stored as an ordered Vec. Members get(fn members): Vec; + + /// The current prime member, if one exists. + Prime get(fn prime): Option; } add_extra_genesis { config(members): Vec; @@ -144,6 +150,7 @@ decl_module! { >::put(&members); T::MembershipChanged::change_members_sorted(&[], &[who], &members[..]); + Self::rejig_prime(&members); Self::deposit_event(RawEvent::MemberRemoved); } @@ -151,6 +158,8 @@ decl_module! { /// Swap out one member `remove` for another `add`. /// /// May only be called from `SwapOrigin` or root. + /// + /// Prime membership is *not* passed from `remove` to `add`, if extant. #[weight = SimpleDispatchInfo::FixedNormal(50_000)] fn swap_member(origin, remove: T::AccountId, add: T::AccountId) { T::SwapOrigin::try_origin(origin) @@ -171,6 +180,7 @@ decl_module! { &[remove], &members[..], ); + Self::rejig_prime(&members); Self::deposit_event(RawEvent::MembersSwapped); } @@ -189,15 +199,19 @@ decl_module! { members.sort(); >::mutate(|m| { T::MembershipChanged::set_members_sorted(&members[..], m); + Self::rejig_prime(&members); *m = members; }); + Self::deposit_event(RawEvent::MembersReset); } /// Swap out the sending member for some other key `new`. /// /// May only be called from `Signed` origin of a current member. + /// + /// Prime membership is passed from the origin account to `new`, if extant. #[weight = SimpleDispatchInfo::FixedNormal(50_000)] fn change_key(origin, new: T::AccountId) { let remove = ensure_signed(origin)?; @@ -211,14 +225,51 @@ decl_module! { >::put(&members); T::MembershipChanged::change_members_sorted( - &[new], - &[remove], + &[new.clone()], + &[remove.clone()], &members[..], ); + + if Prime::::get() == Some(remove) { + Prime::::put(&new); + T::MembershipChanged::set_prime(Some(new)); + } } Self::deposit_event(RawEvent::KeyChanged); } + + /// Set the prime member. Must be a current member. + #[weight = SimpleDispatchInfo::FixedNormal(50_000)] + fn set_prime(origin, who: T::AccountId) { + T::PrimeOrigin::try_origin(origin) + .map(|_| ()) + .or_else(ensure_root)?; + Self::members().binary_search(&who).ok().ok_or(Error::::NotMember)?; + Prime::::put(&who); + T::MembershipChanged::set_prime(Some(who)); + } + + /// Remove the prime member if it exists. + #[weight = SimpleDispatchInfo::FixedNormal(50_000)] + fn clear_prime(origin) { + T::PrimeOrigin::try_origin(origin) + .map(|_| ()) + .or_else(ensure_root)?; + Prime::::kill(); + T::MembershipChanged::set_prime(None); + } + } +} + +impl, I: Instance> Module { + fn rejig_prime(members: &[T::AccountId]) { + if let Some(prime) = Prime::::get() { + match members.binary_search(&prime) { + Ok(_) => T::MembershipChanged::set_prime(Some(prime)), + Err(_) => Prime::::kill(), + } + } } } @@ -283,6 +334,7 @@ mod tests { thread_local! { static MEMBERS: RefCell> = RefCell::new(vec![]); + static PRIME: RefCell> = RefCell::new(None); } pub struct TestChangeMembers; @@ -297,6 +349,10 @@ mod tests { assert_eq!(old_plus_incoming, new_plus_outgoing); MEMBERS.with(|m| *m.borrow_mut() = new.to_vec()); + PRIME.with(|p| *p.borrow_mut() = None); + } + fn set_prime(who: Option) { + PRIME.with(|p| *p.borrow_mut() = who); } } impl InitializeMembers for TestChangeMembers { @@ -311,6 +367,7 @@ mod tests { type RemoveOrigin = EnsureSignedBy; type SwapOrigin = EnsureSignedBy; type ResetOrigin = EnsureSignedBy; + type PrimeOrigin = EnsureSignedBy; type MembershipInitialized = TestChangeMembers; type MembershipChanged = TestChangeMembers; } @@ -337,6 +394,21 @@ mod tests { }); } + #[test] + fn prime_member_works() { + new_test_ext().execute_with(|| { + assert_noop!(Membership::set_prime(Origin::signed(4), 20), BadOrigin); + assert_noop!(Membership::set_prime(Origin::signed(5), 15), Error::::NotMember); + assert_ok!(Membership::set_prime(Origin::signed(5), 20)); + assert_eq!(Membership::prime(), Some(20)); + assert_eq!(PRIME.with(|m| *m.borrow()), Membership::prime()); + + assert_ok!(Membership::clear_prime(Origin::signed(5))); + assert_eq!(Membership::prime(), None); + assert_eq!(PRIME.with(|m| *m.borrow()), Membership::prime()); + }); + } + #[test] fn add_member_works() { new_test_ext().execute_with(|| { @@ -353,9 +425,12 @@ mod tests { new_test_ext().execute_with(|| { assert_noop!(Membership::remove_member(Origin::signed(5), 20), BadOrigin); assert_noop!(Membership::remove_member(Origin::signed(2), 15), Error::::NotMember); + assert_ok!(Membership::set_prime(Origin::signed(5), 20)); assert_ok!(Membership::remove_member(Origin::signed(2), 20)); assert_eq!(Membership::members(), vec![10, 30]); assert_eq!(MEMBERS.with(|m| m.borrow().clone()), Membership::members()); + assert_eq!(Membership::prime(), None); + assert_eq!(PRIME.with(|m| *m.borrow()), Membership::prime()); }); } @@ -365,11 +440,19 @@ mod tests { assert_noop!(Membership::swap_member(Origin::signed(5), 10, 25), BadOrigin); assert_noop!(Membership::swap_member(Origin::signed(3), 15, 25), Error::::NotMember); assert_noop!(Membership::swap_member(Origin::signed(3), 10, 30), Error::::AlreadyMember); + + assert_ok!(Membership::set_prime(Origin::signed(5), 20)); assert_ok!(Membership::swap_member(Origin::signed(3), 20, 20)); assert_eq!(Membership::members(), vec![10, 20, 30]); + assert_eq!(Membership::prime(), Some(20)); + assert_eq!(PRIME.with(|m| *m.borrow()), Membership::prime()); + + assert_ok!(Membership::set_prime(Origin::signed(5), 10)); assert_ok!(Membership::swap_member(Origin::signed(3), 10, 25)); assert_eq!(Membership::members(), vec![20, 25, 30]); assert_eq!(MEMBERS.with(|m| m.borrow().clone()), Membership::members()); + assert_eq!(Membership::prime(), None); + assert_eq!(PRIME.with(|m| *m.borrow()), Membership::prime()); }); } @@ -385,11 +468,14 @@ mod tests { #[test] fn change_key_works() { new_test_ext().execute_with(|| { + assert_ok!(Membership::set_prime(Origin::signed(5), 10)); assert_noop!(Membership::change_key(Origin::signed(3), 25), Error::::NotMember); assert_noop!(Membership::change_key(Origin::signed(10), 20), Error::::AlreadyMember); assert_ok!(Membership::change_key(Origin::signed(10), 40)); assert_eq!(Membership::members(), vec![20, 30, 40]); assert_eq!(MEMBERS.with(|m| m.borrow().clone()), Membership::members()); + assert_eq!(Membership::prime(), Some(40)); + assert_eq!(PRIME.with(|m| *m.borrow()), Membership::prime()); }); } @@ -405,10 +491,20 @@ mod tests { #[test] fn reset_members_works() { new_test_ext().execute_with(|| { + assert_ok!(Membership::set_prime(Origin::signed(5), 20)); assert_noop!(Membership::reset_members(Origin::signed(1), vec![20, 40, 30]), BadOrigin); + assert_ok!(Membership::reset_members(Origin::signed(4), vec![20, 40, 30])); assert_eq!(Membership::members(), vec![20, 30, 40]); assert_eq!(MEMBERS.with(|m| m.borrow().clone()), Membership::members()); + assert_eq!(Membership::prime(), Some(20)); + assert_eq!(PRIME.with(|m| *m.borrow()), Membership::prime()); + + assert_ok!(Membership::reset_members(Origin::signed(4), vec![10, 40, 30])); + assert_eq!(Membership::members(), vec![10, 30, 40]); + assert_eq!(MEMBERS.with(|m| m.borrow().clone()), Membership::members()); + assert_eq!(Membership::prime(), None); + assert_eq!(PRIME.with(|m| *m.borrow()), Membership::prime()); }); } } diff --git a/substrate/frame/support/src/storage/generator/value.rs b/substrate/frame/support/src/storage/generator/value.rs index 4083576e29..9e26131f48 100644 --- a/substrate/frame/support/src/storage/generator/value.rs +++ b/substrate/frame/support/src/storage/generator/value.rs @@ -91,6 +91,14 @@ impl> storage::StorageValue for G { unhashed::put(&Self::storage_value_final_key(), &val) } + fn set(maybe_val: Self::Query) { + if let Some(val) = G::from_query_to_optional_value(maybe_val) { + unhashed::put(&Self::storage_value_final_key(), &val) + } else { + unhashed::kill(&Self::storage_value_final_key()) + } + } + fn kill() { unhashed::kill(&Self::storage_value_final_key()) } diff --git a/substrate/frame/support/src/storage/mod.rs b/substrate/frame/support/src/storage/mod.rs index c28626ad2c..e5d845cb22 100644 --- a/substrate/frame/support/src/storage/mod.rs +++ b/substrate/frame/support/src/storage/mod.rs @@ -73,6 +73,10 @@ pub trait StorageValue { /// Store a value under this key into the provided storage instance. fn put>(val: Arg); + /// Store a value under this key into the provided storage instance; this uses the query + /// type rather than the underlying value. + fn set(val: Self::Query); + /// Mutate the value fn mutate R>(f: F) -> R; diff --git a/substrate/frame/support/src/traits.rs b/substrate/frame/support/src/traits.rs index bd6895bda5..c1e9e7c317 100644 --- a/substrate/frame/support/src/traits.rs +++ b/substrate/frame/support/src/traits.rs @@ -808,6 +808,8 @@ impl WithdrawReasons { pub trait ChangeMembers { /// A number of members `incoming` just joined the set and replaced some `outgoing` ones. The /// new set is given by `new`, and need not be sorted. + /// + /// This resets any previous value of prime. fn change_members(incoming: &[AccountId], outgoing: &[AccountId], mut new: Vec) { new.sort_unstable(); Self::change_members_sorted(incoming, outgoing, &new[..]); @@ -817,6 +819,8 @@ pub trait ChangeMembers { /// new set is thus given by `sorted_new` and **must be sorted**. /// /// NOTE: This is the only function that needs to be implemented in `ChangeMembers`. + /// + /// This resets any previous value of prime. fn change_members_sorted( incoming: &[AccountId], outgoing: &[AccountId], @@ -825,6 +829,8 @@ pub trait ChangeMembers { /// Set the new members; they **must already be sorted**. This will compute the diff and use it to /// call `change_members_sorted`. + /// + /// This resets any previous value of prime. fn set_members_sorted(new_members: &[AccountId], old_members: &[AccountId]) { let (incoming, outgoing) = Self::compute_members_diff(new_members, old_members); Self::change_members_sorted(&incoming[..], &outgoing[..], &new_members); @@ -865,14 +871,20 @@ pub trait ChangeMembers { } (incoming, outgoing) } + + /// Set the prime member. + fn set_prime(_prime: Option) {} } impl ChangeMembers for () { fn change_members(_: &[T], _: &[T], _: Vec) {} fn change_members_sorted(_: &[T], _: &[T], _: &[T]) {} fn set_members_sorted(_: &[T], _: &[T]) {} + fn set_prime(_: Option) {} } + + /// Trait for type that can handle the initialization of account IDs at genesis. pub trait InitializeMembers { /// Initialize the members to the given `members`.