opentensor / subtensor

Bittensor Blockchain Layer
The Unlicense
163 stars 161 forks source link

Governance #388

Open distributedstatemachine opened 7 months ago

distributedstatemachine commented 7 months ago

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)

// 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
        // ...
    }
}

// ...
// 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);

        // ...
    }

    // ...
}

// ...
// runtime/src/lib.rs

// ...

parameter_types! {
    pub const TriumvirateThreshold: Permill = Permill::from_percent(60);
    pub const SenateThreshold: Permill = Permill::from_percent(50);
    pub const SubnetOwnersThreshold: Permill = Permill::from_percent(40);
}

// ...
// 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());
        }

        // ...
    }

    // ...
}

// ...

Python API

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()]

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) 

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 }, }, ... }


- [ ] 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)

class GovernanceMembersCommand:

...

@staticmethod
def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"):
    r"""View Bittensor's governance protocol members"""

    # ...

    senate_members = subtensor.get_governance_members()

    table = Table(show_footer=False)
    table.title = "[white]Senate"
    table.add_column(
        "[overline white]NAME",
        footer_style="overline white",
        style="rgb(50,163,219)",
        no_wrap=True,
    )
    table.add_column(
        "[overline white]ADDRESS",
        footer_style="overline white",
        style="yellow",
        no_wrap=True,
    )
    table.add_column(
        "[overline white]GROUP(S)",
        footer_style="overline white",
        style="yellow",
        no_wrap=True,
    )
    table.show_footer = True

    for ss58_address, groups in governance_members:
        table.add_row(
            (
                delegate_info[ss58_address].name
                if ss58_address in delegate_info
                else ""
            ),
            ss58_address,
            " ".join(groups), # list all groups
        )

    table.box = None
    table.pad_edge = False
    table.width = None
    console.print(table)

# ...

@classmethod
def add_args(cls, parser: argparse.ArgumentParser):
    member_parser = parser.add_parser(
        "members", help="""View all the governance members"""
    )

    bittensor.wallet.add_args(senate_parser)
    bittensor.subtensor.add_args(senate_parser)


## TODO:

- [ ] Python side of things
- [ ] Senate Registrations are currently via the root network . How does this change in a post DTAO world?
distributedstatemachine commented 7 months ago

@sam0x17 breaking change