From 918a64c747a1e770bc4afcb18a514d352f42ef94 Mon Sep 17 00:00:00 2001 From: Rubberbandits Date: Wed, 31 May 2023 18:42:50 +0000 Subject: [PATCH 01/22] Begin senate cli --- bittensor/_cli/__init__.py | 3 + bittensor/_cli/cli_impl.py | 2 + bittensor/_cli/commands/__init__.py | 1 + bittensor/_cli/commands/senate.py | 93 ++++++++++++++++++++++++++ bittensor/_subtensor/chain_data.py | 7 ++ bittensor/_subtensor/subtensor_impl.py | 8 +++ 6 files changed, 114 insertions(+) create mode 100644 bittensor/_cli/commands/senate.py diff --git a/bittensor/_cli/__init__.py b/bittensor/_cli/__init__.py index 8d727a9696..08c41fb6ef 100644 --- a/bittensor/_cli/__init__.py +++ b/bittensor/_cli/__init__.py @@ -84,6 +84,7 @@ def __create_parser__() -> 'argparse.ArgumentParser': ListDelegatesCommand.add_args( cmd_parsers ) RegenColdkeypubCommand.add_args( cmd_parsers ) RecycleRegisterCommand.add_args( cmd_parsers ) + SenateCommand.add_args( cmd_parsers ) return parser @@ -156,6 +157,8 @@ def check_config (config: 'bittensor.Config'): MyDelegatesCommand.check_config( config ) elif config.command == "recycle_register": RecycleRegisterCommand.check_config( config ) + elif config.command == "senate": + SenateCommand.check_config( config ) else: console.print(":cross_mark:[red]Unknown command: {}[/red]".format(config.command)) sys.exit() diff --git a/bittensor/_cli/cli_impl.py b/bittensor/_cli/cli_impl.py index 6c0f6dde80..b73c657c7b 100644 --- a/bittensor/_cli/cli_impl.py +++ b/bittensor/_cli/cli_impl.py @@ -82,4 +82,6 @@ def run ( self ): ListSubnetsCommand.run( self ) elif self.config.command == 'recycle_register': RecycleRegisterCommand.run( self ) + elif self.config.command == "senate": + SenateCommand.run( self ) diff --git a/bittensor/_cli/commands/__init__.py b/bittensor/_cli/commands/__init__.py index 5d501da284..03c2e43e0a 100644 --- a/bittensor/_cli/commands/__init__.py +++ b/bittensor/_cli/commands/__init__.py @@ -9,3 +9,4 @@ from .metagraph import MetagraphCommand from .list import ListCommand from .misc import UpdateCommand, ListSubnetsCommand +from .senate import SenateCommand diff --git a/bittensor/_cli/commands/senate.py b/bittensor/_cli/commands/senate.py new file mode 100644 index 0000000000..c206baa2ac --- /dev/null +++ b/bittensor/_cli/commands/senate.py @@ -0,0 +1,93 @@ +# The MIT License (MIT) +# Copyright © 2021 Yuma Rao + +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the “Software”), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of +# the Software. + +# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +import sys +import argparse +import bittensor +from tqdm import tqdm +from rich.prompt import Confirm +from rich.prompt import Confirm, Prompt +from bittensor.utils.balance import Balance +from typing import List, Union, Optional, Dict, Tuple +from .utils import get_hotkey_wallets_for_wallet +console = bittensor.__console__ + +class SenateCommand: + + @staticmethod + def run( cli ): + r""" Participate in Bittensor's Senate with your senator hotkey. + """ + config = cli.config.copy() + wallet = bittensor.wallet( config = config ) + subtensor: bittensor.Subtensor = bittensor.subtensor( config = config ) + + # Get coldkey balance + wallet_balance: Balance = subtensor.get_balance( wallet.coldkeypub.ss58_address ) + console.print( wallet_balance ) + + @classmethod + def check_config( cls, config: 'bittensor.Config' ): + if config.wallet.get('name') == bittensor.defaults.wallet.name and not config.no_prompt: + wallet_name = Prompt.ask("Enter wallet name", default = bittensor.defaults.wallet.name) + config.wallet.name = str(wallet_name) + + if config.wallet.get('hotkey') == bittensor.defaults.wallet.hotkey and not config.no_prompt and not config.get('all_hotkeys') and not config.get('hotkeys'): + hotkey = Prompt.ask("Enter hotkey name", default = bittensor.defaults.wallet.hotkey) + config.wallet.hotkey = str(hotkey) + + @classmethod + def add_args( cls, parser: argparse.ArgumentParser ): + senate_parser = parser.add_parser( + 'senate', + help='''Participate in senate motions with a senator hotkey''' + ) + senate_parser.add_argument( + '--no_version_checking', + action='store_true', + help='''Set false to stop cli version checking''', + default = False + ) + senate_parser.add_argument( + '--no_prompt', + dest='no_prompt', + action='store_true', + help='''Set true to avoid prompting the user.''', + default=False, + ) + senate_parser.add_argument( + '--hotkeys', + '--exclude_hotkeys', + '--wallet.hotkeys', + '--wallet.exclude_hotkeys', + required=False, + action='store', + default=[], + type=str, + nargs='*', + help='''Specify the hotkeys by name or ss58 address. (e.g. hk1 hk2 hk3)''' + ) + senate_parser.add_argument( + '--all_hotkeys', + '--wallet.all_hotkeys', + required=False, + action='store_true', + default=False, + help='''To specify all hotkeys. Specifying hotkeys will exclude them from this all.''' + ) + bittensor.wallet.add_args( senate_parser ) + bittensor.subtensor.add_args( senate_parser ) \ No newline at end of file diff --git a/bittensor/_subtensor/chain_data.py b/bittensor/_subtensor/chain_data.py index 97e6174ca8..d3be27577d 100644 --- a/bittensor/_subtensor/chain_data.py +++ b/bittensor/_subtensor/chain_data.py @@ -138,6 +138,12 @@ ["port", "u16"], ["ip_type", "u8"], ], + }, + "Proposals": { + "type": "struct", + "type_mapping": [ + ["netuid", "Compact"], + ] }, } } @@ -148,6 +154,7 @@ class ChainDataType(Enum): DelegateInfo = 3 NeuronInfoLite = 4 DelegatedInfo = 5 + ProposalInfo = 6 # Constants RAOPERTAO = 1e9 diff --git a/bittensor/_subtensor/subtensor_impl.py b/bittensor/_subtensor/subtensor_impl.py index 10df88a7a6..acee869a70 100644 --- a/bittensor/_subtensor/subtensor_impl.py +++ b/bittensor/_subtensor/subtensor_impl.py @@ -965,6 +965,14 @@ def metagraph( self, netuid: int, lite: bool = True ) -> 'bittensor.Metagraph': + ################ + #### Senate #### + ################ + + def get_proposals (self, block: Optional[int] = None ) -> List['bittensor.Balance']: + return bittensor.Balance.from_rao( self.query_subtensor( 'TotalIssuance', block ).value ) + + ################ #### Legacy #### ################ From 90e2a095c3ca23293f3b07b7bbbb77ac483315a9 Mon Sep 17 00:00:00 2001 From: Rubberbandits Date: Mon, 19 Jun 2023 14:55:50 -0700 Subject: [PATCH 02/22] Add helper functions to subtensor object for query_module, query_module_map --- bittensor/_subtensor/subtensor_impl.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/bittensor/_subtensor/subtensor_impl.py b/bittensor/_subtensor/subtensor_impl.py index acee869a70..35a762cba2 100644 --- a/bittensor/_subtensor/subtensor_impl.py +++ b/bittensor/_subtensor/subtensor_impl.py @@ -402,6 +402,32 @@ def make_substrate_call_with_retry(): block_hash = None if block == None else substrate.get_block_hash(block) ) return make_substrate_call_with_retry() + + """ Queries any module storage with params and block. """ + def query_module( self, module: str, name: str, block: Optional[int] = None, params: Optional[List[object]] = [] ) -> Optional[object]: + @retry(delay=2, tries=3, backoff=2, max_delay=4) + def make_substrate_call_with_retry(): + with self.substrate as substrate: + return substrate.query( + module=module, + storage_function = name, + params = params, + block_hash = None if block == None else substrate.get_block_hash(block) + ) + return make_substrate_call_with_retry() + + """ Queries any module map storage with params and block. """ + def query_map( self, module: str, name: str, block: Optional[int] = None, params: Optional[List[object]] = [] ) -> Optional[object]: + @retry(delay=2, tries=3, backoff=2, max_delay=4) + def make_substrate_call_with_retry(): + with self.substrate as substrate: + return substrate.query_map( + module=module, + storage_function = name, + params = params, + block_hash = None if block == None else substrate.get_block_hash(block) + ) + return make_substrate_call_with_retry() ##################################### #### Hyper parameter calls. #### From 7b6077766099d6d4233ea782b3430adf54d5a150 Mon Sep 17 00:00:00 2001 From: Rubberbandits Date: Mon, 19 Jun 2023 16:09:17 -0700 Subject: [PATCH 03/22] Remove unused senate helpers --- bittensor/_subtensor/subtensor_impl.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/bittensor/_subtensor/subtensor_impl.py b/bittensor/_subtensor/subtensor_impl.py index 7c756ddfbd..82bada0a16 100644 --- a/bittensor/_subtensor/subtensor_impl.py +++ b/bittensor/_subtensor/subtensor_impl.py @@ -989,14 +989,6 @@ def metagraph( self, netuid: int, lite: bool = True, block: Optional[int] = None return metagraph_ - ################ - #### Senate #### - ################ - - def get_proposals (self, block: Optional[int] = None ) -> List['bittensor.Balance']: - return bittensor.Balance.from_rao( self.query_subtensor( 'TotalIssuance', block ).value ) - - ################ #### Legacy #### ################ From d042aa1204f2878f449b60b4e8dcb4ef43422441 Mon Sep 17 00:00:00 2001 From: Rubberbandits Date: Mon, 19 Jun 2023 16:09:43 -0700 Subject: [PATCH 04/22] Rename SenateCommand -> ProposalsCommand --- bittensor/_cli/__init__.py | 6 +++--- bittensor/_cli/cli_impl.py | 4 ++-- bittensor/_cli/commands/__init__.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/bittensor/_cli/__init__.py b/bittensor/_cli/__init__.py index 08c41fb6ef..290fbcbd97 100644 --- a/bittensor/_cli/__init__.py +++ b/bittensor/_cli/__init__.py @@ -84,7 +84,7 @@ def __create_parser__() -> 'argparse.ArgumentParser': ListDelegatesCommand.add_args( cmd_parsers ) RegenColdkeypubCommand.add_args( cmd_parsers ) RecycleRegisterCommand.add_args( cmd_parsers ) - SenateCommand.add_args( cmd_parsers ) + ProposalsCommand.add_args( cmd_parsers ) return parser @@ -157,8 +157,8 @@ def check_config (config: 'bittensor.Config'): MyDelegatesCommand.check_config( config ) elif config.command == "recycle_register": RecycleRegisterCommand.check_config( config ) - elif config.command == "senate": - SenateCommand.check_config( config ) + elif config.command == "proposals": + ProposalsCommand.check_config( config ) else: console.print(":cross_mark:[red]Unknown command: {}[/red]".format(config.command)) sys.exit() diff --git a/bittensor/_cli/cli_impl.py b/bittensor/_cli/cli_impl.py index b73c657c7b..0f69589acf 100644 --- a/bittensor/_cli/cli_impl.py +++ b/bittensor/_cli/cli_impl.py @@ -82,6 +82,6 @@ def run ( self ): ListSubnetsCommand.run( self ) elif self.config.command == 'recycle_register': RecycleRegisterCommand.run( self ) - elif self.config.command == "senate": - SenateCommand.run( self ) + elif self.config.command == "proposals": + ProposalsCommand.run( self ) diff --git a/bittensor/_cli/commands/__init__.py b/bittensor/_cli/commands/__init__.py index 03c2e43e0a..737ab89467 100644 --- a/bittensor/_cli/commands/__init__.py +++ b/bittensor/_cli/commands/__init__.py @@ -9,4 +9,4 @@ from .metagraph import MetagraphCommand from .list import ListCommand from .misc import UpdateCommand, ListSubnetsCommand -from .senate import SenateCommand +from .senate import ProposalsCommand From 446eb5fdd5981829c10abbe35729f5d872bf527b Mon Sep 17 00:00:00 2001 From: Rubberbandits Date: Mon, 19 Jun 2023 16:09:59 -0700 Subject: [PATCH 05/22] Remove unused proposals info datatype --- bittensor/_subtensor/chain_data.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/bittensor/_subtensor/chain_data.py b/bittensor/_subtensor/chain_data.py index d3be27577d..97e6174ca8 100644 --- a/bittensor/_subtensor/chain_data.py +++ b/bittensor/_subtensor/chain_data.py @@ -138,12 +138,6 @@ ["port", "u16"], ["ip_type", "u8"], ], - }, - "Proposals": { - "type": "struct", - "type_mapping": [ - ["netuid", "Compact"], - ] }, } } @@ -154,7 +148,6 @@ class ChainDataType(Enum): DelegateInfo = 3 NeuronInfoLite = 4 DelegatedInfo = 5 - ProposalInfo = 6 # Constants RAOPERTAO = 1e9 From 0429e41a7acd5abbdcc929bcefd7f67f1a8d92a5 Mon Sep 17 00:00:00 2001 From: Rubberbandits Date: Mon, 19 Jun 2023 16:10:24 -0700 Subject: [PATCH 06/22] Add proposals data in rich table --- bittensor/_cli/commands/senate.py | 97 +++++++++++++++++-------------- 1 file changed, 52 insertions(+), 45 deletions(-) diff --git a/bittensor/_cli/commands/senate.py b/bittensor/_cli/commands/senate.py index c206baa2ac..a5030ccb9c 100644 --- a/bittensor/_cli/commands/senate.py +++ b/bittensor/_cli/commands/senate.py @@ -18,76 +18,83 @@ import sys import argparse import bittensor -from tqdm import tqdm -from rich.prompt import Confirm -from rich.prompt import Confirm, Prompt -from bittensor.utils.balance import Balance +from rich.prompt import Prompt +from rich.table import Table from typing import List, Union, Optional, Dict, Tuple -from .utils import get_hotkey_wallets_for_wallet console = bittensor.__console__ -class SenateCommand: +class ProposalsCommand: @staticmethod def run( cli ): - r""" Participate in Bittensor's Senate with your senator hotkey. + r""" View Bittensor's governance protocol proposals """ config = cli.config.copy() - wallet = bittensor.wallet( config = config ) subtensor: bittensor.Subtensor = bittensor.subtensor( config = config ) - # Get coldkey balance - wallet_balance: Balance = subtensor.get_balance( wallet.coldkeypub.ss58_address ) - console.print( wallet_balance ) + console.print(":satellite: Syncing with chain: [white]{}[/white] ...".format(cli.config.subtensor.network)) + + proposals = dict() + proposal_hashes = subtensor.query_module("Triumvirate", "Proposals") + + for hash in proposal_hashes: + proposals[hash] = [ + subtensor.query_module("Triumvirate", "ProposalOf", None, [hash]), + subtensor.query_module("Triumvirate", "Voting", None, [hash]) + ] + + table = Table(show_footer=False) + table.title = ( + "[white]Proposals:" + ) + table.add_column("[overline white]HASH", footer_style = "overline white", style='yellow', no_wrap=True) + table.add_column("[overline white]THRESHOLD", footer_style = "overline white", style='white') + table.add_column("[overline white]AYES", footer_style = "overline white", style='green') + table.add_column("[overline white]NAYS", footer_style = "overline white", style='red') + table.add_column("[overline white]END", footer_style = "overline white", style='blue') + table.add_column("[overline white]CALLDATA", footer_style = "overline white", style='white') + table.show_footer = True + + for hash in proposals: + call_data = proposals[hash][0].serialize() + vote_data = proposals[hash][1].serialize() + + table.add_row( + hash, + str(vote_data["threshold"]), + str(len(vote_data["ayes"])), + str(len(vote_data["nays"])), + str(vote_data["end"]), + "{}: {}".format(call_data["call_function"], str(call_data["call_args"])) + ) + + table.box = None + table.pad_edge = False + table.width = None + console.print(table) @classmethod def check_config( cls, config: 'bittensor.Config' ): - if config.wallet.get('name') == bittensor.defaults.wallet.name and not config.no_prompt: - wallet_name = Prompt.ask("Enter wallet name", default = bittensor.defaults.wallet.name) - config.wallet.name = str(wallet_name) - - if config.wallet.get('hotkey') == bittensor.defaults.wallet.hotkey and not config.no_prompt and not config.get('all_hotkeys') and not config.get('hotkeys'): - hotkey = Prompt.ask("Enter hotkey name", default = bittensor.defaults.wallet.hotkey) - config.wallet.hotkey = str(hotkey) + None @classmethod def add_args( cls, parser: argparse.ArgumentParser ): - senate_parser = parser.add_parser( - 'senate', - help='''Participate in senate motions with a senator hotkey''' + proposals_parser = parser.add_parser( + 'proposals', + help='''View active triumvirate proposals and their status''' ) - senate_parser.add_argument( + proposals_parser.add_argument( '--no_version_checking', action='store_true', help='''Set false to stop cli version checking''', default = False ) - senate_parser.add_argument( + proposals_parser.add_argument( '--no_prompt', dest='no_prompt', action='store_true', help='''Set true to avoid prompting the user.''', default=False, ) - senate_parser.add_argument( - '--hotkeys', - '--exclude_hotkeys', - '--wallet.hotkeys', - '--wallet.exclude_hotkeys', - required=False, - action='store', - default=[], - type=str, - nargs='*', - help='''Specify the hotkeys by name or ss58 address. (e.g. hk1 hk2 hk3)''' - ) - senate_parser.add_argument( - '--all_hotkeys', - '--wallet.all_hotkeys', - required=False, - action='store_true', - default=False, - help='''To specify all hotkeys. Specifying hotkeys will exclude them from this all.''' - ) - bittensor.wallet.add_args( senate_parser ) - bittensor.subtensor.add_args( senate_parser ) \ No newline at end of file + bittensor.wallet.add_args( proposals_parser ) + bittensor.subtensor.add_args( proposals_parser ) \ No newline at end of file From 144b97b5c7e493ee5edc55b1951798b6aa0bb4f9 Mon Sep 17 00:00:00 2001 From: Rubberbandits Date: Mon, 19 Jun 2023 16:22:14 -0700 Subject: [PATCH 07/22] Clarify + beautify calldata column --- bittensor/_cli/commands/senate.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/bittensor/_cli/commands/senate.py b/bittensor/_cli/commands/senate.py index a5030ccb9c..57f4d8cfb8 100644 --- a/bittensor/_cli/commands/senate.py +++ b/bittensor/_cli/commands/senate.py @@ -59,13 +59,17 @@ def run( cli ): call_data = proposals[hash][0].serialize() vote_data = proposals[hash][1].serialize() + human_call_data = list() + for arg in call_data["call_args"]: + human_call_data.append("{}: {}".format(arg["name"], str(arg["value"]))) + table.add_row( hash, str(vote_data["threshold"]), str(len(vote_data["ayes"])), str(len(vote_data["nays"])), str(vote_data["end"]), - "{}: {}".format(call_data["call_function"], str(call_data["call_args"])) + "{}({})".format(call_data["call_function"], ", ".join(human_call_data)) ) table.box = None From e63d6fb3863b09f75dc77022819e4f0961aba8e3 Mon Sep 17 00:00:00 2001 From: Rubberbandits Date: Mon, 19 Jun 2023 22:35:50 -0700 Subject: [PATCH 08/22] Add SenateRegisterCommand --- bittensor/_cli/__init__.py | 3 + bittensor/_cli/cli_impl.py | 2 + bittensor/_cli/commands/__init__.py | 2 +- bittensor/_cli/commands/senate.py | 58 ++++++++++++++- bittensor/_subtensor/extrinsics/senate.py | 90 +++++++++++++++++++++++ bittensor/_subtensor/subtensor_impl.py | 15 +++- 6 files changed, 166 insertions(+), 4 deletions(-) create mode 100644 bittensor/_subtensor/extrinsics/senate.py diff --git a/bittensor/_cli/__init__.py b/bittensor/_cli/__init__.py index 290fbcbd97..e4b43d3e01 100644 --- a/bittensor/_cli/__init__.py +++ b/bittensor/_cli/__init__.py @@ -85,6 +85,7 @@ def __create_parser__() -> 'argparse.ArgumentParser': RegenColdkeypubCommand.add_args( cmd_parsers ) RecycleRegisterCommand.add_args( cmd_parsers ) ProposalsCommand.add_args( cmd_parsers ) + SenateRegisterCommand.add_args( cmd_parsers ) return parser @@ -159,6 +160,8 @@ def check_config (config: 'bittensor.Config'): RecycleRegisterCommand.check_config( config ) elif config.command == "proposals": ProposalsCommand.check_config( config ) + elif config.command == "senate_register": + SenateRegisterCommand.check_config( config ) else: console.print(":cross_mark:[red]Unknown command: {}[/red]".format(config.command)) sys.exit() diff --git a/bittensor/_cli/cli_impl.py b/bittensor/_cli/cli_impl.py index 0f69589acf..ddc7f9a40a 100644 --- a/bittensor/_cli/cli_impl.py +++ b/bittensor/_cli/cli_impl.py @@ -84,4 +84,6 @@ def run ( self ): RecycleRegisterCommand.run( self ) elif self.config.command == "proposals": ProposalsCommand.run( self ) + elif self.config.command == "senate_register": + SenateRegisterCommand.run( self ) diff --git a/bittensor/_cli/commands/__init__.py b/bittensor/_cli/commands/__init__.py index 737ab89467..5f397d6d99 100644 --- a/bittensor/_cli/commands/__init__.py +++ b/bittensor/_cli/commands/__init__.py @@ -9,4 +9,4 @@ from .metagraph import MetagraphCommand from .list import ListCommand from .misc import UpdateCommand, ListSubnetsCommand -from .senate import ProposalsCommand +from .senate import ProposalsCommand, SenateRegisterCommand diff --git a/bittensor/_cli/commands/senate.py b/bittensor/_cli/commands/senate.py index 57f4d8cfb8..08e06a18cf 100644 --- a/bittensor/_cli/commands/senate.py +++ b/bittensor/_cli/commands/senate.py @@ -101,4 +101,60 @@ def add_args( cls, parser: argparse.ArgumentParser ): default=False, ) bittensor.wallet.add_args( proposals_parser ) - bittensor.subtensor.add_args( proposals_parser ) \ No newline at end of file + bittensor.subtensor.add_args( proposals_parser ) + +class SenateRegisterCommand: + + @staticmethod + def run( cli ): + r""" Register to participate in Bittensor's governance protocol proposals + """ + config = cli.config.copy() + wallet = bittensor.wallet( config = cli.config ) + subtensor: bittensor.Subtensor = bittensor.subtensor( config = config ) + + # Unlock the wallet. + wallet.hotkey + wallet.coldkey + + # Check if the hotkey is a delegate. + if not subtensor.is_hotkey_delegate( wallet.hotkey.ss58_address ): + console.print('Aborting: Hotkey {} isn\'t a delegate.'.format(wallet.hotkey.ss58_address)) + return + + subtensor.register_senate( + wallet = wallet, + prompt = not cli.config.no_prompt + ) + + @classmethod + def check_config( cls, config: 'bittensor.Config' ): + if not config.is_set('wallet.name') and not config.no_prompt: + wallet_name = Prompt.ask("Enter wallet name", default = bittensor.defaults.wallet.name) + config.wallet.name = str(wallet_name) + + if not config.is_set('wallet.hotkey') and not config.no_prompt: + hotkey = Prompt.ask("Enter hotkey name", default = bittensor.defaults.wallet.hotkey) + config.wallet.hotkey = str(hotkey) + + @classmethod + def add_args( cls, parser: argparse.ArgumentParser ): + senate_register_parser = parser.add_parser( + 'senate_register', + help='''Register as a senate member to participate in proposals''' + ) + senate_register_parser.add_argument( + '--no_version_checking', + action='store_true', + help='''Set false to stop cli version checking''', + default = False + ) + senate_register_parser.add_argument( + '--no_prompt', + dest='no_prompt', + action='store_true', + help='''Set true to avoid prompting the user.''', + default=False, + ) + bittensor.wallet.add_args( senate_register_parser ) + bittensor.subtensor.add_args( senate_register_parser ) \ No newline at end of file diff --git a/bittensor/_subtensor/extrinsics/senate.py b/bittensor/_subtensor/extrinsics/senate.py new file mode 100644 index 0000000000..939d64ee71 --- /dev/null +++ b/bittensor/_subtensor/extrinsics/senate.py @@ -0,0 +1,90 @@ +# The MIT License (MIT) +# Copyright © 2021 Yuma Rao +# Copyright © 2023 Opentensor Foundation + +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the “Software”), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of +# the Software. + +# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +# Imports +import bittensor + +import time +from rich.prompt import Confirm +from ..errors import * + +def register_senate_extrinsic ( + subtensor: 'bittensor.Subtensor', + wallet: 'bittensor.Wallet', + wait_for_inclusion: bool = False, + wait_for_finalization: bool = True, + prompt: bool = False +) -> bool: + r""" Registers the wallet to chain for senate voting. + Args: + wallet (bittensor.wallet): + bittensor wallet object. + wait_for_inclusion (bool): + If set, waits for the extrinsic to enter a block before returning true, + or returns false if the extrinsic fails to enter the block within the timeout. + wait_for_finalization (bool): + If set, waits for the extrinsic to be finalized on the chain before returning true, + or returns false if the extrinsic fails to be finalized within the timeout. + prompt (bool): + If true, the call waits for confirmation from the user before proceeding. + Returns: + success (bool): + flag is true if extrinsic was finalized or included in the block. + If we did not wait for finalization / inclusion, the response is true. + """ + wallet.coldkey # unlock coldkey + wallet.hotkey # unlock hotkey + + if prompt: + # Prompt user for confirmation. + if not Confirm.ask( f"Register delegate hotkey to senate?" ): + return False + + with bittensor.__console__.status(":satellite: Registering with senate..."): + with subtensor.substrate as substrate: + # create extrinsic call + call = substrate.compose_call( + call_module='SubtensorModule', + call_function='join_senate', + call_params={} + ) + extrinsic = substrate.create_signed_extrinsic( call = call, keypair = wallet.hotkey ) + response = substrate.submit_extrinsic( extrinsic, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization ) + + # We only wait here if we expect finalization. + if not wait_for_finalization and not wait_for_inclusion: + bittensor.__console__.print(":white_heavy_check_mark: [green]Sent[/green]") + return True + + # process if registration successful + response.process_events() + if not response.is_success: + bittensor.__console__.print(":cross_mark: [red]Failed[/red]: error:{}".format(response.error_message)) + time.sleep(0.5) + + # Successful registration, final check for membership + else: + senate_members = subtensor.query_module("senateMembers", "Members").serialize() + is_registered = senate_members.count(wallet.hotkey.ss58_address) + + if is_registered: + bittensor.__console__.print(":white_heavy_check_mark: [green]Registered[/green]") + return True + else: + # neuron not found, try again + bittensor.__console__.print(":cross_mark: [red]Unknown error. Senate membership not found.[/red]") diff --git a/bittensor/_subtensor/subtensor_impl.py b/bittensor/_subtensor/subtensor_impl.py index 82bada0a16..fc9e2f6ede 100644 --- a/bittensor/_subtensor/subtensor_impl.py +++ b/bittensor/_subtensor/subtensor_impl.py @@ -37,6 +37,7 @@ from .extrinsics.set_weights import set_weights_extrinsic from .extrinsics.prometheus import prometheus_extrinsic from .extrinsics.delegation import delegate_extrinsic, nominate_extrinsic,undelegate_extrinsic +from .extrinsics.senate import register_senate_extrinsic # Logging from loguru import logger @@ -346,8 +347,6 @@ def unstake_multiple ( """ Removes stake from each hotkey_ss58 in the list, using each amount, to a common coldkey. """ return unstake_multiple_extrinsic( self, wallet, hotkey_ss58s, amounts, wait_for_inclusion, wait_for_finalization, prompt) - - def unstake ( self, wallet: 'bittensor.wallet', @@ -360,6 +359,18 @@ def unstake ( """ Removes stake into the wallet coldkey from the specified hotkey uid.""" return unstake_extrinsic( self, wallet, hotkey_ss58, amount, wait_for_inclusion, wait_for_finalization, prompt ) + ################ + #### Senate #### + ################ + + def register_senate( + self, + wallet: 'bittensor.wallet', + wait_for_inclusion:bool = True, + wait_for_finalization:bool = False, + prompt: bool = False, + ) -> bool: + return register_senate_extrinsic( self, wallet, wait_for_inclusion, wait_for_finalization, prompt ) ######################## #### Standard Calls #### From af5151218517f913566c135ec348bdd66cadb0c2 Mon Sep 17 00:00:00 2001 From: Rubberbandits Date: Tue, 20 Jun 2023 15:30:51 -0700 Subject: [PATCH 09/22] Add helper function wallet.is_senate_member --- bittensor/_wallet/wallet_impl.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/bittensor/_wallet/wallet_impl.py b/bittensor/_wallet/wallet_impl.py index dd82d676fa..af91191065 100644 --- a/bittensor/_wallet/wallet_impl.py +++ b/bittensor/_wallet/wallet_impl.py @@ -175,6 +175,21 @@ def is_registered( self, subtensor: Optional['bittensor.Subtensor'] = None, netu return subtensor.is_hotkey_registered_any( self.hotkey.ss58_address ) else: return subtensor.is_hotkey_registered_on_subnet( self.hotkey.ss58_address, netuid = netuid ) + + def is_senate_member( self, subtensor: Optional['bittensor.Subtensor'] = None ) -> bool: + """ Returns true if this wallet is registered as a senate member. + Args: + subtensor( Optional['bittensor.Subtensor'] ): + Bittensor subtensor connection. Overrides with defaults if None. + Determines which network we check for senate membership. + Return: + is_registered (bool): + Is the wallet apart of the senate. + """ + if subtensor == None: subtensor = bittensor.subtensor(self.config) + + # default to finney + return subtensor.is_senate_member( self.hotkey.ss58_address ) def get_neuron ( self, netuid: int, subtensor: Optional['bittensor.Subtensor'] = None ) -> Optional['bittensor.NeuronInfo'] : From ad138c7d11a5287a3f02f0264a649b6069153b75 Mon Sep 17 00:00:00 2001 From: Rubberbandits Date: Tue, 20 Jun 2023 15:31:23 -0700 Subject: [PATCH 10/22] Add subtensor *_senate extrinsics, is_senate_member, get_vote_data impl --- bittensor/_subtensor/subtensor_impl.py | 35 ++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/bittensor/_subtensor/subtensor_impl.py b/bittensor/_subtensor/subtensor_impl.py index fc9e2f6ede..577b85eb79 100644 --- a/bittensor/_subtensor/subtensor_impl.py +++ b/bittensor/_subtensor/subtensor_impl.py @@ -371,6 +371,41 @@ def register_senate( prompt: bool = False, ) -> bool: return register_senate_extrinsic( self, wallet, wait_for_inclusion, wait_for_finalization, prompt ) + + def leave_senate( + self, + wallet: 'bittensor.wallet', + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + prompt: bool = False, + ) -> bool: + return leave_senate_extrinsic( self, wallet, wait_for_inclusion, wait_for_finalization, prompt ) + + def vote_senate( + self, + wallet: 'bittensor.wallet', + proposal_hash: str, + proposal_idx: int, + vote: bool, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + prompt: bool = False, + ) -> bool: + return vote_senate_extrinsic( self, wallet, proposal_hash, proposal_idx, vote, wait_for_inclusion, wait_for_finalization, prompt ) + + def is_senate_member( + self, + hotkey_ss58: str + ) -> bool: + senate_members = self.query_module("Senate", "Members").serialize() + return senate_members.count( hotkey_ss58 ) > 0 + + def get_vote_data( + self, + proposal_hash: str + ) -> Optional[dict]: + vote_data = self.query_module("Triumvirate", "Voting", None, [proposal_hash]) + return vote_data.serialize() if vote_data != None else None ######################## #### Standard Calls #### From babc6f9098f8e9e37e636d819cc2c5115dcba24c Mon Sep 17 00:00:00 2001 From: Rubberbandits Date: Tue, 20 Jun 2023 15:32:30 -0700 Subject: [PATCH 11/22] Use helper function to check senate membership --- bittensor/_subtensor/extrinsics/senate.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bittensor/_subtensor/extrinsics/senate.py b/bittensor/_subtensor/extrinsics/senate.py index 939d64ee71..fe978b46da 100644 --- a/bittensor/_subtensor/extrinsics/senate.py +++ b/bittensor/_subtensor/extrinsics/senate.py @@ -79,8 +79,7 @@ def register_senate_extrinsic ( # Successful registration, final check for membership else: - senate_members = subtensor.query_module("senateMembers", "Members").serialize() - is_registered = senate_members.count(wallet.hotkey.ss58_address) + is_registered = wallet.is_senate_member(subtensor) if is_registered: bittensor.__console__.print(":white_heavy_check_mark: [green]Registered[/green]") From decd7c59b0aaa7598493f71bcab0bd7441d92114 Mon Sep 17 00:00:00 2001 From: Rubberbandits Date: Tue, 20 Jun 2023 15:33:19 -0700 Subject: [PATCH 12/22] Add command to view senate members --- bittensor/_cli/commands/senate.py | 58 ++++++++++++++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/bittensor/_cli/commands/senate.py b/bittensor/_cli/commands/senate.py index 08e06a18cf..e86a14126c 100644 --- a/bittensor/_cli/commands/senate.py +++ b/bittensor/_cli/commands/senate.py @@ -18,11 +18,67 @@ import sys import argparse import bittensor -from rich.prompt import Prompt +from rich.prompt import Prompt, Confirm from rich.table import Table from typing import List, Union, Optional, Dict, Tuple console = bittensor.__console__ +class SenateCommand: + + @staticmethod + def run( cli ): + r""" View Bittensor's governance protocol proposals + """ + config = cli.config.copy() + subtensor: bittensor.Subtensor = bittensor.subtensor( config = config ) + + console.print(":satellite: Syncing with chain: [white]{}[/white] ...".format(cli.config.subtensor.network)) + + senate_members = subtensor.query_module("Senate", "Members").serialize() + + table = Table(show_footer=False) + table.title = ( + "[white]Senate" + ) + table.add_column("[overline white]ADDRESS", footer_style = "overline white", style='yellow', no_wrap=True) + table.show_footer = True + + for ss58_address in senate_members: + table.add_row( + ss58_address + ) + + table.box = None + table.pad_edge = False + table.width = None + console.print(table) + + @classmethod + def check_config( cls, config: 'bittensor.Config' ): + None + + @classmethod + def add_args( cls, parser: argparse.ArgumentParser ): + senate_parser = parser.add_parser( + 'senate', + help='''View senate and it's members''' + ) + senate_parser.add_argument( + '--no_version_checking', + action='store_true', + help='''Set false to stop cli version checking''', + default = False + ) + senate_parser.add_argument( + '--no_prompt', + dest='no_prompt', + action='store_true', + help='''Set true to avoid prompting the user.''', + default=False, + ) + bittensor.wallet.add_args( senate_parser ) + bittensor.subtensor.add_args( senate_parser ) + class ProposalsCommand: @staticmethod From 9f28a2b65787f54e5146950773d3d669db905cd2 Mon Sep 17 00:00:00 2001 From: Rubberbandits Date: Tue, 20 Jun 2023 15:33:58 -0700 Subject: [PATCH 13/22] Use get_vote_data helper and refactor call data formatting for recursion --- bittensor/_cli/commands/senate.py | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/bittensor/_cli/commands/senate.py b/bittensor/_cli/commands/senate.py index e86a14126c..bf4bfd6c31 100644 --- a/bittensor/_cli/commands/senate.py +++ b/bittensor/_cli/commands/senate.py @@ -96,12 +96,12 @@ def run( cli ): for hash in proposal_hashes: proposals[hash] = [ subtensor.query_module("Triumvirate", "ProposalOf", None, [hash]), - subtensor.query_module("Triumvirate", "Voting", None, [hash]) + subtensor.get_vote_data( hash ) ] table = Table(show_footer=False) table.title = ( - "[white]Proposals:" + "[white]Proposals" ) table.add_column("[overline white]HASH", footer_style = "overline white", style='yellow', no_wrap=True) table.add_column("[overline white]THRESHOLD", footer_style = "overline white", style='white') @@ -111,13 +111,25 @@ def run( cli ): table.add_column("[overline white]CALLDATA", footer_style = "overline white", style='white') table.show_footer = True - for hash in proposals: - call_data = proposals[hash][0].serialize() - vote_data = proposals[hash][1].serialize() - + def format_call_data(call_data: List) -> str: human_call_data = list() + for arg in call_data["call_args"]: - human_call_data.append("{}: {}".format(arg["name"], str(arg["value"]))) + arg_value = arg["value"] + + # If this argument is a nested call + func_args = format_call_data({ + "call_function": arg_value["call_function"], + "call_args": arg_value["call_args"] + }) if isinstance(arg_value, dict) and "call_function" in arg_value else str(arg_value) + + human_call_data.append("{}: {}".format(arg["name"], func_args)) + + return "{}({})".format(call_data["call_function"], ", ".join(human_call_data)) + + for hash in proposals: + call_data = proposals[hash][0].serialize() + vote_data = proposals[hash][1] table.add_row( hash, @@ -125,7 +137,7 @@ def run( cli ): str(len(vote_data["ayes"])), str(len(vote_data["nays"])), str(vote_data["end"]), - "{}({})".format(call_data["call_function"], ", ".join(human_call_data)) + format_call_data(call_data) ) table.box = None From b4607d8547ee1230a447e131cf5b823c50134754 Mon Sep 17 00:00:00 2001 From: Rubberbandits Date: Tue, 20 Jun 2023 15:34:48 -0700 Subject: [PATCH 14/22] Add senate_vote and senate_leave cmd classes --- bittensor/_cli/commands/senate.py | 220 +++++++++++++++++++++++++++++- 1 file changed, 219 insertions(+), 1 deletion(-) diff --git a/bittensor/_cli/commands/senate.py b/bittensor/_cli/commands/senate.py index bf4bfd6c31..3559a651cf 100644 --- a/bittensor/_cli/commands/senate.py +++ b/bittensor/_cli/commands/senate.py @@ -171,6 +171,88 @@ def add_args( cls, parser: argparse.ArgumentParser ): bittensor.wallet.add_args( proposals_parser ) bittensor.subtensor.add_args( proposals_parser ) +class ShowVotesCommand: + + @staticmethod + def run( cli ): + r""" View Bittensor's governance protocol proposals active votes + """ + config = cli.config.copy() + subtensor: bittensor.Subtensor = bittensor.subtensor( config = config ) + + console.print(":satellite: Syncing with chain: [white]{}[/white] ...".format(cli.config.subtensor.network)) + + proposal_hash = cli.config.proposal_hash + if len(proposal_hash) == 0: + console.print('Aborting: Proposal hash not specified. View all proposals with the "proposals" command.') + return + + proposal_vote_data = subtensor.get_vote_data( proposal_hash ) + if proposal_vote_data == None: + console.print(":cross_mark: [red]Failed[/red]: Proposal not found.") + return + + table = Table(show_footer=False) + table.title = ( + "[white]Votes for Proposal {}".format(proposal_hash) + ) + table.add_column("[overline white]ADDRESS", footer_style = "overline white", style='yellow', no_wrap=True) + table.add_column("[overline white]VOTE", footer_style = "overline white", style='white') + table.show_footer = True + + for address in proposal_vote_data["ayes"]: + table.add_row( + address, + "Aye" + ) + + for address in proposal_vote_data["nays"]: + table.add_row( + address, + "Nay" + ) + + table.box = None + table.pad_edge = False + table.min_width = 64 + console.print(table) + + @classmethod + def check_config( cls, config: 'bittensor.Config' ): + if config.proposal_hash == "" and not config.no_prompt: + proposal_hash = Prompt.ask("Enter proposal hash") + config.proposal_hash = str(proposal_hash) + + @classmethod + def add_args( cls, parser: argparse.ArgumentParser ): + show_votes_parser = parser.add_parser( + 'proposal_votes', + help='''View an active proposal's votes by address.''' + ) + show_votes_parser.add_argument( + '--no_version_checking', + action='store_true', + help='''Set false to stop cli version checking''', + default = False + ) + show_votes_parser.add_argument( + '--no_prompt', + dest='no_prompt', + action='store_true', + help='''Set true to avoid prompting the user.''', + default=False, + ) + show_votes_parser.add_argument( + '--proposal', + dest='proposal_hash', + type=str, + nargs='?', + help='''Set the proposal to show votes for.''', + default="" + ) + bittensor.wallet.add_args( show_votes_parser ) + bittensor.subtensor.add_args( show_votes_parser ) + class SenateRegisterCommand: @staticmethod @@ -225,4 +307,140 @@ def add_args( cls, parser: argparse.ArgumentParser ): default=False, ) bittensor.wallet.add_args( senate_register_parser ) - bittensor.subtensor.add_args( senate_register_parser ) \ No newline at end of file + bittensor.subtensor.add_args( senate_register_parser ) + +class SenateLeaveCommand: + + @staticmethod + def run( cli ): + r""" Discard membership in Bittensor's governance protocol proposals + """ + config = cli.config.copy() + wallet = bittensor.wallet( config = cli.config ) + subtensor: bittensor.Subtensor = bittensor.subtensor( config = config ) + + # Unlock the wallet. + wallet.hotkey + wallet.coldkey + + if not wallet.is_senate_member(subtensor): + console.print('Aborting: Hotkey {} isn\'t a senate member.'.format(wallet.hotkey.ss58_address)) + return + + subtensor.leave_senate( + wallet = wallet, + prompt = not cli.config.no_prompt + ) + + @classmethod + def check_config( cls, config: 'bittensor.Config' ): + if not config.is_set('wallet.name') and not config.no_prompt: + wallet_name = Prompt.ask("Enter wallet name", default = bittensor.defaults.wallet.name) + config.wallet.name = str(wallet_name) + + if not config.is_set('wallet.hotkey') and not config.no_prompt: + hotkey = Prompt.ask("Enter hotkey name", default = bittensor.defaults.wallet.hotkey) + config.wallet.hotkey = str(hotkey) + + @classmethod + def add_args( cls, parser: argparse.ArgumentParser ): + senate_leave_parser = parser.add_parser( + 'senate_leave', + help='''Discard senate membership in the governance protocol''' + ) + senate_leave_parser.add_argument( + '--no_version_checking', + action='store_true', + help='''Set false to stop cli version checking''', + default = False + ) + senate_leave_parser.add_argument( + '--no_prompt', + dest='no_prompt', + action='store_true', + help='''Set true to avoid prompting the user.''', + default=False, + ) + bittensor.wallet.add_args( senate_leave_parser ) + bittensor.subtensor.add_args( senate_leave_parser ) + +class VoteCommand: + + @staticmethod + def run( cli ): + r""" Vote in Bittensor's governance protocol proposals + """ + config = cli.config.copy() + wallet = bittensor.wallet( config = cli.config ) + subtensor: bittensor.Subtensor = bittensor.subtensor( config = config ) + + proposal_hash = cli.config.proposal_hash + if len(proposal_hash) == 0: + console.print('Aborting: Proposal hash not specified. View all proposals with the "proposals" command.') + return + + if not wallet.is_senate_member(subtensor): + console.print('Aborting: Hotkey {} isn\'t a senate member.'.format(wallet.hotkey.ss58_address)) + return + + # Unlock the wallet. + wallet.hotkey + wallet.coldkey + + vote_data = subtensor.get_vote_data( proposal_hash ) + if vote_data == None: + console.print(":cross_mark: [red]Failed[/red]: Proposal not found.") + return + + vote = Confirm.ask("Desired vote for proposal") + subtensor.vote_senate( + wallet = wallet, + proposal_hash = proposal_hash, + proposal_idx = vote_data["index"], + vote = vote, + prompt = not cli.config.no_prompt + ) + + @classmethod + def check_config( cls, config: 'bittensor.Config' ): + if not config.is_set('wallet.name') and not config.no_prompt: + wallet_name = Prompt.ask("Enter wallet name", default = bittensor.defaults.wallet.name) + config.wallet.name = str(wallet_name) + + if not config.is_set('wallet.hotkey') and not config.no_prompt: + hotkey = Prompt.ask("Enter hotkey name", default = bittensor.defaults.wallet.hotkey) + config.wallet.hotkey = str(hotkey) + + if config.proposal_hash == "" and not config.no_prompt: + proposal_hash = Prompt.ask("Enter proposal hash") + config.proposal_hash = str(proposal_hash) + + @classmethod + def add_args( cls, parser: argparse.ArgumentParser ): + vote_parser = parser.add_parser( + 'senate_vote', + help='''Vote on an active proposal by hash.''' + ) + vote_parser.add_argument( + '--no_version_checking', + action='store_true', + help='''Set false to stop cli version checking''', + default = False + ) + vote_parser.add_argument( + '--no_prompt', + dest='no_prompt', + action='store_true', + help='''Set true to avoid prompting the user.''', + default=False, + ) + 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 ) From 53a72497b7902eda8c4f8124aa89130d14fa99aa Mon Sep 17 00:00:00 2001 From: Rubberbandits Date: Tue, 20 Jun 2023 15:35:25 -0700 Subject: [PATCH 15/22] Add membership check in senate_register command --- bittensor/_cli/commands/senate.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bittensor/_cli/commands/senate.py b/bittensor/_cli/commands/senate.py index 3559a651cf..3245169e4c 100644 --- a/bittensor/_cli/commands/senate.py +++ b/bittensor/_cli/commands/senate.py @@ -272,6 +272,10 @@ def run( cli ): console.print('Aborting: Hotkey {} isn\'t a delegate.'.format(wallet.hotkey.ss58_address)) return + if wallet.is_senate_member(subtensor): + console.print('Aborting: Hotkey {} is already a senate member.'.format(wallet.hotkey.ss58_address)) + return + subtensor.register_senate( wallet = wallet, prompt = not cli.config.no_prompt From 7b9251fd72b17f9ff3e9e2786d2c791e9fa1ec63 Mon Sep 17 00:00:00 2001 From: Rubberbandits Date: Tue, 20 Jun 2023 15:35:52 -0700 Subject: [PATCH 16/22] Add senate_leave, senate_vote extrinsic functions --- bittensor/_subtensor/extrinsics/senate.py | 138 ++++++++++++++++++++++ 1 file changed, 138 insertions(+) diff --git a/bittensor/_subtensor/extrinsics/senate.py b/bittensor/_subtensor/extrinsics/senate.py index fe978b46da..9a8e987788 100644 --- a/bittensor/_subtensor/extrinsics/senate.py +++ b/bittensor/_subtensor/extrinsics/senate.py @@ -87,3 +87,141 @@ def register_senate_extrinsic ( else: # neuron not found, try again bittensor.__console__.print(":cross_mark: [red]Unknown error. Senate membership not found.[/red]") + +def leave_senate_extrinsic ( + subtensor: 'bittensor.Subtensor', + wallet: 'bittensor.Wallet', + wait_for_inclusion: bool = False, + wait_for_finalization: bool = True, + prompt: bool = False +) -> bool: + r""" Removes the wallet from chain for senate voting. + Args: + wallet (bittensor.wallet): + bittensor wallet object. + wait_for_inclusion (bool): + If set, waits for the extrinsic to enter a block before returning true, + or returns false if the extrinsic fails to enter the block within the timeout. + wait_for_finalization (bool): + If set, waits for the extrinsic to be finalized on the chain before returning true, + or returns false if the extrinsic fails to be finalized within the timeout. + prompt (bool): + If true, the call waits for confirmation from the user before proceeding. + Returns: + success (bool): + flag is true if extrinsic was finalized or included in the block. + If we did not wait for finalization / inclusion, the response is true. + """ + wallet.coldkey # unlock coldkey + wallet.hotkey # unlock hotkey + + if prompt: + # Prompt user for confirmation. + if not Confirm.ask( f"Remove delegate hotkey from senate?" ): + return False + + with bittensor.__console__.status(":satellite: Leaving senate..."): + with subtensor.substrate as substrate: + # create extrinsic call + call = substrate.compose_call( + call_module='SubtensorModule', + call_function='leave_senate', + call_params={} + ) + extrinsic = substrate.create_signed_extrinsic( call = call, keypair = wallet.hotkey ) + response = substrate.submit_extrinsic( extrinsic, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization ) + + # We only wait here if we expect finalization. + if not wait_for_finalization and not wait_for_inclusion: + bittensor.__console__.print(":white_heavy_check_mark: [green]Sent[/green]") + return True + + # process if registration successful + response.process_events() + if not response.is_success: + bittensor.__console__.print(":cross_mark: [red]Failed[/red]: error:{}".format(response.error_message)) + time.sleep(0.5) + + # Successful registration, final check for membership + else: + is_registered = wallet.is_senate_member(subtensor) + + if not is_registered: + bittensor.__console__.print(":white_heavy_check_mark: [green]Left senate[/green]") + return True + else: + # neuron not found, try again + bittensor.__console__.print(":cross_mark: [red]Unknown error. Senate membership still found.[/red]") + +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""" Removes the wallet from chain for senate voting. + Args: + wallet (bittensor.wallet): + bittensor wallet object. + wait_for_inclusion (bool): + If set, waits for the extrinsic to enter a block before returning true, + or returns false if the extrinsic fails to enter the block within the timeout. + wait_for_finalization (bool): + If set, waits for the extrinsic to be finalized on the chain before returning true, + or returns false if the extrinsic fails to be finalized within the timeout. + prompt (bool): + If true, the call waits for confirmation from the user before proceeding. + Returns: + success (bool): + flag is true if extrinsic was finalized or included in the block. + If we did not wait for finalization / inclusion, the response is true. + """ + wallet.coldkey # unlock coldkey + wallet.hotkey # unlock hotkey + + if prompt: + # Prompt user for confirmation. + if not Confirm.ask( "Cast a vote of {}?".format( vote ) ): + return False + + with bittensor.__console__.status( ":satellite: Casting vote.." ): + with subtensor.substrate as substrate: + # create extrinsic call + call = substrate.compose_call( + call_module='Triumvirate', + call_function='vote', + call_params={ + "proposal": proposal_hash, + "index": proposal_idx, + "approve": vote + } + ) + extrinsic = substrate.create_signed_extrinsic( call = call, keypair = wallet.hotkey ) + response = substrate.submit_extrinsic( extrinsic, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization ) + + # We only wait here if we expect finalization. + if not wait_for_finalization and not wait_for_inclusion: + bittensor.__console__.print(":white_heavy_check_mark: [green]Sent[/green]") + return True + + # process if vote successful + response.process_events() + if not response.is_success: + bittensor.__console__.print(":cross_mark: [red]Failed[/red]: error:{}".format(response.error_message)) + time.sleep(0.5) + + # Successful vote, final check for data + else: + vote_data = subtensor.get_vote_data( proposal_hash ) + has_voted = vote_data["ayes"].count( wallet.hotkey.ss58_address ) > 0 or vote_data["nays"].count( wallet.hotkey.ss58_address ) > 0 + + if has_voted: + bittensor.__console__.print(":white_heavy_check_mark: [green]Vote cast.[/green]") + return True + else: + # hotkey not found in ayes/nays + bittensor.__console__.print(":cross_mark: [red]Unknown error. Couldn't find vote.[/red]") \ No newline at end of file From ba15fd515e66aafa8aefeff5d71379dfb222e58b Mon Sep 17 00:00:00 2001 From: Rubberbandits Date: Tue, 20 Jun 2023 15:36:24 -0700 Subject: [PATCH 17/22] Import senate_leave, senate_vote extrinsic functions in subtensor_impl --- bittensor/_subtensor/subtensor_impl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor/_subtensor/subtensor_impl.py b/bittensor/_subtensor/subtensor_impl.py index 577b85eb79..08dba2ec6a 100644 --- a/bittensor/_subtensor/subtensor_impl.py +++ b/bittensor/_subtensor/subtensor_impl.py @@ -37,7 +37,7 @@ from .extrinsics.set_weights import set_weights_extrinsic from .extrinsics.prometheus import prometheus_extrinsic from .extrinsics.delegation import delegate_extrinsic, nominate_extrinsic,undelegate_extrinsic -from .extrinsics.senate import register_senate_extrinsic +from .extrinsics.senate import register_senate_extrinsic, leave_senate_extrinsic, vote_senate_extrinsic # Logging from loguru import logger From 0f845e078c05eb7e89a412459ee35253e346f4d4 Mon Sep 17 00:00:00 2001 From: Rubberbandits Date: Tue, 20 Jun 2023 15:36:53 -0700 Subject: [PATCH 18/22] Add senate, proposal_votes, senate_leave, senate_vote cmds to cli --- bittensor/_cli/__init__.py | 13 +++++++++++++ bittensor/_cli/cli_impl.py | 8 ++++++++ bittensor/_cli/commands/__init__.py | 2 +- 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/bittensor/_cli/__init__.py b/bittensor/_cli/__init__.py index e4b43d3e01..6f7c58e9ac 100644 --- a/bittensor/_cli/__init__.py +++ b/bittensor/_cli/__init__.py @@ -84,8 +84,13 @@ def __create_parser__() -> 'argparse.ArgumentParser': ListDelegatesCommand.add_args( cmd_parsers ) RegenColdkeypubCommand.add_args( cmd_parsers ) RecycleRegisterCommand.add_args( cmd_parsers ) + SenateCommand.add_args( cmd_parsers ) ProposalsCommand.add_args( cmd_parsers ) + ShowVotesCommand.add_args( cmd_parsers ) SenateRegisterCommand.add_args( cmd_parsers ) + SenateLeaveCommand.add_args( cmd_parsers ) + VoteCommand.add_args( cmd_parsers ) + return parser @@ -158,10 +163,18 @@ def check_config (config: 'bittensor.Config'): MyDelegatesCommand.check_config( config ) elif config.command == "recycle_register": RecycleRegisterCommand.check_config( config ) + elif config.command == "senate": + SenateCommand.check_config( config ) elif config.command == "proposals": ProposalsCommand.check_config( config ) + elif config.command == "proposal_votes": + ShowVotesCommand.check_config( config ) elif config.command == "senate_register": SenateRegisterCommand.check_config( config ) + elif config.command == "senate_leave": + SenateLeaveCommand.check_config( config ) + elif config.command == "senate_vote": + VoteCommand.check_config( config ) else: console.print(":cross_mark:[red]Unknown command: {}[/red]".format(config.command)) sys.exit() diff --git a/bittensor/_cli/cli_impl.py b/bittensor/_cli/cli_impl.py index ddc7f9a40a..5e1cfb3d84 100644 --- a/bittensor/_cli/cli_impl.py +++ b/bittensor/_cli/cli_impl.py @@ -82,8 +82,16 @@ def run ( self ): ListSubnetsCommand.run( self ) elif self.config.command == 'recycle_register': RecycleRegisterCommand.run( self ) + elif self.config.command == "senate": + SenateCommand.run( self ) elif self.config.command == "proposals": ProposalsCommand.run( self ) + elif self.config.command == "proposal_votes": + ShowVotesCommand.run( self ) elif self.config.command == "senate_register": SenateRegisterCommand.run( self ) + elif self.config.command == "senate_leave": + SenateLeaveCommand.run( self ) + elif self.config.command == "senate_vote": + VoteCommand.run( self ) diff --git a/bittensor/_cli/commands/__init__.py b/bittensor/_cli/commands/__init__.py index 5f397d6d99..0a53ed3cc8 100644 --- a/bittensor/_cli/commands/__init__.py +++ b/bittensor/_cli/commands/__init__.py @@ -9,4 +9,4 @@ from .metagraph import MetagraphCommand from .list import ListCommand from .misc import UpdateCommand, ListSubnetsCommand -from .senate import ProposalsCommand, SenateRegisterCommand +from .senate import SenateCommand, ProposalsCommand, ShowVotesCommand, SenateRegisterCommand, SenateLeaveCommand, VoteCommand From 8460c0d548c795226ef7941bc469775eb053a3fb Mon Sep 17 00:00:00 2001 From: Rubberbandits Date: Tue, 20 Jun 2023 19:19:17 -0700 Subject: [PATCH 19/22] Move closure helper funcs to main scope, add display_votes helper --- bittensor/_cli/commands/senate.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/bittensor/_cli/commands/senate.py b/bittensor/_cli/commands/senate.py index 3245169e4c..d80d395f2b 100644 --- a/bittensor/_cli/commands/senate.py +++ b/bittensor/_cli/commands/senate.py @@ -79,6 +79,34 @@ def add_args( cls, parser: argparse.ArgumentParser ): bittensor.wallet.add_args( senate_parser ) bittensor.subtensor.add_args( senate_parser ) +from .utils import get_delegates_details, DelegatesDetails +def format_call_data(call_data: List) -> str: + human_call_data = list() + + for arg in call_data["call_args"]: + arg_value = arg["value"] + + # If this argument is a nested call + func_args = format_call_data({ + "call_function": arg_value["call_function"], + "call_args": arg_value["call_args"] + }) if isinstance(arg_value, dict) and "call_function" in arg_value else str(arg_value) + + human_call_data.append("{}: {}".format(arg["name"], func_args)) + + return "{}({})".format(call_data["call_function"], ", ".join(human_call_data)) + +def display_votes(vote_data, delegate_info) -> str: + vote_list = list() + + for address in vote_data["ayes"]: + vote_list.append("{}: {}".format(delegate_info[address].name if address in delegate_info else address, "[bold green]Aye[/bold green]")) + + for address in vote_data["nays"]: + vote_list.append("{}: {}".format(delegate_info[address].name if address in delegate_info else address, "[bold red]Nay[/bold red]")) + + return "\n".join(vote_list) + class ProposalsCommand: @staticmethod From 2795834202e01fc666cfc04209d0cf703864e305 Mon Sep 17 00:00:00 2001 From: Rubberbandits Date: Tue, 20 Jun 2023 19:20:00 -0700 Subject: [PATCH 20/22] Add senate size and active proposals metric, vote overview and nice names --- bittensor/_cli/commands/senate.py | 23 ++++++----------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/bittensor/_cli/commands/senate.py b/bittensor/_cli/commands/senate.py index d80d395f2b..621b9d9e62 100644 --- a/bittensor/_cli/commands/senate.py +++ b/bittensor/_cli/commands/senate.py @@ -118,6 +118,7 @@ def run( cli ): console.print(":satellite: Syncing with chain: [white]{}[/white] ...".format(cli.config.subtensor.network)) + senate_members = subtensor.query_module("SenateMembers", "Members").serialize() proposals = dict() proposal_hashes = subtensor.query_module("Triumvirate", "Proposals") @@ -127,34 +128,21 @@ def run( cli ): subtensor.get_vote_data( hash ) ] + registered_delegate_info: Optional[Dict[str, DelegatesDetails]] = get_delegates_details(url = bittensor.__delegates_details_url__) + table = Table(show_footer=False) table.title = ( - "[white]Proposals" + "[white]Proposals\t\tActive Proposals: {}\t\tSenate Size: {}".format(len(proposals), len(senate_members)) ) table.add_column("[overline white]HASH", footer_style = "overline white", style='yellow', no_wrap=True) table.add_column("[overline white]THRESHOLD", footer_style = "overline white", style='white') table.add_column("[overline white]AYES", footer_style = "overline white", style='green') table.add_column("[overline white]NAYS", footer_style = "overline white", style='red') + table.add_column("[overline white]VOTES", footer_style = "overline white", style='rgb(50,163,219)') table.add_column("[overline white]END", footer_style = "overline white", style='blue') table.add_column("[overline white]CALLDATA", footer_style = "overline white", style='white') table.show_footer = True - def format_call_data(call_data: List) -> str: - human_call_data = list() - - for arg in call_data["call_args"]: - arg_value = arg["value"] - - # If this argument is a nested call - func_args = format_call_data({ - "call_function": arg_value["call_function"], - "call_args": arg_value["call_args"] - }) if isinstance(arg_value, dict) and "call_function" in arg_value else str(arg_value) - - human_call_data.append("{}: {}".format(arg["name"], func_args)) - - return "{}({})".format(call_data["call_function"], ", ".join(human_call_data)) - for hash in proposals: call_data = proposals[hash][0].serialize() vote_data = proposals[hash][1] @@ -164,6 +152,7 @@ def format_call_data(call_data: List) -> str: str(vote_data["threshold"]), str(len(vote_data["ayes"])), str(len(vote_data["nays"])), + display_votes(vote_data, registered_delegate_info), str(vote_data["end"]), format_call_data(call_data) ) From 6a3ddde346268218a1578dc9eed8c22b4f84ea7e Mon Sep 17 00:00:00 2001 From: Rubberbandits Date: Tue, 20 Jun 2023 20:23:55 -0700 Subject: [PATCH 21/22] Add delegate nice-name support to proposal_votes --- bittensor/_cli/commands/senate.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/bittensor/_cli/commands/senate.py b/bittensor/_cli/commands/senate.py index 621b9d9e62..b2e84f991b 100644 --- a/bittensor/_cli/commands/senate.py +++ b/bittensor/_cli/commands/senate.py @@ -208,6 +208,8 @@ def run( cli ): if proposal_vote_data == None: console.print(":cross_mark: [red]Failed[/red]: Proposal not found.") return + + registered_delegate_info: Optional[Dict[str, DelegatesDetails]] = get_delegates_details(url = bittensor.__delegates_details_url__) table = Table(show_footer=False) table.title = ( @@ -217,16 +219,12 @@ def run( cli ): table.add_column("[overline white]VOTE", footer_style = "overline white", style='white') table.show_footer = True - for address in proposal_vote_data["ayes"]: - table.add_row( - address, - "Aye" - ) - - for address in proposal_vote_data["nays"]: + votes = display_votes(proposal_vote_data, registered_delegate_info).split("\n") + for vote in votes: + split_vote_data = vote.split(": ") # Nasty, but will work. table.add_row( - address, - "Nay" + split_vote_data[0], + split_vote_data[1] ) table.box = None From 23cc6576c998b5fb7dbd961fcf2aacafe51008f8 Mon Sep 17 00:00:00 2001 From: Rubberbandits Date: Mon, 26 Jun 2023 10:45:50 -0700 Subject: [PATCH 22/22] Use coldkey for senate actions instead of hotkey --- bittensor/_subtensor/extrinsics/senate.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/bittensor/_subtensor/extrinsics/senate.py b/bittensor/_subtensor/extrinsics/senate.py index 9a8e987788..66e29cb14c 100644 --- a/bittensor/_subtensor/extrinsics/senate.py +++ b/bittensor/_subtensor/extrinsics/senate.py @@ -61,9 +61,11 @@ def register_senate_extrinsic ( call = substrate.compose_call( call_module='SubtensorModule', call_function='join_senate', - call_params={} + call_params={ + "hotkey": wallet.hotkey.ss58_address + } ) - extrinsic = substrate.create_signed_extrinsic( call = call, keypair = wallet.hotkey ) + extrinsic = substrate.create_signed_extrinsic( call = call, keypair = wallet.coldkey ) response = substrate.submit_extrinsic( extrinsic, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization ) # We only wait here if we expect finalization. @@ -126,9 +128,11 @@ def leave_senate_extrinsic ( call = substrate.compose_call( call_module='SubtensorModule', call_function='leave_senate', - call_params={} + call_params={ + "hotkey": wallet.hotkey.ss58_address + } ) - extrinsic = substrate.create_signed_extrinsic( call = call, keypair = wallet.hotkey ) + extrinsic = substrate.create_signed_extrinsic( call = call, keypair = wallet.coldkey ) response = substrate.submit_extrinsic( extrinsic, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization ) # We only wait here if we expect finalization. @@ -192,15 +196,16 @@ def vote_senate_extrinsic ( with subtensor.substrate as substrate: # create extrinsic call call = substrate.compose_call( - call_module='Triumvirate', + call_module='SubtensorModule', call_function='vote', call_params={ + "hotkey": wallet.hotkey.ss58_address, "proposal": proposal_hash, "index": proposal_idx, "approve": vote } ) - extrinsic = substrate.create_signed_extrinsic( call = call, keypair = wallet.hotkey ) + extrinsic = substrate.create_signed_extrinsic( call = call, keypair = wallet.coldkey ) response = substrate.submit_extrinsic( extrinsic, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization ) # We only wait here if we expect finalization.