From c58b873ebf444551f1bc62d88a71b3a666ef0fbe Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Thu, 15 Aug 2024 14:18:49 -0700 Subject: [PATCH 1/6] Modifies conftest to properly wait or exit building the chain --- tests/e2e_tests/conftest.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/tests/e2e_tests/conftest.py b/tests/e2e_tests/conftest.py index 4575ff298d..9db51c1007 100644 --- a/tests/e2e_tests/conftest.py +++ b/tests/e2e_tests/conftest.py @@ -13,7 +13,6 @@ clone_or_update_templates, install_templates, uninstall_templates, - template_path, ) logging.basicConfig(level=logging.INFO) @@ -54,10 +53,9 @@ def local_chain(request): def wait_for_node_start(process, pattern): for line in process.stdout: print(line.strip()) - # 10 min as timeout - if int(time.time()) - timestamp > 10 * 60: - print("Subtensor not started in time") - break + # 20 min as timeout + if int(time.time()) - timestamp > 20 * 60: + pytest.fail("Subtensor not started in time") if pattern.search(line): print("Node started!") break @@ -82,4 +80,4 @@ def wait_for_node_start(process, pattern): # uninstall templates logging.info("uninstalling neuron templates") - uninstall_templates(template_path) + uninstall_templates(templates_dir) From f1477d5e7d4380114970bd7ca9f4fc7fc4e49ab1 Mon Sep 17 00:00:00 2001 From: Gus Date: Fri, 16 Aug 2024 16:50:40 -0400 Subject: [PATCH 2/6] feat: return error message instead raising exception --- bittensor/commands/delegates.py | 8 +++++- bittensor/commands/identity.py | 9 ++++++- bittensor/commands/senate.py | 26 ++++++++++++++++--- bittensor/extrinsics/delegation.py | 38 ++++++++++++++++++++++++---- bittensor/extrinsics/network.py | 8 +++++- bittensor/extrinsics/registration.py | 24 +++++++++++++++--- bittensor/extrinsics/root.py | 16 ++++++++++-- bittensor/extrinsics/senate.py | 18 +++++++++++-- bittensor/extrinsics/staking.py | 8 +++++- bittensor/extrinsics/transfer.py | 11 ++++++-- bittensor/extrinsics/unstaking.py | 24 +++++++++++++++--- bittensor/keyfile.py | 6 ++++- tests/unit_tests/test_keyfile.py | 17 +++++++++++++ 13 files changed, 188 insertions(+), 25 deletions(-) diff --git a/bittensor/commands/delegates.py b/bittensor/commands/delegates.py index 4d03b289e4..cfba3526d2 100644 --- a/bittensor/commands/delegates.py +++ b/bittensor/commands/delegates.py @@ -752,7 +752,13 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): # Unlock the wallet. wallet.hotkey - wallet.coldkey + try: + wallet.coldkey + except bittensor.KeyFileError: + bittensor.__console__.print( + ":cross_mark: [red]Keyfile is corrupt, non-writable, non-readable or the password used to decrypt is invalid[/red]:[bold white]\n [/bold white]" + ) + return # Check if the hotkey is already a delegate. if subtensor.is_hotkey_delegate(wallet.hotkey.ss58_address): diff --git a/bittensor/commands/identity.py b/bittensor/commands/identity.py index 15232c4440..4f74548495 100644 --- a/bittensor/commands/identity.py +++ b/bittensor/commands/identity.py @@ -115,7 +115,14 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): console.print(":cross_mark: Aborted!") exit(0) - wallet.coldkey # unlock coldkey + try: + wallet.coldkey # unlock coldkey + except bittensor.KeyFileError: + bittensor.__console__.print( + ":cross_mark: [red]Keyfile is corrupt, non-writable, non-readable or the password used to decrypt is invalid[/red]:[bold white]\n [/bold white]" + ) + return + with console.status(":satellite: [bold green]Updating identity on-chain..."): try: subtensor.update_identity( diff --git a/bittensor/commands/senate.py b/bittensor/commands/senate.py index 03a73cde5b..37f2d79585 100644 --- a/bittensor/commands/senate.py +++ b/bittensor/commands/senate.py @@ -432,7 +432,13 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): # Unlock the wallet. wallet.hotkey - wallet.coldkey + try: + wallet.coldkey + except bittensor.KeyFileError: + bittensor.__console__.print( + ":cross_mark: [red]Keyfile is corrupt, non-writable, non-readable or the password used to decrypt is invalid[/red]:[bold white]\n [/bold white]" + ) + return # Check if the hotkey is a delegate. if not subtensor.is_hotkey_delegate(wallet.hotkey.ss58_address): @@ -514,7 +520,13 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.cli"): # Unlock the wallet. wallet.hotkey - wallet.coldkey + try: + wallet.coldkey + except bittensor.KeyFileError: + bittensor.__console__.print( + ":cross_mark: [red]Keyfile is corrupt, non-writable, non-readable or the password used to decrypt is invalid[/red]:[bold white]\n [/bold white]" + ) + return if not subtensor.is_senate_member(hotkey_ss58=wallet.hotkey.ss58_address): console.print( @@ -603,7 +615,15 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): # Unlock the wallet. wallet.hotkey - wallet.coldkey + try: + wallet.coldkey + except bittensor.KeyFileError: + bittensor.__console__.print( + ":cross_mark: [red]Keyfile is corrupt, non-writable, non-readable or the password used to decrypt is invalid[/red]:[bold white]\n [/bold white]" + ) + return + + vote_data = subtensor.get_vote_data(proposal_hash) vote_data = subtensor.get_vote_data(proposal_hash) if vote_data == None: diff --git a/bittensor/extrinsics/delegation.py b/bittensor/extrinsics/delegation.py index 5d31855cdb..e61a97efb4 100644 --- a/bittensor/extrinsics/delegation.py +++ b/bittensor/extrinsics/delegation.py @@ -47,9 +47,17 @@ def nominate_extrinsic( success (bool): ``True`` if the transaction was successful. """ # Unlock the coldkey. - wallet.coldkey - wallet.hotkey + try: + wallet.coldkey + + except bittensor.KeyFileError: + bittensor.__console__.print( + ":cross_mark: [red]Keyfile is corrupt, non-writable, non-readable or the password used to decrypt is invalid[/red]:[bold white]\n [/bold white]" + ) + return False + + wallet.hotkey # Check if the hotkey is already a delegate. if subtensor.is_hotkey_delegate(wallet.hotkey.ss58_address): logger.error( @@ -133,7 +141,13 @@ def delegate_extrinsic( NotDelegateError: If the hotkey is not a delegate on the chain. """ # Decrypt keys, - wallet.coldkey + try: + wallet.coldkey + except bittensor.KeyFileError: + bittensor.__console__.print( + ":cross_mark: [red]Keyfile is corrupt, non-writable, non-readable or the password used to decrypt is invalid[/red]:[bold white]\n [/bold white]" + ) + return False if not subtensor.is_hotkey_delegate(delegate_ss58): raise NotDelegateError("Hotkey: {} is not a delegate.".format(delegate_ss58)) @@ -394,7 +408,14 @@ def decrease_take_extrinsic( success (bool): ``True`` if the transaction was successful. """ # Unlock the coldkey. - wallet.coldkey + try: + wallet.coldkey + except bittensor.KeyFileError: + bittensor.__console__.print( + ":cross_mark: [red]Keyfile is corrupt, non-writable, non-readable or the password used to decrypt is invalid[/red]:[bold white]\n [/bold white]" + ) + return False + wallet.hotkey with bittensor.__console__.status( @@ -454,7 +475,14 @@ def increase_take_extrinsic( success (bool): ``True`` if the transaction was successful. """ # Unlock the coldkey. - wallet.coldkey + try: + wallet.coldkey + except bittensor.KeyFileError: + bittensor.__console__.print( + ":cross_mark: [red]Keyfile is corrupt, non-writable, non-readable or the password used to decrypt is invalid[/red]:[bold white]\n [/bold white]" + ) + return False + wallet.hotkey with bittensor.__console__.status( diff --git a/bittensor/extrinsics/network.py b/bittensor/extrinsics/network.py index 16cbc0ed26..5aecaa459a 100644 --- a/bittensor/extrinsics/network.py +++ b/bittensor/extrinsics/network.py @@ -87,7 +87,13 @@ def register_subnetwork_extrinsic( ): return False - wallet.coldkey # unlock coldkey + try: + wallet.coldkey # unlock coldkey + except bittensor.KeyFileError: + bittensor.__console__.print( + ":cross_mark: [red]Keyfile is corrupt, non-writable, non-readable or the password used to decrypt is invalid[/red]:[bold white]\n [/bold white]" + ) + return False with bittensor.__console__.status(":satellite: Registering subnet..."): with subtensor.substrate as substrate: diff --git a/bittensor/extrinsics/registration.py b/bittensor/extrinsics/registration.py index e82add8383..40bde3fc89 100644 --- a/bittensor/extrinsics/registration.py +++ b/bittensor/extrinsics/registration.py @@ -259,7 +259,13 @@ def burned_register_extrinsic( ) return False - wallet.coldkey # unlock coldkey + try: + wallet.coldkey # unlock coldkey + except bittensor.KeyFileError: + bittensor.__console__.print( + ":cross_mark: [red]Keyfile is corrupt, non-writable, non-readable or the password used to decrypt is invalid[/red]:[bold white]\n [/bold white]" + ) + return False with bittensor.__console__.status( f":satellite: Checking Account on [bold]subnet:{netuid}[/bold]..." ): @@ -394,7 +400,13 @@ def run_faucet_extrinsic( return False, "Requires torch" # Unlock coldkey - wallet.coldkey + try: + wallet.coldkey + except bittensor.KeyFileError: + bittensor.__console__.print( + ":cross_mark: [red]Keyfile is corrupt, non-writable, non-readable or the password used to decrypt is invalid[/red]:[bold white]\n [/bold white]" + ) + return False, "" # Get previous balance. old_balance = subtensor.get_balance(wallet.coldkeypub.ss58_address) @@ -497,7 +509,13 @@ def swap_hotkey_extrinsic( wait_for_finalization: bool = True, prompt: bool = False, ) -> bool: - wallet.coldkey # unlock coldkey + try: + wallet.coldkey # unlock coldkey + except bittensor.KeyFileError: + bittensor.__console__.print( + ":cross_mark: [red]Keyfile is corrupt, non-writable, non-readable or the password used to decrypt is invalid[/red]:[bold white]\n [/bold white]" + ) + return False if prompt: # Prompt user for confirmation. if not Confirm.ask( diff --git a/bittensor/extrinsics/root.py b/bittensor/extrinsics/root.py index 8a7e9e3863..c0a4fcabd1 100644 --- a/bittensor/extrinsics/root.py +++ b/bittensor/extrinsics/root.py @@ -54,7 +54,13 @@ def root_register_extrinsic( Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. """ - wallet.coldkey # unlock coldkey + try: + wallet.coldkey # unlock coldkey + except bittensor.KeyFileError: + bittensor.__console__.print( + ":cross_mark: [red]Keyfile is corrupt, non-writable, non-readable or the password used to decrypt is invalid[/red]:[bold white]\n [/bold white]" + ) + return False is_registered = subtensor.is_hotkey_registered( netuid=0, hotkey_ss58=wallet.hotkey.ss58_address @@ -131,7 +137,13 @@ def set_root_weights_extrinsic( Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. """ - wallet.coldkey # unlock coldkey + try: + wallet.coldkey # unlock coldkey + except bittensor.KeyFileError: + bittensor.__console__.print( + ":cross_mark: [red]Keyfile is corrupt, non-writable, non-readable or the password used to decrypt is invalid[/red]:[bold white]\n [/bold white]" + ) + return False # First convert types. if isinstance(netuids, list): diff --git a/bittensor/extrinsics/senate.py b/bittensor/extrinsics/senate.py index 043233996c..f586cec399 100644 --- a/bittensor/extrinsics/senate.py +++ b/bittensor/extrinsics/senate.py @@ -46,7 +46,14 @@ def register_senate_extrinsic( 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 + try: + wallet.coldkey # unlock coldkey + except bittensor.KeyFileError: + bittensor.__console__.print( + ":cross_mark: [red]Keyfile is corrupt, non-writable, non-readable or the password used to decrypt is invalid[/red]:[bold white]\n [/bold white]" + ) + return False + wallet.hotkey # unlock hotkey if prompt: @@ -121,7 +128,14 @@ def leave_senate_extrinsic( 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 + try: + wallet.coldkey # unlock coldkey + except bittensor.KeyFileError: + bittensor.__console__.print( + ":cross_mark: [red]Keyfile is corrupt, non-writable, non-readable or the password used to decrypt is invalid[/red]:[bold white]\n [/bold white]" + ) + return False + wallet.hotkey # unlock hotkey if prompt: diff --git a/bittensor/extrinsics/staking.py b/bittensor/extrinsics/staking.py index 686b7d60e5..93ac1ae2de 100644 --- a/bittensor/extrinsics/staking.py +++ b/bittensor/extrinsics/staking.py @@ -86,7 +86,13 @@ def add_stake_extrinsic( If the hotkey is not a delegate on the chain. """ # Decrypt keys, - wallet.coldkey + try: + wallet.coldkey + except bittensor.KeyFileError: + bittensor.__console__.print( + ":cross_mark: [red]Keyfile is corrupt, non-writable, non-readable or the password used to decrypt is invalid[/red]:[bold white]\n [/bold white]" + ) + return False # Default to wallet's own hotkey if the value is not passed. if hotkey_ss58 is None: diff --git a/bittensor/extrinsics/transfer.py b/bittensor/extrinsics/transfer.py index 91ef3237eb..aa340ab406 100644 --- a/bittensor/extrinsics/transfer.py +++ b/bittensor/extrinsics/transfer.py @@ -68,8 +68,15 @@ def transfer_extrinsic( # Convert bytes to hex string. dest = "0x" + dest.hex() - # Unlock wallet coldkey. - wallet.coldkey + try: + # Unlock wallet coldkey. + wallet.coldkey + + except bittensor.KeyFileError: + bittensor.__console__.print( + ":cross_mark: [red]Keyfile is corrupt, non-writable, non-readable or the password used to decrypt is invalid[/red]:[bold white]\n [/bold white]" + ) + return False # Convert to bittensor.Balance if not isinstance(amount, bittensor.Balance): diff --git a/bittensor/extrinsics/unstaking.py b/bittensor/extrinsics/unstaking.py index 57329915eb..a5de71b7d7 100644 --- a/bittensor/extrinsics/unstaking.py +++ b/bittensor/extrinsics/unstaking.py @@ -58,7 +58,13 @@ def __do_remove_stake_single( """ # Decrypt keys, - wallet.coldkey + try: + wallet.coldkey + except bittensor.KeyFileError: + bittensor.__console__.print( + ":cross_mark: [red]Keyfile is corrupt, non-writable, non-readable or the password used to decrypt is invalid[/red]:[bold white]\n [/bold white]" + ) + return False success = subtensor._do_unstake( wallet=wallet, @@ -126,7 +132,13 @@ def unstake_extrinsic( Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. """ # Decrypt keys, - wallet.coldkey + try: + wallet.coldkey + except bittensor.KeyFileError: + bittensor.__console__.print( + ":cross_mark: [red]Keyfile is corrupt, non-writable, non-readable or the password used to decrypt is invalid[/red]:[bold white]\n [/bold white]" + ) + return False if hotkey_ss58 is None: hotkey_ss58 = wallet.hotkey.ss58_address # Default to wallet's own hotkey. @@ -304,7 +316,13 @@ def unstake_multiple_extrinsic( return True # Unlock coldkey. - wallet.coldkey + try: + wallet.coldkey + except bittensor.KeyFileError: + bittensor.__console__.print( + ":cross_mark: [red]Keyfile is corrupt, non-writable, non-readable or the password used to decrypt is invalid[/red]:[bold white]\n [/bold white]" + ) + return False old_stakes = [] own_hotkeys = [] diff --git a/bittensor/keyfile.py b/bittensor/keyfile.py index f1b2ad622e..d2c75c1041 100644 --- a/bittensor/keyfile.py +++ b/bittensor/keyfile.py @@ -33,6 +33,7 @@ from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC from nacl import pwhash, secret +from nacl.exceptions import CryptoError from password_strength import PasswordPolicy from substrateinterface.utils.ss58 import ss58_encode from termcolor import colored @@ -321,7 +322,10 @@ def decrypt_keyfile_data( memlimit=pwhash.argon2i.MEMLIMIT_SENSITIVE, ) box = secret.SecretBox(key) - decrypted_keyfile_data = box.decrypt(keyfile_data[len("$NACL") :]) + try: + decrypted_keyfile_data = box.decrypt(keyfile_data[len("$NACL") :]) + except CryptoError: + raise bittensor.KeyFileError("Invalid password") # Ansible decrypt. elif keyfile_data_is_encrypted_ansible(keyfile_data): vault = Vault(password) diff --git a/tests/unit_tests/test_keyfile.py b/tests/unit_tests/test_keyfile.py index 8db105c3bd..0f3b69cacf 100644 --- a/tests/unit_tests/test_keyfile.py +++ b/tests/unit_tests/test_keyfile.py @@ -624,3 +624,20 @@ def test_get_coldkey_password_from_environment(monkeypatch): assert get_coldkey_password_from_environment(wallet) == password assert get_coldkey_password_from_environment("non_existent_wallet") is None + + +def test_keyfile_error_incorrect_password(keyfile_setup_teardown): + """ + Test case for attempting to decrypt a keyfile with an incorrect password. + """ + root_path = keyfile_setup_teardown + keyfile = bittensor.keyfile(path=os.path.join(root_path, "keyfile")) + + # Ensure the keyfile is encrypted + assert keyfile.is_encrypted() + + # Attempt to decrypt with an incorrect password + with pytest.raises(bittensor.KeyFileError) as excinfo: + keyfile.get_keypair(password="incorrect_password") + + assert "Invalid password" in str(excinfo.value) From 91428ed123cafb49b2cc8130c49781c6181af861 Mon Sep 17 00:00:00 2001 From: opendansor Date: Fri, 16 Aug 2024 14:47:50 -0700 Subject: [PATCH 3/6] Update Child Hotkey commands Adjusted the proportions handling and format consistency in the set_children command. Enhanced validation checks for hotkey addresses and netuid ranges. Updated tests to reflect these changes and ensure accurate assertion of output and child hotkey revocations. --- bittensor/commands/stake.py | 115 ++++++++++++------ bittensor/extrinsics/staking.py | 8 ++ bittensor/subtensor.py | 8 +- bittensor/utils/subtensor.py | 3 +- .../subcommands/stake/test_childkeys.py | 25 ++-- 5 files changed, 105 insertions(+), 54 deletions(-) diff --git a/bittensor/commands/stake.py b/bittensor/commands/stake.py index abcbc51ed6..969ef92091 100644 --- a/bittensor/commands/stake.py +++ b/bittensor/commands/stake.py @@ -36,6 +36,7 @@ ) from . import defaults # type: ignore from ..utils import wallet_utils +from ..utils.formatting import u64_to_float console = bittensor.__console__ @@ -574,19 +575,20 @@ def add_args(parser: argparse.ArgumentParser): class SetChildrenCommand: """ - Executes the ``set_children`` command to add children hotkeys on a specified subnet on the Bittensor network. + Executes the ``set_children`` command to add children hotkeys on a specified subnet on the Bittensor network to the caller. This command is used to delegate authority to different hotkeys, securing their position and influence on the subnet. Usage: Users can specify the amount or 'proportion' to delegate to child hotkeys (``SS58`` address), - the user needs to have sufficient authority to make this call, and the sum of proportions cannot be greater than 1. + the user needs to have sufficient authority to make this call, and the sum of proportions must equal 1, + representing 100% of the proportion allocation. The command prompts for confirmation before executing the set_children operation. Example usage:: - btcli stake set_children --children , --hotkey --netuid 1 --proportions 0.3,0.3 + btcli stake set_children --children , --netuid 1 --proportions 0.4,0.6 Note: This command is critical for users who wish to delegate children hotkeys among different neurons (hotkeys) on the network. @@ -613,45 +615,62 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): # Get values if not set. if not cli.config.is_set("netuid"): cli.config.netuid = int(Prompt.ask("Enter netuid")) + + netuid = cli.config.netuid + total_subnets = subtensor.get_total_subnets() + if total_subnets is not None and total_subnets <= netuid <= 0: + raise ValueError("Netuid is outside the current subnet range") if not cli.config.is_set("hotkey"): cli.config.hotkey = Prompt.ask("Enter parent hotkey (ss58)") + if not wallet_utils.is_valid_ss58_address(cli.config.hotkey): + console.print(f":cross_mark:[red] Invalid SS58 address: {cli.config.hotkey}[/red]") + return - # display children - GetChildrenCommand.retrieve_children( + # get children + curr_children = GetChildrenCommand.retrieve_children( subtensor=subtensor, hotkey=cli.config.hotkey, netuid=cli.config.netuid, - render_table=True, + render_table=False, ) + + if curr_children: + GetChildrenCommand.retrieve_children( + subtensor=subtensor, + hotkey=cli.config.hotkey, + netuid=cli.config.netuid, + render_table=True, + ) + raise ValueError(f"There are already children hotkeys under parent hotkey {cli.config.hotkey}. " + f"Call revoke_children command before attempting to set_children again, or call the get_children command to view them.") if not cli.config.is_set("children"): cli.config.children = Prompt.ask( - "Enter children hotkey (ss58) as comma-separated values" + "Enter child(ren) hotkeys (ss58) as comma-separated values" ) - - if not cli.config.is_set("proportions"): - cli.config.proportions = Prompt.ask( - "Enter proportions for children as comma-separated values (sum less than 1)" - ) - - # Parse from strings - netuid = cli.config.netuid - - # extract proportions and child addresses from cli input - proportions = [float(x) for x in re.split(r"[ ,]+", cli.config.proportions)] children = [str(x) for x in re.split(r"[ ,]+", cli.config.children)] - + # Validate children SS58 addresses for child in children: if not wallet_utils.is_valid_ss58_address(child): console.print(f":cross_mark:[red] Invalid SS58 address: {child}[/red]") return + + if len(children) == 1: # if only one child, then they have full proportion by default + cli.config.proportions = 1.0 + + if not cli.config.is_set("proportions"): + cli.config.proportions = Prompt.ask( + "Enter the percentage of proportion for each child as comma-separated values (total must equal 1)" + ) + # extract proportions and child addresses from cli input + proportions = [float(x) for x in re.split(r"[ ,]+", cli.config.proportions)] total_proposed = sum(proportions) - if total_proposed > 1: + if total_proposed != 1: raise ValueError( - f"Invalid proportion: The sum of all proportions cannot be greater than 1. Proposed sum of proportions is {total_proposed}." + f"Invalid proportion: The sum of all proportions must equal 1 (representing 100% of the allocation). Proposed sum of proportions is {total_proposed}." ) children_with_proportions = list(zip(proportions, children)) @@ -775,18 +794,23 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): # Get values if not set. if not cli.config.is_set("netuid"): cli.config.netuid = int(Prompt.ask("Enter netuid")) + netuid = cli.config.netuid + total_subnets = subtensor.get_total_subnets() + if total_subnets is not None and total_subnets <= netuid <= 0: + raise ValueError("Netuid is outside the current subnet range") # Get values if not set. if not cli.config.is_set("hotkey"): cli.config.hotkey = Prompt.ask("Enter parent hotkey (ss58)") - - # Parse from strings - netuid = cli.config.netuid hotkey = cli.config.hotkey + if not wallet_utils.is_valid_ss58_address(cli.config.hotkey): + console.print(f":cross_mark:[red] Invalid SS58 address: {cli.config.hotkey}[/red]") + return children = subtensor.get_children(hotkey, netuid) + hotkey_stake = subtensor.get_total_stake_for_hotkey(hotkey) - GetChildrenCommand.render_table(subtensor, hotkey, children, netuid, True) + GetChildrenCommand.render_table(subtensor, hotkey, hotkey_stake, children, netuid, True) return children @@ -800,7 +824,7 @@ def retrieve_children( Args: subtensor (bittensor.subtensor): The subtensor object used to interact with the Bittensor network. - hotkey (str): The hotkey of the tensor owner. + hotkey (str): The hotkey of the parent. netuid (int): The network unique identifier of the subtensor. render_table (bool): Flag indicating whether to render the retrieved children in a table. @@ -810,7 +834,8 @@ def retrieve_children( """ children = subtensor.get_children(hotkey, netuid) if render_table: - GetChildrenCommand.render_table(subtensor, hotkey, children, netuid, False) + hotkey_stake = subtensor.get_total_stake_for_hotkey(hotkey) + GetChildrenCommand.render_table(subtensor, hotkey, hotkey_stake, children, netuid, False) return children @staticmethod @@ -837,6 +862,7 @@ def add_args(parser: argparse.ArgumentParser): def render_table( subtensor: "bittensor.subtensor", hotkey: str, + hotkey_stake: "Balance", children: list[Tuple[int, str]], netuid: int, prompt: bool, @@ -880,12 +906,13 @@ def render_table( table.add_column("Index", style="cyan", no_wrap=True, justify="right") table.add_column("ChildHotkey", style="cyan", no_wrap=True) table.add_column("Proportion", style="cyan", no_wrap=True, justify="right") - table.add_column("Total Stake", style="cyan", no_wrap=True, justify="right") + table.add_column("Child Stake", style="cyan", no_wrap=True, justify="right") + table.add_column("Total Stake Weight", style="cyan", no_wrap=True, justify="right") if not children: - console.print(table) + # console.print(table) console.print( - f"There are currently no child hotkeys on subnet {netuid} with ParentHotKey {hotkey}." + f"There are currently no child hotkeys on subnet {netuid} with Parent HotKey {hotkey}." ) if prompt: command = f"btcli stake set_children --children --hotkey --netuid {netuid} --proportion " @@ -894,12 +921,13 @@ def render_table( ) return - console.print("ParentHotKey:", style="cyan", no_wrap=True) - console.print(hotkey) + console.print(f"Parent HotKey: {hotkey} | ", style="cyan", end="", no_wrap=True) + console.print(f"Total Parent Stake: {hotkey_stake.tao}τ") # calculate totals total_proportion = 0 total_stake = 0 + total_stake_weight = 0 children_info = [] for child in children: @@ -910,8 +938,9 @@ def render_table( ) or Balance(0) # add to totals - total_proportion += proportion - total_stake += child_stake + total_stake += child_stake.tao + + proportion = u64_to_float(proportion) children_info.append((proportion, child_hotkey, child_stake)) @@ -921,17 +950,25 @@ def render_table( # add the children info to the table for i, (proportion, hotkey, stake) in enumerate(children_info, 1): - proportion_str = Text( - str(proportion), style="red" if proportion == 0 else "" - ) + proportion_percent = proportion * 100 # Proportion in percent + proportion_tao = hotkey_stake.tao * proportion # Proportion in TAO + + total_proportion += proportion_percent + + # Conditionally format text + proportion_str = f"{proportion_percent}% ({proportion_tao}τ)" + stake_weight = stake.tao + proportion_tao + total_stake_weight += stake_weight + hotkey = Text(hotkey, style="red" if proportion == 0 else "") table.add_row( str(i), hotkey, proportion_str, - str(stake), + str(stake.tao), + str(stake_weight), ) # add totals row - table.add_row("", "Total", str(total_proportion), str(total_stake), "") + table.add_row("", "Total", f"{total_proportion}%", f"{total_stake}τ", f"{total_stake_weight}τ") console.print(table) diff --git a/bittensor/extrinsics/staking.py b/bittensor/extrinsics/staking.py index 686b7d60e5..14f7cc6349 100644 --- a/bittensor/extrinsics/staking.py +++ b/bittensor/extrinsics/staking.py @@ -560,6 +560,14 @@ def set_children_extrinsic( bittensor.errors.NotRegisteredError: If the hotkey is not registered in any subnets. """ + + # Decrypt coldkey. + wallet.coldkey + + user_hotkey_ss58 = wallet.hotkey.ss58_address # Default to wallet's own hotkey. + if hotkey != user_hotkey_ss58: + raise ValueError("Can only call children for other hotkeys.") + # Check if all children are being revoked all_revoked = all(prop == 0.0 for prop, _ in children_with_proportions) diff --git a/bittensor/subtensor.py b/bittensor/subtensor.py index 9bb0197100..007a09e107 100644 --- a/bittensor/subtensor.py +++ b/bittensor/subtensor.py @@ -3253,7 +3253,9 @@ def query_runtime_api( """ call_definition = bittensor.__type_registry__["runtime_api"][runtime_api][ # type: ignore "methods" # type: ignore - ][method] # type: ignore + ][ + method + ] # type: ignore json_result = self.state_call( method=f"{runtime_api}_{method}", @@ -4639,7 +4641,9 @@ def get_parents(self, child_hotkey, netuid): return [] formatted_parents = [ - format_parent(proportion, parent) for proportion, parent in parents + format_parent(proportion, parent) + for proportion, parent in parents + if proportion != 0 ] return formatted_parents except SubstrateRequestException as e: diff --git a/bittensor/utils/subtensor.py b/bittensor/utils/subtensor.py index 13e2f0086b..279a683222 100644 --- a/bittensor/utils/subtensor.py +++ b/bittensor/utils/subtensor.py @@ -168,5 +168,6 @@ def format_children(children) -> List[Tuple[str, str]]: int_proportion = ( proportion.value if hasattr(proportion, "value") else int(proportion) ) - formatted_children.append((int_proportion, child.value)) + if int_proportion > 0: + formatted_children.append((int_proportion, child.value)) return formatted_children diff --git a/tests/e2e_tests/subcommands/stake/test_childkeys.py b/tests/e2e_tests/subcommands/stake/test_childkeys.py index e3374d72d1..d29fb877d9 100644 --- a/tests/e2e_tests/subcommands/stake/test_childkeys.py +++ b/tests/e2e_tests/subcommands/stake/test_childkeys.py @@ -74,7 +74,7 @@ async def wait(): await wait() children_with_proportions = [ - [0.3, bob_keypair.ss58_address], + [0.6, bob_keypair.ss58_address], [0.4, eve_keypair.ss58_address], ] @@ -125,10 +125,14 @@ async def wait(): ], ) output = capsys.readouterr().out - assert "ParentHotKey:\n5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY" in output - assert "5HGjWAeFDfFCWPsjFQdVV2Msvz2XtMktvgocEZcCj6… │ 105409966135483…" in output - assert "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJ… │ 790574746016123…" in output - assert "Total │ 184467440737095…" in output + assert ( + "Parent HotKey: 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY | Total Parent Stake: 100000.0" + in output + ) + assert "ChildHotkey ┃ Proportion" in output + assert "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92U… │ 60.0%" in output + assert "5HGjWAeFDfFCWPsjFQdVV2Msvz2XtMktvgocEZc… │ 40.0%" in output + assert "Total │ 100.0%" in output await wait() @@ -151,10 +155,9 @@ async def wait(): await wait() - assert subtensor.get_children(netuid=1, hotkey=alice_keypair.ss58_address) == [ - (0, children_with_proportions[0][1]), - (0, children_with_proportions[1][1]), - ], "Failed to revoke children hotkeys" + assert ( + subtensor.get_children(netuid=1, hotkey=alice_keypair.ss58_address) == [] + ), "Failed to revoke children hotkeys" await wait() # Test 4: Get children after revocation @@ -170,6 +173,4 @@ async def wait(): ], ) output = capsys.readouterr().out - assert "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM69… │ 0 " in output - assert "5HGjWAeFDfFCWPsjFQdVV2Msvz2XtMktvgocEZcCj68kU… │ 0" in output - assert "Total │ 0" in output + assert "There are currently no child hotkeys on subnet" in output From 01646a793e54b895ee38a64c362934c1ff34441f Mon Sep 17 00:00:00 2001 From: opendansor Date: Fri, 16 Aug 2024 14:52:38 -0700 Subject: [PATCH 4/6] Lint --- bittensor/commands/stake.py | 58 +++++++++++++++++++++++++------------ bittensor/subtensor.py | 4 +-- 2 files changed, 41 insertions(+), 21 deletions(-) diff --git a/bittensor/commands/stake.py b/bittensor/commands/stake.py index 969ef92091..cce1fc5c2a 100644 --- a/bittensor/commands/stake.py +++ b/bittensor/commands/stake.py @@ -615,7 +615,7 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): # Get values if not set. if not cli.config.is_set("netuid"): cli.config.netuid = int(Prompt.ask("Enter netuid")) - + netuid = cli.config.netuid total_subnets = subtensor.get_total_subnets() if total_subnets is not None and total_subnets <= netuid <= 0: @@ -624,7 +624,9 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): if not cli.config.is_set("hotkey"): cli.config.hotkey = Prompt.ask("Enter parent hotkey (ss58)") if not wallet_utils.is_valid_ss58_address(cli.config.hotkey): - console.print(f":cross_mark:[red] Invalid SS58 address: {cli.config.hotkey}[/red]") + console.print( + f":cross_mark:[red] Invalid SS58 address: {cli.config.hotkey}[/red]" + ) return # get children @@ -634,7 +636,7 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): netuid=cli.config.netuid, render_table=False, ) - + if curr_children: GetChildrenCommand.retrieve_children( subtensor=subtensor, @@ -642,24 +644,28 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): netuid=cli.config.netuid, render_table=True, ) - raise ValueError(f"There are already children hotkeys under parent hotkey {cli.config.hotkey}. " - f"Call revoke_children command before attempting to set_children again, or call the get_children command to view them.") + raise ValueError( + f"There are already children hotkeys under parent hotkey {cli.config.hotkey}. " + f"Call revoke_children command before attempting to set_children again, or call the get_children command to view them." + ) if not cli.config.is_set("children"): cli.config.children = Prompt.ask( "Enter child(ren) hotkeys (ss58) as comma-separated values" ) children = [str(x) for x in re.split(r"[ ,]+", cli.config.children)] - + # Validate children SS58 addresses for child in children: if not wallet_utils.is_valid_ss58_address(child): console.print(f":cross_mark:[red] Invalid SS58 address: {child}[/red]") return - - if len(children) == 1: # if only one child, then they have full proportion by default + + if ( + len(children) == 1 + ): # if only one child, then they have full proportion by default cli.config.proportions = 1.0 - + if not cli.config.is_set("proportions"): cli.config.proportions = Prompt.ask( "Enter the percentage of proportion for each child as comma-separated values (total must equal 1)" @@ -804,13 +810,17 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): cli.config.hotkey = Prompt.ask("Enter parent hotkey (ss58)") hotkey = cli.config.hotkey if not wallet_utils.is_valid_ss58_address(cli.config.hotkey): - console.print(f":cross_mark:[red] Invalid SS58 address: {cli.config.hotkey}[/red]") + console.print( + f":cross_mark:[red] Invalid SS58 address: {cli.config.hotkey}[/red]" + ) return children = subtensor.get_children(hotkey, netuid) hotkey_stake = subtensor.get_total_stake_for_hotkey(hotkey) - GetChildrenCommand.render_table(subtensor, hotkey, hotkey_stake, children, netuid, True) + GetChildrenCommand.render_table( + subtensor, hotkey, hotkey_stake, children, netuid, True + ) return children @@ -835,7 +845,9 @@ def retrieve_children( children = subtensor.get_children(hotkey, netuid) if render_table: hotkey_stake = subtensor.get_total_stake_for_hotkey(hotkey) - GetChildrenCommand.render_table(subtensor, hotkey, hotkey_stake, children, netuid, False) + GetChildrenCommand.render_table( + subtensor, hotkey, hotkey_stake, children, netuid, False + ) return children @staticmethod @@ -907,7 +919,9 @@ def render_table( table.add_column("ChildHotkey", style="cyan", no_wrap=True) table.add_column("Proportion", style="cyan", no_wrap=True, justify="right") table.add_column("Child Stake", style="cyan", no_wrap=True, justify="right") - table.add_column("Total Stake Weight", style="cyan", no_wrap=True, justify="right") + table.add_column( + "Total Stake Weight", style="cyan", no_wrap=True, justify="right" + ) if not children: # console.print(table) @@ -921,7 +935,9 @@ def render_table( ) return - console.print(f"Parent HotKey: {hotkey} | ", style="cyan", end="", no_wrap=True) + console.print( + f"Parent HotKey: {hotkey} | ", style="cyan", end="", no_wrap=True + ) console.print(f"Total Parent Stake: {hotkey_stake.tao}τ") # calculate totals @@ -939,7 +955,7 @@ def render_table( # add to totals total_stake += child_stake.tao - + proportion = u64_to_float(proportion) children_info.append((proportion, child_hotkey, child_stake)) @@ -952,14 +968,14 @@ def render_table( for i, (proportion, hotkey, stake) in enumerate(children_info, 1): proportion_percent = proportion * 100 # Proportion in percent proportion_tao = hotkey_stake.tao * proportion # Proportion in TAO - + total_proportion += proportion_percent # Conditionally format text proportion_str = f"{proportion_percent}% ({proportion_tao}τ)" stake_weight = stake.tao + proportion_tao total_stake_weight += stake_weight - + hotkey = Text(hotkey, style="red" if proportion == 0 else "") table.add_row( str(i), @@ -970,5 +986,11 @@ def render_table( ) # add totals row - table.add_row("", "Total", f"{total_proportion}%", f"{total_stake}τ", f"{total_stake_weight}τ") + table.add_row( + "", + "Total", + f"{total_proportion}%", + f"{total_stake}τ", + f"{total_stake_weight}τ", + ) console.print(table) diff --git a/bittensor/subtensor.py b/bittensor/subtensor.py index 007a09e107..05ee9bb2c8 100644 --- a/bittensor/subtensor.py +++ b/bittensor/subtensor.py @@ -3253,9 +3253,7 @@ def query_runtime_api( """ call_definition = bittensor.__type_registry__["runtime_api"][runtime_api][ # type: ignore "methods" # type: ignore - ][ - method - ] # type: ignore + ][method] # type: ignore json_result = self.state_call( method=f"{runtime_api}_{method}", From 558e56bdfab8c525e5cd1563486c053b3652132c Mon Sep 17 00:00:00 2001 From: opendansor Date: Fri, 16 Aug 2024 15:03:42 -0700 Subject: [PATCH 5/6] Add parent hotkey flag --- bittensor/commands/stake.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor/commands/stake.py b/bittensor/commands/stake.py index cce1fc5c2a..8240613220 100644 --- a/bittensor/commands/stake.py +++ b/bittensor/commands/stake.py @@ -588,7 +588,7 @@ class SetChildrenCommand: Example usage:: - btcli stake set_children --children , --netuid 1 --proportions 0.4,0.6 + btcli stake set_children --children , --hotkey --netuid 1 --proportions 0.4,0.6 Note: This command is critical for users who wish to delegate children hotkeys among different neurons (hotkeys) on the network. From 782601bf9b444a2bf208c2b5ccbf4e7c56750971 Mon Sep 17 00:00:00 2001 From: opendansor Date: Fri, 16 Aug 2024 15:04:30 -0700 Subject: [PATCH 6/6] Add table back --- bittensor/commands/stake.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor/commands/stake.py b/bittensor/commands/stake.py index 8240613220..3061ea7f79 100644 --- a/bittensor/commands/stake.py +++ b/bittensor/commands/stake.py @@ -924,7 +924,7 @@ def render_table( ) if not children: - # console.print(table) + console.print(table) console.print( f"There are currently no child hotkeys on subnet {netuid} with Parent HotKey {hotkey}." )