Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Governance #388

Open
3 of 14 tasks
distributedstatemachine opened this issue May 1, 2024 · 1 comment · May be fixed by #425
Open
3 of 14 tasks

Governance #388

distributedstatemachine opened this issue May 1, 2024 · 1 comment · May be fixed by #425
Assignees
Labels
breaking-change This PR introduces a noteworthy breaking change red team feature development, etc

Comments

@distributedstatemachine
Copy link
Contributor

distributedstatemachine commented May 1, 2024

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

  • 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)

  • 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);

        // ...
    }

    // ...
}

// ...
  • Implement configurable minimum thresholds for each acceptance group.
// 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);
}

// ...
  • 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
# 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
# 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) 
  • move voting to a governance command
# bittensor/cli.py
# bittensor/commands/senate.py -> bittensor/commands/governance.py

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)
# 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)
  • UI to list all governance members
# bittensor/commands/governance.py

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 distributedstatemachine added red team feature development, etc breaking-change This PR introduces a noteworthy breaking change labels May 1, 2024
@distributedstatemachine
Copy link
Contributor Author

@sam0x17 breaking change

@camfairchild camfairchild linked a pull request May 16, 2024 that will close this issue
24 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
breaking-change This PR introduces a noteworthy breaking change red team feature development, etc
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants