The current governance mechanism in the Subtensor blockchain needs to be revised to introduce a new group called "SubnetOwners" alongside the existing "Triumvirate" and "Senate" groups. The goal is to establish a checks and balances system where a proposal must be accepted by the other two groups in order to pass.
For instance, if the Triumvirate proposes a change, both the SubnetOwners and Senate must accept it for the proposal to be enacted. Each acceptance group should have a configurable minimum threshold for proposal acceptance.
Acceptance Criteria
Introduce a new "SubnetOwners" group in the governance mechanism.
Modify the proposal process to require acceptance from the other two groups for a proposal to pass.
Implement configurable minimum thresholds for each acceptance group.
Update the existing code to accommodate the new governance structure.
Tasks
Substrate (rust)
[x] Create a new SubnetOwners struct and associated storage items.
// runtime/src/lib.rs
// ...
pub struct SubnetOwners;
impl SubnetOwners {
fn is_member(account: &AccountId) -> bool {
// Implement logic to check if an account is a member of SubnetOwners
// ...
}
fn members() -> Vec<AccountId> {
// Implement logic to retrieve the list of SubnetOwners members
// ...
}
fn max_members() -> u32 {
// Implement logic to retrieve the maximum number of SubnetOwners members
// ...
}
}
// ...
[ ] Modify the propose function to include the new acceptance requirements.
// pallets/collective/src/lib.rs
// ...
#[pallet::call]
impl<T: Config<I>, I: 'static> Pallet<T, I> {
// ...
#[pallet::call_index(2)]
#[pallet::weight(/* ... */)]
pub fn propose(
origin: OriginFor<T>,
proposal: Box<<T as Config<I>>::Proposal>,
#[pallet::compact] length_bound: u32,
duration: BlockNumberFor<T>,
) -> DispatchResultWithPostInfo {
// ...
// Check if the proposer is a member of the Triumvirate
ensure!(T::CanPropose::can_propose(&who), Error::<T, I>::NotMember);
// ...
// Initialize vote trackers for Senate and SubnetOwners
let senate_votes = Votes {
index,
threshold: SenateThreshold::get(),
ayes: sp_std::vec![],
nays: sp_std::vec![],
end,
};
let subnet_owners_votes = Votes {
index,
threshold: SubnetOwnersThreshold::get(),
ayes: sp_std::vec![],
nays: sp_std::vec![],
end,
};
// Store the vote trackers
<SenateVoting<T, I>>::insert(proposal_hash, senate_votes);
<SubnetOwnersVoting<T, I>>::insert(proposal_hash, subnet_owners_votes);
// ...
}
// ...
}
// ...
[x] Implement configurable minimum thresholds for each acceptance group.
[x] Update the do_vote function to handle voting from the new SubnetOwners group.
// pallets/collective/src/lib.rs
impl<T: Config<I>, I: 'static> Pallet<T, I> {
// ...
pub fn do_vote(
who: T::AccountId,
proposal: T::Hash,
index: ProposalIndex,
approve: bool,
) -> DispatchResult {
// ...
// Check if the voter is a member of the Senate or SubnetOwners
if Senate::is_member(&who) {
// Update the Senate vote tracker
<SenateVoting<T, I>>::mutate(proposal, |v| {
if let Some(mut votes) = v.take() {
if approve {
votes.ayes.push(who.clone());
} else {
votes.nays.push(who.clone());
}
*v = Some(votes);
}
});
} else if SubnetOwners::is_member(&who) {
// Update the SubnetOwners vote tracker
<SubnetOwnersVoting<T, I>>::mutate(proposal, |v| {
if let Some(mut votes) = v.take() {
if approve {
votes.ayes.push(who.clone());
} else {
votes.nays.push(who.clone());
}
*v = Some(votes);
}
});
} else {
return Err(Error::<T, I>::NotMember.into());
}
// ...
}
// ...
}
// pallets/collective/src/lib.rs
// ...
impl<T: Config<I>, I: 'static> Pallet<T, I> {
// ...
pub fn do_vote(
who: T::AccountId,
proposal: T::Hash,
index: ProposalIndex,
approve: bool,
) -> DispatchResult {
// ...
// Check if the voter is a member of the Senate or SubnetOwners
if Senate::is_member(&who) {
// Update the Senate vote tracker
<SenateVoting<T, I>>::mutate(proposal, |v| {
if let Some(mut votes) = v.take() {
if approve {
votes.ayes.push(who.clone());
} else {
votes.nays.push(who.clone());
}
*v = Some(votes);
}
});
} else if SubnetOwners::is_member(&who) {
// Update the SubnetOwners vote tracker
<SubnetOwnersVoting<T, I>>::mutate(proposal, |v| {
if let Some(mut votes) = v.take() {
if approve {
votes.ayes.push(who.clone());
} else {
votes.nays.push(who.clone());
}
*v = Some(votes);
}
});
} else {
return Err(Error::<T, I>::NotMember.into());
}
// ...
}
// ...
}
// ...
[ ] Migrate the collective pallet name/storage
let old_pallet = "Triumvirate";
let new_pallet = <Governance as PalletInfoAccess>::name();
frame_support::storage::migration::move_pallet(
new_pallet.as_bytes(),
old_pallet.as_bytes(),
);
Python API
[ ] call to grab the list of subnet owners (governance members)
# bittensor/subtensor.py
class subtensor:
# ...
def get_subnet_owners_members(self, block: Optional[int] = None) -> Optional[List[str]]:
subnet_owners_members = self.query_module("SubnetOwnersMembers", "Members", block=block)
if not hasattr(subnet_owners_members, "serialize"):
return None
return subnet_owners_members.serialize() if subnet_owners_members != None else None
- [ ] call to grab the list of governance members
```python
# bittensor/subtensor.py
class subtensor:
# ...
def get_governance_members(self, block: Optional[int] = None) -> Optional[List[Tuple[str, Tuple[Union[GovernanceEnum, str]]]]]:
senate_members = self.get_senate_members(block=block)
subnet_owners_members = self.get_subnet_owners_members(block=block)
triumvirate_members = self.get_triumvirate_members(block=block)
if senate_members is None and subnet_owners_members is None and triumvirate_members is None:
return None
governance_members = {}
for member in senate_members:
governance_members[member] = (GovernanceEnum.Senate)
for member in subnet_owners_members:
if member not in governance_members:
governance_members[member] = ()
governance_members[member] += (GovernanceEnum.SubnetOwner)
for member in triumvirate_members:
if member not in governance_members:
governance_members[member] = ()
governance_members[member] += (GovernanceEnum.Triumvirate)
return [item for item in governance_members.items()]
[ ] call to vote as a subnet owner member
# bittensor/subtensor.py
class subtensor:
# ...
def vote_subnet_owner(self, wallet=wallet,
proposal_hash: str,
proposal_idx: int,
vote: bool,
) -> bool:
return vote_subnet_owner_extrinsic(...)
def vote_senate_extrinsic(
subtensor: "bittensor.subtensor",
wallet: "bittensor.wallet",
proposal_hash: str,
proposal_idx: int,
vote: bool,
wait_for_inclusion: bool = False,
wait_for_finalization: bool = True,
prompt: bool = False,
) -> bool:
r"""Votes ayes or nays on proposals."""
if prompt:
# Prompt user for confirmation.
if not Confirm.ask("Cast a vote of {}?".format(vote)):
return False
# Unlock coldkey
wallet.coldkey
with bittensor.__console__.status(":satellite: Casting vote.."):
with subtensor.substrate as substrate:
# create extrinsic call
call = substrate.compose_call(
call_module="SubtensorModule",
call_function="subnet_owner_vote",
call_params={
"proposal": proposal_hash,
"index": proposal_idx,
"approve": vote,
},
)
# Sign using coldkey
# ...
bittensor.__console__.print(
":white_heavy_check_mark: [green]Vote cast.[/green]"
)
return True
- [ ] call to vote as a governance member
```python
# bittensor/subtensor.py
class subtensor:
# ...
def vote_governance(self, wallet=wallet,
proposal_hash: str,
proposal_idx: int,
vote: bool,
group_choice: Tuple[GovernanceEnum],
) -> Tuple[bool]:
result = []
for group in group_choice:
if GovernanceEnum.Senate == group:
result.append( self.vote_senate(...) )
if GovernanceEnum.Triumvirate == group:
result.append( self.vote_triumvirate(...) )
if GovernanceEnum.SubnetOwner == group:
result.append( self.vote_subnet_owner(...) )
return tuple(result)
- [ ] UI to vote as a governance member (now including subnet owners)
```python
# bittensor/commands/governance.py
class VoteCommand:
@staticmethod
def run(cli: "bittensor.cli"):
# ...
@staticmethod
def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"):
r"""Vote in Bittensor's governance protocol proposals"""
wallet = bittensor.wallet(config=cli.config)
# ...
member_groups = subtensor.get_governance_groups(hotkey, coldkey)
if len(member_groups) == 0:
# Abort; Not a governance member
return
elif len(member_groups) > 1: # belongs to multiple groups
# Ask which group(s) to vote as
group_choice = ask_group_select( member_groups )
else: # belongs to only one group
group_choice = member_groups
# ...
subtensor.governance_vote(
wallet=wallet,
proposal_hash=proposal_hash,
proposal_idx=vote_data["index"],
vote=vote,
group_choice=group_choice,
)
# ...
@classmethod
def add_args(cls, parser: argparse.ArgumentParser):
vote_parser = parser.add_parser(
"vote", help="""Vote on an active proposal by hash."""
)
vote_parser.add_argument(
"--proposal",
dest="proposal_hash",
type=str,
nargs="?",
help="""Set the proposal to show votes for.""",
default="",
)
bittensor.wallet.add_args(vote_parser)
bittensor.subtensor.add_args(vote_parser)
Description
The current governance mechanism in the Subtensor blockchain needs to be revised to introduce a new group called "SubnetOwners" alongside the existing "Triumvirate" and "Senate" groups. The goal is to establish a checks and balances system where a proposal must be accepted by the other two groups in order to pass.
For instance, if the Triumvirate proposes a change, both the SubnetOwners and Senate must accept it for the proposal to be enacted. Each acceptance group should have a configurable minimum threshold for proposal acceptance.
Acceptance Criteria
Tasks
Substrate (rust)
SubnetOwners
struct and associated storage items.propose
function to include the new acceptance requirements.do_vote
function to handle voting from the newSubnetOwners
group.Python API
class subtensor:
class subtensor:
COMMANDS = { "governance": { "name": "governance", "aliases": ["g", "gov"], "help": "Commands for managing and viewing governance.", "commands": { "list": GovernanceListCommand, "senate_vote": SenateVoteCommand, "senate": SenateCommand, "owner_vote": OwnerVoteCommand, "proposals": ProposalsCommand, "register": SenateRegisterCommand, # prev: RootRegisterCommand }, }, ... }
class GovernanceMembersCommand:
...