subtensor icon indicating copy to clipboard operation
subtensor copied to clipboard

Governance

Open distributedstatemachine opened this issue 1 year ago • 4 comments

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)

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

// ...
  • [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
# 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 avatar May 01 '24 16:05 distributedstatemachine

@sam0x17 breaking change

distributedstatemachine avatar May 04 '24 04:05 distributedstatemachine

We need to research if governance FRAME pallet can be used.

gztensor avatar Apr 25 '25 18:04 gztensor

how is "X is a sn owner and in triumvirate" handled? That's fine?

sam0x17 avatar Apr 25 '25 18:04 sam0x17

Lmao! This was on the list of my 30 deliverables when I joined the OTF 😂😂😂

distributedstatemachine avatar Apr 30 '25 10:04 distributedstatemachine