From 822c55d8242e663964be6c5d050596c42d7c6404 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Wed, 14 Aug 2024 22:21:47 +0200 Subject: [PATCH 1/4] SubtensorInterface async update, `sudo set` command --- cli.py | 154 ++++++++++- src/__init__.py | 26 ++ src/bittensor/chain_data.py | 88 +++++++ src/commands/root.py | 465 ++++++++++++++++----------------- src/commands/stake.py | 439 ++++++++++++++++--------------- src/commands/subnets.py | 34 +++ src/commands/sudo.py | 185 ++++++++++++++ src/commands/wallets.py | 497 ++++++++++++++++++------------------ src/subtensor_interface.py | 34 ++- src/utils.py | 56 +++- 10 files changed, 1254 insertions(+), 724 deletions(-) create mode 100644 src/commands/subnets.py create mode 100644 src/commands/sudo.py diff --git a/cli.py b/cli.py index 8f2b0cf5..496b457d 100755 --- a/cli.py +++ b/cli.py @@ -2,7 +2,7 @@ import asyncio import os.path import re -from typing import Optional, Coroutine, Collection +from typing import Optional, Coroutine from bittensor_wallet import Wallet from git import Repo @@ -14,8 +14,8 @@ from websockets import ConnectionClosed from yaml import safe_load, safe_dump -from src import defaults, utils -from src.commands import wallets, root, stake +from src import defaults, utils, HYPERPARAMS +from src.commands import wallets, root, stake, sudo from src.subtensor_interface import SubtensorInterface from src.bittensor.async_substrate_interface import SubstrateRequestException from src.utils import console, err_console @@ -162,6 +162,7 @@ class CLIManager: :var wallet_app: the Typer app as it relates to wallet commands :var root_app: the Typer app as it relates to root commands :var stake_app: the Typer app as it relates to stake commands + :var sudo_app: the Typer app as it relates to sudo commands :var not_subtensor: the `SubtensorInterface` object passed to the various commands that require it """ @@ -186,6 +187,7 @@ def __init__(self): self.wallet_app = typer.Typer() self.root_app = typer.Typer() self.stake_app = typer.Typer() + self.sudo_app = typer.Typer() # config alias self.app.add_typer( @@ -221,6 +223,14 @@ def __init__(self): ) self.app.add_typer(self.stake_app, name="st", hidden=True) + # sudo aliases + self.app.add_typer( + self.sudo_app, + name="sudo", + short_help="Sudo commands, alias: `su`", + ) + self.app.add_typer(self.sudo_app, name="su", hidden=True) + # config commands self.config_app.command("set")(self.set_config) self.config_app.command("get")(self.get_config) @@ -267,6 +277,9 @@ def __init__(self): self.stake_app.command("set-children")(self.stake_set_children) self.stake_app.command("revoke-children")(self.stake_revoke_children) + # sudo commands + self.sudo_app.command("set")(self.sudo_set) + def initialize_chain( self, network: Optional[str] = typer.Option("default_network", help="Network name"), @@ -297,8 +310,14 @@ def _run_command(self, cmd: Coroutine) -> None: """ Runs the supplied coroutine with asyncio.run """ + async def _run(): + if self.not_subtensor: + async with self.not_subtensor: + await cmd + else: + await cmd try: - return asyncio.run(cmd) + return asyncio.run(_run()) except ConnectionRefusedError: err_console.print( f"Connection refused when connecting to chain: {self.not_subtensor}" @@ -2600,6 +2619,133 @@ def stake_revoke_children( ) ) + def sudo_set(self, + network: Optional[str] = Options.network, + chain: Optional[str] = Options.chain, + wallet_name: str = Options.wallet_name, + wallet_path: str = Options.wallet_path, + wallet_hotkey: str = Options.wallet_hotkey, + netuid: int = Options.netuid, + param_name: str = typer.Option( + "", + "--param", "--parameter", + help="The subnet hyperparameter to set" + ), + param_value: str = typer.Option( + "", + "--value", + help="The subnet hyperparameter value to set." + ) + ): + """ + # sudo set + Executes the `set` command to set hyperparameters for a specific subnet on the Bittensor network. + + This command allows subnet owners to modify various hyperparameters of theirs subnet, such as its tempo, + emission rates, and other network-specific settings. + + ## Usage: + The command first prompts the user to enter the hyperparameter they wish to change and its new value. + It then uses the user's wallet and configuration settings to authenticate and send the hyperparameter update + to the specified subnet. + + ### Example usage: + + ``` + btcli sudo set --netuid 1 --param 'tempo' --value '0.5' + ``` + + #### Note: + This command requires the user to specify the subnet identifier (``netuid``) and both the hyperparameter + and its new value. It is intended for advanced users who are familiar with the network's functioning + and the impact of changing these parameters. + """ + if not param_name: + param_name = Prompt.ask("Enter hyperparameter", choices=list(HYPERPARAMS.keys())) + if not param_value: + param_value = Prompt.ask(f"Enter new value for {param_name}") + wallet = self.wallet_ask(wallet_name, wallet_path, wallet_hotkey) + return self._run_command(sudo.sudo_set_hyperparameter( + wallet, + self.initialize_chain(network, chain), + netuid, + param_name, + param_value + )) + + def subnets_get_hyperparameters(self, + network: str = Options.network, + chain: str = Options.chain, + netuid: int = Options.netuid + ): + """ + # subnets get-hyperparameters + Executes the `hyperparameters` command to view the current hyperparameters of a specific subnet on the Bittensor + network. + + This command is useful for users who wish to understand the configuration and + operational parameters of a particular subnet. + + ## Usage: + Upon invocation, the command fetches and displays a list of all hyperparameters for the specified subnet. + These include settings like tempo, emission rates, and other critical network parameters that define + the subnet's behavior. + + ### Example usage: + + ``` + $ btcli subnets hyperparameters --netuid 1 + + + + Subnet Hyperparameters - NETUID: 1 - finney + + HYPERPARAMETER VALUE + + rho 10 + + kappa 32767 + + immunity_period 7200 + + min_allowed_weights 8 + + max_weight_limit 455 + + tempo 99 + + min_difficulty 1000000000000000000 + + max_difficulty 1000000000000000000 + + weights_version 2013 + + weights_rate_limit 100 + + adjustment_interval 112 + + activity_cutoff 5000 + + registration_allowed True + + target_regs_per_interval 2 + + min_burn 1000000000 + + max_burn 100000000000 + + bonds_moving_avg 900000 + + max_regs_per_block 1 + + + ``` + + #### Note: + The user must specify the subnet identifier (`netuid`) for which they want to view the hyperparameters. + This command is read-only and does not modify the network state or configurations. + """ + def run(self): self.app() diff --git a/src/__init__.py b/src/__init__.py index dbba012e..cea1f09d 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -188,3 +188,29 @@ class logging: "finney": "https://x.taostats.io", }, } + + +HYPERPARAMS = { + "serving_rate_limit": "sudo_set_serving_rate_limit", + "min_difficulty": "sudo_set_min_difficulty", + "max_difficulty": "sudo_set_max_difficulty", + "weights_version": "sudo_set_weights_version_key", + "weights_rate_limit": "sudo_set_weights_set_rate_limit", + "max_weight_limit": "sudo_set_max_weight_limit", + "immunity_period": "sudo_set_immunity_period", + "min_allowed_weights": "sudo_set_min_allowed_weights", + "activity_cutoff": "sudo_set_activity_cutoff", + "network_registration_allowed": "sudo_set_network_registration_allowed", + "network_pow_registration_allowed": "sudo_set_network_pow_registration_allowed", + "min_burn": "sudo_set_min_burn", + "max_burn": "sudo_set_max_burn", + "adjustment_alpha": "sudo_set_adjustment_alpha", + "rho": "sudo_set_rho", + "kappa": "sudo_set_kappa", + "difficulty": "sudo_set_difficulty", + "bonds_moving_avg": "sudo_set_bonds_moving_average", + "commit_reveal_weights_interval": "sudo_set_commit_reveal_weights_interval", + "commit_reveal_weights_enabled": "sudo_set_commit_reveal_weights_enabled", + "alpha_values": "sudo_set_alpha_values", + "liquid_alpha_enabled": "sudo_set_liquid_alpha_enabled", +} diff --git a/src/bittensor/chain_data.py b/src/bittensor/chain_data.py index e94b1e56..a8a20264 100644 --- a/src/bittensor/chain_data.py +++ b/src/bittensor/chain_data.py @@ -109,6 +109,94 @@ def from_neuron_info(cls, neuron_info: dict) -> "AxonInfo": coldkey=neuron_info["coldkey"], ) +@dataclass +class SubnetHyperparameters: + """Dataclass for subnet hyperparameters.""" + + rho: int + kappa: int + immunity_period: int + min_allowed_weights: int + max_weight_limit: float + tempo: int + min_difficulty: int + max_difficulty: int + weights_version: int + weights_rate_limit: int + adjustment_interval: int + activity_cutoff: int + registration_allowed: bool + target_regs_per_interval: int + min_burn: int + max_burn: int + bonds_moving_avg: int + max_regs_per_block: int + serving_rate_limit: int + max_validators: int + adjustment_alpha: int + difficulty: int + commit_reveal_weights_interval: int + commit_reveal_weights_enabled: bool + alpha_high: int + alpha_low: int + liquid_alpha_enabled: bool + + @classmethod + def from_vec_u8(cls, vec_u8: list[int]) -> Optional["SubnetHyperparameters"]: + """Returns a SubnetHyperparameters object from a ``vec_u8``.""" + if len(vec_u8) == 0: + return None + + decoded = from_scale_encoding(vec_u8, ChainDataType.SubnetHyperparameters) + if decoded is None: + return None + + return SubnetHyperparameters.fix_decoded_values(decoded) + + @classmethod + def list_from_vec_u8(cls, vec_u8: list[int]) -> list["SubnetHyperparameters"]: + """Returns a list of SubnetHyperparameters objects from a ``vec_u8``.""" + decoded = from_scale_encoding( + vec_u8, ChainDataType.SubnetHyperparameters, is_vec=True, is_option=True + ) + if decoded is None: + return [] + + return [SubnetHyperparameters.fix_decoded_values(d) for d in decoded] + + @classmethod + def fix_decoded_values(cls, decoded: dict) -> "SubnetHyperparameters": + """Returns a SubnetInfo object from a decoded SubnetInfo dictionary.""" + return SubnetHyperparameters( + rho=decoded["rho"], + kappa=decoded["kappa"], + immunity_period=decoded["immunity_period"], + min_allowed_weights=decoded["min_allowed_weights"], + max_weight_limit=decoded["max_weights_limit"], + tempo=decoded["tempo"], + min_difficulty=decoded["min_difficulty"], + max_difficulty=decoded["max_difficulty"], + weights_version=decoded["weights_version"], + weights_rate_limit=decoded["weights_rate_limit"], + adjustment_interval=decoded["adjustment_interval"], + activity_cutoff=decoded["activity_cutoff"], + registration_allowed=decoded["registration_allowed"], + target_regs_per_interval=decoded["target_regs_per_interval"], + min_burn=decoded["min_burn"], + max_burn=decoded["max_burn"], + max_regs_per_block=decoded["max_regs_per_block"], + max_validators=decoded["max_validators"], + serving_rate_limit=decoded["serving_rate_limit"], + bonds_moving_avg=decoded["bonds_moving_avg"], + adjustment_alpha=decoded["adjustment_alpha"], + difficulty=decoded["difficulty"], + commit_reveal_weights_interval=decoded["commit_reveal_weights_interval"], + commit_reveal_weights_enabled=decoded["commit_reveal_weights_enabled"], + alpha_high=decoded["alpha_high"], + alpha_low=decoded["alpha_low"], + liquid_alpha_enabled=decoded["liquid_alpha_enabled"], + ) + @dataclass class StakeInfo: diff --git a/src/commands/root.py b/src/commands/root.py index 9bffd81e..760d8e05 100644 --- a/src/commands/root.py +++ b/src/commands/root.py @@ -695,29 +695,28 @@ async def root_list(subtensor: SubtensorInterface): """List the root network""" async def _get_list() -> tuple: - async with subtensor: - senate_query = await subtensor.substrate.query( - module="SenateMembers", - storage_function="Members", - params=None, - ) - sm = ( - senate_query.serialize() if hasattr(senate_query, "serialize") else None - ) + senate_query = await subtensor.substrate.query( + module="SenateMembers", + storage_function="Members", + params=None, + ) + sm = ( + senate_query.serialize() if hasattr(senate_query, "serialize") else None + ) - rn: list[NeuronInfoLite] = await subtensor.neurons_lite(netuid=0) - if not rn: - return None, None, None, None + rn: list[NeuronInfoLite] = await subtensor.neurons_lite(netuid=0) + if not rn: + return None, None, None, None - di: dict[str, DelegatesDetails] = await get_delegates_details_from_github( - url=Constants.delegates_detail_url - ) - ts: dict[str, ScaleType] = await subtensor.substrate.query_multiple( - [n.hotkey for n in rn], - module="SubtensorModule", - storage_function="TotalHotkeyStake", - reuse_block_hash=True, - ) + di: dict[str, DelegatesDetails] = await get_delegates_details_from_github( + url=Constants.delegates_detail_url + ) + ts: dict[str, ScaleType] = await subtensor.substrate.query_multiple( + [n.hotkey for n in rn], + module="SubtensorModule", + storage_function="TotalHotkeyStake", + reuse_block_hash=True, + ) return sm, rn, di, ts table = Table( @@ -799,24 +798,22 @@ async def set_weights( # Run the set weights operation. with console.status("Setting root weights..."): - async with subtensor: - await set_root_weights_extrinsic( - subtensor=subtensor, - wallet=wallet, - netuids=netuids_, - weights=weights_, - version_key=0, - prompt=True, - wait_for_finalization=True, - wait_for_inclusion=True, - ) + await set_root_weights_extrinsic( + subtensor=subtensor, + wallet=wallet, + netuids=netuids_, + weights=weights_, + version_key=0, + prompt=True, + wait_for_finalization=True, + wait_for_inclusion=True, + ) async def get_weights(subtensor: SubtensorInterface): """Get weights for root network.""" with console.status(":satellite: Synchronizing with chain..."): - async with subtensor: - weights = await subtensor.weights(0) + weights = await subtensor.weights(0) uid_to_weights: dict[int, dict] = {} netuids = set() @@ -907,56 +904,53 @@ async def set_boost( wallet: Wallet, subtensor: SubtensorInterface, netuid: int, amount: float ): """Boosts weight of a given netuid for root network.""" + my_weights = await _get_my_weights(subtensor, wallet.hotkey.ss58_address) + prev_weight = my_weights[netuid] + new_weight = prev_weight + amount - async with subtensor: - my_weights = await _get_my_weights(subtensor, wallet.hotkey.ss58_address) - prev_weight = my_weights[netuid] - new_weight = prev_weight + amount + console.print( + f"Boosting weight for netuid {netuid} from {prev_weight} -> {new_weight}" + ) + my_weights[netuid] = new_weight + all_netuids = np.arange(len(my_weights)) - console.print( - f"Boosting weight for netuid {netuid} from {prev_weight} -> {new_weight}" + console.print("all netuids", all_netuids) + with console.status("Setting root weights..."): + await set_root_weights_extrinsic( + subtensor=subtensor, + wallet=wallet, + netuids=all_netuids, + weights=my_weights, + version_key=0, + wait_for_inclusion=True, + wait_for_finalization=True, + prompt=True, ) - my_weights[netuid] = new_weight - all_netuids = np.arange(len(my_weights)) - - console.print("all netuids", all_netuids) - with console.status("Setting root weights..."): - await set_root_weights_extrinsic( - subtensor=subtensor, - wallet=wallet, - netuids=all_netuids, - weights=my_weights, - version_key=0, - wait_for_inclusion=True, - wait_for_finalization=True, - prompt=True, - ) async def set_slash( wallet: Wallet, subtensor: SubtensorInterface, netuid: int, amount: float ): """Slashes weight I think""" - async with subtensor: - my_weights = await _get_my_weights(subtensor, wallet.hotkey.ss58_address) - prev_weights = my_weights.copy() - my_weights[netuid] -= amount - my_weights[my_weights < 0] = 0 # Ensure weights don't go negative - all_netuids = np.arange(len(my_weights)) - - console.print(f"Slash weights from {prev_weights} -> {my_weights}") - - with console.status("Setting root weights..."): - await set_root_weights_extrinsic( - subtensor=subtensor, - wallet=wallet, - netuids=all_netuids, - weights=my_weights, - version_key=0, - wait_for_inclusion=True, - wait_for_finalization=True, - prompt=True, - ) + my_weights = await _get_my_weights(subtensor, wallet.hotkey.ss58_address) + prev_weights = my_weights.copy() + my_weights[netuid] -= amount + my_weights[my_weights < 0] = 0 # Ensure weights don't go negative + all_netuids = np.arange(len(my_weights)) + + console.print(f"Slash weights from {prev_weights} -> {my_weights}") + + with console.status("Setting root weights..."): + await set_root_weights_extrinsic( + subtensor=subtensor, + wallet=wallet, + netuids=all_netuids, + weights=my_weights, + version_key=0, + wait_for_inclusion=True, + wait_for_finalization=True, + prompt=True, + ) async def senate_vote( @@ -970,43 +964,41 @@ async def senate_vote( ) return False - async with subtensor: - if not await _is_senate_member( - subtensor, hotkey_ss58=wallet.hotkey.ss58_address - ): - err_console.print( - f"Aborting: Hotkey {wallet.hotkey.ss58_address} isn't a senate member." - ) - return False + if not await _is_senate_member( + subtensor, hotkey_ss58=wallet.hotkey.ss58_address + ): + err_console.print( + f"Aborting: Hotkey {wallet.hotkey.ss58_address} isn't a senate member." + ) + return False - # Unlock the wallet. - wallet.unlock_hotkey() - wallet.unlock_coldkey() + # Unlock the wallet. + wallet.unlock_hotkey() + wallet.unlock_coldkey() - vote_data = await _get_vote_data(subtensor, proposal_hash, reuse_block=True) - if not vote_data: - err_console.print(":cross_mark: [red]Failed[/red]: Proposal not found.") - return False + vote_data = await _get_vote_data(subtensor, proposal_hash, reuse_block=True) + if not vote_data: + err_console.print(":cross_mark: [red]Failed[/red]: Proposal not found.") + return False - vote: bool = Confirm.ask("Desired vote for proposal") - success = await vote_senate_extrinsic( - subtensor=subtensor, - wallet=wallet, - proposal_hash=proposal_hash, - proposal_idx=vote_data["index"], - vote=vote, - wait_for_inclusion=True, - wait_for_finalization=False, - prompt=True, - ) + vote: bool = Confirm.ask("Desired vote for proposal") + success = await vote_senate_extrinsic( + subtensor=subtensor, + wallet=wallet, + proposal_hash=proposal_hash, + proposal_idx=vote_data["index"], + vote=vote, + wait_for_inclusion=True, + wait_for_finalization=False, + prompt=True, + ) return success async def get_senate(subtensor: SubtensorInterface): """View Bittensor's governance protocol proposals""" - console.print(f":satellite: Syncing with chain: [white]{subtensor}[/white] ...") - async with subtensor: + with console.status(f":satellite: Syncing with chain: [white]{subtensor}[/white] ..."): senate_members = await _get_senate_members(subtensor) delegate_info: dict[ @@ -1044,59 +1036,57 @@ async def get_senate(subtensor: SubtensorInterface): async def register(wallet: Wallet, subtensor: SubtensorInterface, netuid: int): """Register neuron by recycling some TAO.""" + # Verify subnet exists + if not await subtensor.subnet_exists(netuid=netuid): + err_console.print(f"[red]Subnet {netuid} does not exist[/red]") + return False - async with subtensor: - # Verify subnet exists - if not await subtensor.subnet_exists(netuid=netuid): - err_console.print(f"[red]Subnet {netuid} does not exist[/red]") - return False + # Check current recycle amount + recycle_call, balance_ = await asyncio.gather( + subtensor.get_hyperparameter( + param_name="Burn", netuid=netuid, reuse_block=True + ), + subtensor.get_balance(wallet.coldkeypub.ss58_address, reuse_block=True), + ) + try: + current_recycle = Balance.from_rao(int(recycle_call)) + balance: Balance = balance_[wallet.coldkeypub.ss58_address] + except TypeError: + err_console.print("Unable to retrieve current recycle.") + return False + except KeyError: + err_console.print("Unable to retrieve current balance.") + return False - # Check current recycle amount - recycle_call, balance_ = await asyncio.gather( - subtensor.get_hyperparameter( - param_name="Burn", netuid=netuid, reuse_block=True - ), - subtensor.get_balance(wallet.coldkeypub.ss58_address, reuse_block=True), + # Check balance is sufficient + if balance < current_recycle: + err_console.print( + f"[red]Insufficient balance {balance} to register neuron. " + f"Current recycle is {current_recycle} TAO[/red]" ) - try: - current_recycle = Balance.from_rao(int(recycle_call)) - balance: Balance = balance_[wallet.coldkeypub.ss58_address] - except TypeError: - err_console.print("Unable to retrieve current recycle.") - return False - except KeyError: - err_console.print("Unable to retrieve current balance.") - return False - - # Check balance is sufficient - if balance < current_recycle: - err_console.print( - f"[red]Insufficient balance {balance} to register neuron. " - f"Current recycle is {current_recycle} TAO[/red]" - ) - return False - - # if not cli.config.no_prompt: - if not ( - Confirm.ask( - f"Your balance is: [bold green]{balance}[/bold green]\n" - f"The cost to register by recycle is [bold red]{current_recycle}[/bold red]\n" - f"Do you want to continue?", - default=False, - ) - ): - return False + return False - await burned_register_extrinsic( - subtensor, - wallet, - netuid, - current_recycle, - balance, - wait_for_inclusion=False, - wait_for_finalization=True, - prompt=True, + # if not cli.config.no_prompt: + if not ( + Confirm.ask( + f"Your balance is: [bold green]{balance}[/bold green]\n" + f"The cost to register by recycle is [bold red]{current_recycle}[/bold red]\n" + f"Do you want to continue?", + default=False, ) + ): + return False + + await burned_register_extrinsic( + subtensor, + wallet, + netuid, + current_recycle, + balance, + wait_for_inclusion=False, + wait_for_finalization=True, + prompt=True, + ) async def proposals(subtensor: SubtensorInterface): @@ -1105,12 +1095,11 @@ async def proposals(subtensor: SubtensorInterface): subtensor.network ) ) - async with subtensor: - block_hash = await subtensor.substrate.get_chain_head() - senate_members, all_proposals = await asyncio.gather( - _get_senate_members(subtensor, block_hash), - _get_proposals(subtensor, block_hash), - ) + block_hash = await subtensor.substrate.get_chain_head() + senate_members, all_proposals = await asyncio.gather( + _get_senate_members(subtensor, block_hash), + _get_proposals(subtensor, block_hash), + ) registered_delegate_info: dict[ str, DelegatesDetails @@ -1211,8 +1200,7 @@ async def _do_set_take() -> bool: wallet.unlock_hotkey() wallet.unlock_coldkey() - async with subtensor: - result_ = await _do_set_take() + result_ = await _do_set_take() return result_ @@ -1225,16 +1213,15 @@ async def delegate_stake( ): """Delegates stake to a chain delegate.""" - async with subtensor: - await delegate_extrinsic( - subtensor, - wallet, - delegate_ss58key, - Balance.from_tao(amount), - wait_for_inclusion=True, - prompt=True, - delegate=True, - ) + await delegate_extrinsic( + subtensor, + wallet, + delegate_ss58key, + Balance.from_tao(amount), + wait_for_inclusion=True, + prompt=True, + delegate=True, + ) async def delegate_unstake( @@ -1244,16 +1231,15 @@ async def delegate_unstake( delegate_ss58key: str, ): """Undelegates stake from a chain delegate.""" - async with subtensor: - await delegate_extrinsic( - subtensor, - wallet, - delegate_ss58key, - Balance.from_tao(amount), - wait_for_inclusion=True, - prompt=True, - delegate=False, - ) + await delegate_extrinsic( + subtensor, + wallet, + delegate_ss58key, + Balance.from_tao(amount), + wait_for_inclusion=True, + prompt=True, + delegate=False, + ) async def my_delegates( @@ -1319,22 +1305,21 @@ async def wallet_to_delegates( total_delegated = 0 - async with subtensor: - block_hash = await subtensor.substrate.get_chain_head() - registered_delegate_info: dict[str, DelegatesDetails] - wallets_with_delegates: tuple[ - tuple[Optional[Wallet], Optional[list[tuple[DelegateInfo, Balance]]]] - ] - wallets_with_delegates, registered_delegate_info = await asyncio.gather( - asyncio.gather( - *[wallet_to_delegates(wallet_, block_hash) for wallet_ in wallets] - ), - get_delegates_details_from_github(Constants.delegates_detail_url), + block_hash = await subtensor.substrate.get_chain_head() + registered_delegate_info: dict[str, DelegatesDetails] + wallets_with_delegates: tuple[ + tuple[Optional[Wallet], Optional[list[tuple[DelegateInfo, Balance]]]] + ] + wallets_with_delegates, registered_delegate_info = await asyncio.gather( + asyncio.gather( + *[wallet_to_delegates(wallet_, block_hash) for wallet_ in wallets] + ), + get_delegates_details_from_github(Constants.delegates_detail_url), + ) + if not registered_delegate_info: + console.print( + ":warning:[yellow]Could not get delegate info from chain.[/yellow]" ) - if not registered_delegate_info: - console.print( - ":warning:[yellow]Could not get delegate info from chain.[/yellow]" - ) for wall, delegates in wallets_with_delegates: if not wall or not delegates: @@ -1401,25 +1386,24 @@ async def list_delegates(subtensor: SubtensorInterface): """List all delegates on the network.""" with console.status(":satellite: Loading delegates..."): - async with subtensor: - block_hash, registered_delegate_info = await asyncio.gather( - subtensor.substrate.get_chain_head(), - get_delegates_details_from_github(Constants.delegates_detail_url), + block_hash, registered_delegate_info = await asyncio.gather( + subtensor.substrate.get_chain_head(), + get_delegates_details_from_github(Constants.delegates_detail_url), + ) + block_number = await subtensor.substrate.get_block_number(block_hash) + delegates: list[DelegateInfo] = await subtensor.get_delegates( + block_hash=block_hash + ) + + try: + prev_block_hash = await subtensor.substrate.get_block_hash( + max(0, block_number - 1200) ) - block_number = await subtensor.substrate.get_block_number(block_hash) - delegates: list[DelegateInfo] = await subtensor.get_delegates( - block_hash=block_hash + prev_delegates = await subtensor.get_delegates( + block_hash=prev_block_hash ) - - try: - prev_block_hash = await subtensor.substrate.get_block_hash( - max(0, block_number - 1200) - ) - prev_delegates = await subtensor.get_delegates( - block_hash=prev_block_hash - ) - except SubstrateRequestException: - prev_delegates = None + except SubstrateRequestException: + prev_delegates = None if prev_delegates is None: err_console.print( @@ -1561,40 +1545,39 @@ async def nominate(wallet: Wallet, subtensor: SubtensorInterface): wallet.unlock_hotkey() wallet.unlock_coldkey() - async with subtensor: - # Check if the hotkey is already a delegate. - if await subtensor.is_hotkey_delegate(wallet.hotkey.ss58_address): - err_console.print( - f"Aborting: Hotkey {wallet.hotkey.ss58_address} is already a delegate." - ) - return + # Check if the hotkey is already a delegate. + if await subtensor.is_hotkey_delegate(wallet.hotkey.ss58_address): + err_console.print( + f"Aborting: Hotkey {wallet.hotkey.ss58_address} is already a delegate." + ) + return - result: bool = await nominate_extrinsic(subtensor, wallet) - if not result: + result: bool = await nominate_extrinsic(subtensor, wallet) + if not result: + err_console.print( + f"Could not became a delegate on [white]{subtensor.network}[/white]" + ) + return + else: + # Check if we are a delegate. + is_delegate: bool = await subtensor.is_hotkey_delegate( + wallet.hotkey.ss58_address + ) + if not is_delegate: err_console.print( f"Could not became a delegate on [white]{subtensor.network}[/white]" ) return - else: - # Check if we are a delegate. - is_delegate: bool = await subtensor.is_hotkey_delegate( - wallet.hotkey.ss58_address - ) - if not is_delegate: - err_console.print( - f"Could not became a delegate on [white]{subtensor.network}[/white]" - ) - return - console.print( - f"Successfully became a delegate on [white]{subtensor.network}[/white]" - ) + console.print( + f"Successfully became a delegate on [white]{subtensor.network}[/white]" + ) - # Prompt use to set identity on chain. - if not False: # TODO no-prompt here - do_set_identity = Confirm.ask( - "Subnetwork registered successfully. Would you like to set your identity? [y/n]" - ) + # Prompt use to set identity on chain. + if not False: # TODO no-prompt here + do_set_identity = Confirm.ask( + "Subnetwork registered successfully. Would you like to set your identity? [y/n]" + ) - if do_set_identity: - id_prompts = set_id_prompts() - await set_id(wallet, subtensor, *id_prompts) + if do_set_identity: + id_prompts = set_id_prompts() + await set_id(wallet, subtensor, *id_prompts) diff --git a/src/commands/stake.py b/src/commands/stake.py index 07ac86b8..dfaca126 100644 --- a/src/commands/stake.py +++ b/src/commands/stake.py @@ -1086,9 +1086,8 @@ async def get_all_wallet_accounts( return accounts_ with console.status(":satellite:Retrieving account data..."): - async with subtensor: - block_hash_ = await subtensor.substrate.get_chain_head() - accounts = await get_all_wallet_accounts(block_hash=block_hash_) + block_hash_ = await subtensor.substrate.get_chain_head() + accounts = await get_all_wallet_accounts(block_hash=block_hash_) total_stake = 0 total_balance = 0 @@ -1188,128 +1187,127 @@ async def is_hotkey_registered_any(hk: str, bh: str) -> bool: hotkeys_to_stake_to = [(None, hotkey_ss58_or_name)] try: - async with subtensor: - # Get coldkey balance - wallet_balance_: dict[str, Balance] = await subtensor.get_balance( - wallet.coldkeypub.ss58_address - ) - block_hash = subtensor.substrate.last_block_hash - wallet_balance: Balance = wallet_balance_[wallet.coldkeypub.ss58_address] - old_balance = copy.copy(wallet_balance) - final_hotkeys: list[tuple[Optional[str], str]] = [] - final_amounts: list[Union[float, Balance]] = [] - hotkey: tuple[Optional[str], str] # (hotkey_name (or None), hotkey_ss58) - registered_ = asyncio.gather( + # Get coldkey balance + wallet_balance_: dict[str, Balance] = await subtensor.get_balance( + wallet.coldkeypub.ss58_address + ) + block_hash = subtensor.substrate.last_block_hash + wallet_balance: Balance = wallet_balance_[wallet.coldkeypub.ss58_address] + old_balance = copy.copy(wallet_balance) + final_hotkeys: list[tuple[Optional[str], str]] = [] + final_amounts: list[Union[float, Balance]] = [] + hotkey: tuple[Optional[str], str] # (hotkey_name (or None), hotkey_ss58) + registered_ = asyncio.gather( + *[ + is_hotkey_registered_any(h[1], block_hash) + for h in hotkeys_to_stake_to + ] + ) + if max_stake: + hotkey_stakes_ = asyncio.gather( *[ - is_hotkey_registered_any(h[1], block_hash) + subtensor.get_stake_for_coldkey_and_hotkey( + hotkey_ss58=h[1], + coldkey_ss58=wallet.coldkeypub.ss58_address, + block_hash=block_hash, + ) for h in hotkeys_to_stake_to ] ) - if max_stake: - hotkey_stakes_ = asyncio.gather( - *[ - subtensor.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=h[1], - coldkey_ss58=wallet.coldkeypub.ss58_address, - block_hash=block_hash, - ) - for h in hotkeys_to_stake_to - ] - ) - else: + else: - async def null(): - return [None] * len(hotkeys_to_stake_to) + async def null(): + return [None] * len(hotkeys_to_stake_to) - hotkey_stakes_ = null() - registered: list[bool] - hotkey_stakes: list[Optional[Balance]] - registered, hotkey_stakes = await asyncio.gather( - registered_, hotkey_stakes_ - ) + hotkey_stakes_ = null() + registered: list[bool] + hotkey_stakes: list[Optional[Balance]] + registered, hotkey_stakes = await asyncio.gather( + registered_, hotkey_stakes_ + ) - for hotkey, reg, hotkey_stake in zip( - hotkeys_to_stake_to, registered, hotkey_stakes - ): - if not reg: - # Hotkey is not registered. - if len(hotkeys_to_stake_to) == 1: - # Only one hotkey, error - err_console.print( - f"[red]Hotkey [bold]{hotkey[1]}[/bold] is not registered. Aborting.[/red]" - ) - raise ValueError - else: - # Otherwise, print warning and skip - console.print( - f"[yellow]Hotkey [bold]{hotkey[1]}[/bold] is not registered. Skipping.[/yellow]" - ) - continue - - stake_amount_tao: float = amount - if max_stake: - stake_amount_tao = max_stake - hotkey_stake.tao - - # If the max_stake is greater than the current wallet balance, stake the entire balance. - stake_amount_tao = min(stake_amount_tao, wallet_balance.tao) - if ( - stake_amount_tao <= 0.00001 - ): # Threshold because of fees, might create a loop otherwise - # Skip hotkey if max_stake is less than current stake. - continue - wallet_balance = Balance.from_tao( - wallet_balance.tao - stake_amount_tao + for hotkey, reg, hotkey_stake in zip( + hotkeys_to_stake_to, registered, hotkey_stakes + ): + if not reg: + # Hotkey is not registered. + if len(hotkeys_to_stake_to) == 1: + # Only one hotkey, error + err_console.print( + f"[red]Hotkey [bold]{hotkey[1]}[/bold] is not registered. Aborting.[/red]" ) + raise ValueError + else: + # Otherwise, print warning and skip + console.print( + f"[yellow]Hotkey [bold]{hotkey[1]}[/bold] is not registered. Skipping.[/yellow]" + ) + continue - if wallet_balance.tao < 0: - # No more balance to stake. - break + stake_amount_tao: float = amount + if max_stake: + stake_amount_tao = max_stake - hotkey_stake.tao + + # If the max_stake is greater than the current wallet balance, stake the entire balance. + stake_amount_tao = min(stake_amount_tao, wallet_balance.tao) + if ( + stake_amount_tao <= 0.00001 + ): # Threshold because of fees, might create a loop otherwise + # Skip hotkey if max_stake is less than current stake. + continue + wallet_balance = Balance.from_tao( + wallet_balance.tao - stake_amount_tao + ) - final_amounts.append(stake_amount_tao) - final_hotkeys.append(hotkey) # add both the name and the ss58 address. + if wallet_balance.tao < 0: + # No more balance to stake. + break - if len(final_hotkeys) == 0: - # No hotkeys to stake to. - err_console.print( - "Not enough balance to stake to any hotkeys or max_stake is less than current stake." - ) - raise ValueError + final_amounts.append(stake_amount_tao) + final_hotkeys.append(hotkey) # add both the name and the ss58 address. - # Ask to stake - if not False: # TODO no-prompt - if not Confirm.ask( - f"Do you want to stake to the following keys from {wallet.name}:\n" - + "".join( - [ - f" [bold white]- {hotkey[0] + ':' if hotkey[0] else ''}{hotkey[1]}: " - f"{f'{amount} {Balance.unit}' if amount else 'All'}[/bold white]\n" - for hotkey, amount in zip(final_hotkeys, final_amounts) - ] - ) - ): - raise ValueError + if len(final_hotkeys) == 0: + # No hotkeys to stake to. + err_console.print( + "Not enough balance to stake to any hotkeys or max_stake is less than current stake." + ) + raise ValueError - if len(final_hotkeys) == 1: - # do regular stake - await add_stake_extrinsic( - subtensor, - wallet=wallet, - old_balance=old_balance, - hotkey_ss58=final_hotkeys[0][1], - amount=None if stake_all else final_amounts[0], - wait_for_inclusion=True, - prompt=True, - ) - else: - await add_stake_multiple_extrinsic( - subtensor, - wallet=wallet, - old_balance=old_balance, - hotkey_ss58s=[hotkey_ss58 for _, hotkey_ss58 in final_hotkeys], - amounts=None if stake_all else final_amounts, - wait_for_inclusion=True, - prompt=False, + # Ask to stake + if not False: # TODO no-prompt + if not Confirm.ask( + f"Do you want to stake to the following keys from {wallet.name}:\n" + + "".join( + [ + f" [bold white]- {hotkey[0] + ':' if hotkey[0] else ''}{hotkey[1]}: " + f"{f'{amount} {Balance.unit}' if amount else 'All'}[/bold white]\n" + for hotkey, amount in zip(final_hotkeys, final_amounts) + ] ) + ): + raise ValueError + + if len(final_hotkeys) == 1: + # do regular stake + await add_stake_extrinsic( + subtensor, + wallet=wallet, + old_balance=old_balance, + hotkey_ss58=final_hotkeys[0][1], + amount=None if stake_all else final_amounts[0], + wait_for_inclusion=True, + prompt=True, + ) + else: + await add_stake_multiple_extrinsic( + subtensor, + wallet=wallet, + old_balance=old_balance, + hotkey_ss58s=[hotkey_ss58 for _, hotkey_ss58 in final_hotkeys], + amounts=None if stake_all else final_amounts, + wait_for_inclusion=True, + prompt=False, + ) except ValueError: pass @@ -1369,79 +1367,78 @@ async def unstake( final_amounts: list[Union[float, Balance]] = [] hotkey: tuple[Optional[str], str] # (hotkey_name (or None), hotkey_ss58) with suppress(ValueError): - async with subtensor: - with console.status(f":satellite:Syncing with chain {subtensor}"): - block_hash = await subtensor.substrate.get_chain_head() - hotkey_stakes = await asyncio.gather( - *[ - subtensor.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=hotkey[1], - coldkey_ss58=wallet.coldkeypub.ss58_address, - block_hash=block_hash, - ) - for hotkey in hotkeys_to_unstake_from - ] - ) - for hotkey, hotkey_stake in zip(hotkeys_to_unstake_from, hotkey_stakes): - unstake_amount_tao: float = amount - - if unstake_all: - unstake_amount_tao = hotkey_stake.tao - if max_stake: - # Get the current stake of the hotkey from this coldkey. - unstake_amount_tao = hotkey_stake.tao - max_stake - amount = unstake_amount_tao - if unstake_amount_tao < 0: - # Skip if max_stake is greater than current stake. - continue - else: - if unstake_amount_tao > hotkey_stake.tao: - # Skip if the specified amount is greater than the current stake. - continue + with console.status(f":satellite:Syncing with chain {subtensor}"): + block_hash = await subtensor.substrate.get_chain_head() + hotkey_stakes = await asyncio.gather( + *[ + subtensor.get_stake_for_coldkey_and_hotkey( + hotkey_ss58=hotkey[1], + coldkey_ss58=wallet.coldkeypub.ss58_address, + block_hash=block_hash, + ) + for hotkey in hotkeys_to_unstake_from + ] + ) + for hotkey, hotkey_stake in zip(hotkeys_to_unstake_from, hotkey_stakes): + unstake_amount_tao: float = amount - final_amounts.append(unstake_amount_tao) - final_hotkeys.append(hotkey) # add both the name and the ss58 address. + if unstake_all: + unstake_amount_tao = hotkey_stake.tao + if max_stake: + # Get the current stake of the hotkey from this coldkey. + unstake_amount_tao = hotkey_stake.tao - max_stake + amount = unstake_amount_tao + if unstake_amount_tao < 0: + # Skip if max_stake is greater than current stake. + continue + else: + if unstake_amount_tao > hotkey_stake.tao: + # Skip if the specified amount is greater than the current stake. + continue - if len(final_hotkeys) == 0: - # No hotkeys to unstake from. - err_console.print( - "Not enough stake to unstake from any hotkeys or max_stake is more than current stake." + final_amounts.append(unstake_amount_tao) + final_hotkeys.append(hotkey) # add both the name and the ss58 address. + + if len(final_hotkeys) == 0: + # No hotkeys to unstake from. + err_console.print( + "Not enough stake to unstake from any hotkeys or max_stake is more than current stake." + ) + return None + + # Ask to unstake + if not False: # TODO no prompt + if not Confirm.ask( + f"Do you want to unstake from the following keys to {wallet.name}:\n" + + "".join( + [ + f" [bold white]- {hotkey[0] + ':' if hotkey[0] else ''}{hotkey[1]}: " + f"{f'{amount} {Balance.unit}' if amount else 'All'}[/bold white]\n" + for hotkey, amount in zip(final_hotkeys, final_amounts) + ] ) + ): return None - # Ask to unstake - if not False: # TODO no prompt - if not Confirm.ask( - f"Do you want to unstake from the following keys to {wallet.name}:\n" - + "".join( - [ - f" [bold white]- {hotkey[0] + ':' if hotkey[0] else ''}{hotkey[1]}: " - f"{f'{amount} {Balance.unit}' if amount else 'All'}[/bold white]\n" - for hotkey, amount in zip(final_hotkeys, final_amounts) - ] - ) - ): - return None - - if len(final_hotkeys) == 1: - # do regular unstake - await unstake_extrinsic( - subtensor, - wallet=wallet, - hotkey_ss58=final_hotkeys[0][1], - amount=None if unstake_all else final_amounts[0], - wait_for_inclusion=True, - prompt=True, - ) - else: - await unstake_multiple_extrinsic( - subtensor, - wallet=wallet, - hotkey_ss58s=[hotkey_ss58 for _, hotkey_ss58 in final_hotkeys], - amounts=None if unstake_all else final_amounts, - wait_for_inclusion=True, - prompt=False, - ) + if len(final_hotkeys) == 1: + # do regular unstake + await unstake_extrinsic( + subtensor, + wallet=wallet, + hotkey_ss58=final_hotkeys[0][1], + amount=None if unstake_all else final_amounts[0], + wait_for_inclusion=True, + prompt=True, + ) + else: + await unstake_multiple_extrinsic( + subtensor, + wallet=wallet, + hotkey_ss58s=[hotkey_ss58 for _, hotkey_ss58 in final_hotkeys], + amounts=None if unstake_all else final_amounts, + wait_for_inclusion=True, + prompt=False, + ) async def get_children(wallet: Wallet, subtensor: "SubtensorInterface", netuid: int): @@ -1531,16 +1528,15 @@ async def render_table( table.add_row("", "Total", str(total_proportion), str(total_stake), "") console.print(table) - async with subtensor: - success, children, err_mg = await subtensor.get_children(wallet.hotkey, netuid) - if not success: - err_console.print( - f"Failed to get children from subtensor. {children[0]}: {err_mg}" - ) - if not children: - console.print("[yellow]No children found.[/yellow]") + success, children, err_mg = await subtensor.get_children(wallet.hotkey, netuid) + if not success: + err_console.print( + f"Failed to get children from subtensor. {children[0]}: {err_mg}" + ) + if not children: + console.print("[yellow]No children found.[/yellow]") - await render_table(wallet.hotkey, children, netuid) + await render_table(wallet.hotkey, children, netuid) return children @@ -1567,16 +1563,14 @@ async def set_children( ) children_with_proportions = list(zip(proportions, children)) - - async with subtensor: - success, message = await set_children_extrinsic( - subtensor=subtensor, - wallet=wallet, - netuid=netuid, - hotkey=wallet.hotkey.ss58_address, - children_with_proportions=children_with_proportions, - prompt=True, - ) + success, message = await set_children_extrinsic( + subtensor=subtensor, + wallet=wallet, + netuid=netuid, + hotkey=wallet.hotkey.ss58_address, + children_with_proportions=children_with_proportions, + prompt=True, + ) # Result if success: console.print(":white_heavy_check_mark: [green]Set children hotkeys.[/green]") @@ -1614,32 +1608,31 @@ async def revoke_children( >>> revoke_children(wallet, subtensor, 12345, wait_for_inclusion=True) """ # print table with diff prompts - async with subtensor: - success, current_children, err_msg = await subtensor.get_children( - wallet.hotkey.ss58_address, netuid - ) - if not success: - err_console.print(f"[red]Error retrieving children[/red]: {err_msg}") + success, current_children, err_msg = await subtensor.get_children( + wallet.hotkey.ss58_address, netuid + ) + if not success: + err_console.print(f"[red]Error retrieving children[/red]: {err_msg}") + return + # Validate children SS58 addresses + for child in current_children: + if not is_valid_ss58_address(child): + err_console.print( + f":cross_mark:[red] Invalid SS58 address: {child}[/red]" + ) return - # Validate children SS58 addresses - for child in current_children: - if not is_valid_ss58_address(child): - err_console.print( - f":cross_mark:[red] Invalid SS58 address: {child}[/red]" - ) - return - - # Prepare children with zero proportions - children_with_zero_proportions = [(0.0, child[1]) for child in current_children] - - success, message = await set_children_extrinsic( - subtensor=subtensor, - wallet=wallet, - netuid=netuid, - hotkey=wallet.hotkey.ss58_address, - children_with_proportions=children_with_zero_proportions, - prompt=True, - ) + + # Prepare children with zero proportions + children_with_zero_proportions = [(0.0, child[1]) for child in current_children] + + success, message = await set_children_extrinsic( + subtensor=subtensor, + wallet=wallet, + netuid=netuid, + hotkey=wallet.hotkey.ss58_address, + children_with_proportions=children_with_zero_proportions, + prompt=True, + ) # Result if success: diff --git a/src/commands/subnets.py b/src/commands/subnets.py new file mode 100644 index 00000000..1b676d3a --- /dev/null +++ b/src/commands/subnets.py @@ -0,0 +1,34 @@ +from typing import TYPE_CHECKING + +from rich.table import Table, Column + +from src.utils import console, normalize_hyperparameters + +if TYPE_CHECKING: + from src.subtensor_interface import SubtensorInterface + + +async def hyperparameters(subtensor: "SubtensorInterface", netuid: int): + """View hyperparameters of a subnetwork.""" + subnet = await subtensor.get_subnet_hyperparameters( + netuid + ) + + table = Table( + Column("[overline white]HYPERPARAMETER", style="white"), + Column("[overline white]VALUE", style="green"), + Column("[overline white]NORMALIZED", style="cyan"), + title=f"[white]Subnet Hyperparameters - NETUID: {netuid} - {subtensor}", + show_footer=True, + width=None, + pad_edge=True, + box=None, + show_edge=True, + ) + + normalized_values = normalize_hyperparameters(subnet) + + for param, value, norm_value in normalized_values: + table.add_row(" " + param, value, norm_value) + + console.print(table) diff --git a/src/commands/sudo.py b/src/commands/sudo.py new file mode 100644 index 00000000..c8cd36aa --- /dev/null +++ b/src/commands/sudo.py @@ -0,0 +1,185 @@ +import asyncio +from typing import TYPE_CHECKING, Union + +from bittensor_wallet import Wallet + +from src import HYPERPARAMS +from src.commands.subnets import hyperparameters +from src.utils import console, err_console + +if TYPE_CHECKING: + from src.subtensor_interface import SubtensorInterface + + +# helpers and extrinsics + +def allowed_value( + param: str, value: Union[str, bool] +) -> tuple[bool, Union[str, list[float], float, bool]]: + """ + Check the allowed values on hyperparameters. Return False if value is out of bounds. + + Reminder error message ends like: Value is {value} but must be {error_message}. (the second part of return + statement) + + Check if value is a boolean, only allow boolean and floats + """ + try: + if not isinstance(value, bool): + if param == "alpha_values": + # Split the string into individual values + alpha_low_str, alpha_high_str = value.split(",") + alpha_high = float(alpha_high_str) + alpha_low = float(alpha_low_str) + + # Check alpha_high value + if alpha_high <= 52428 or alpha_high >= 65535: + return ( + False, + f"between 52428 and 65535 for alpha_high (but is {alpha_high})", + ) + + # Check alpha_low value + if alpha_low < 0 or alpha_low > 52428: + return ( + False, + f"between 0 and 52428 for alpha_low (but is {alpha_low})", + ) + + return True, [alpha_low, alpha_high] + except ValueError: + return False, "a number or a boolean" + + return True, value + + +async def set_hyperparameter_extrinsic( + subtensor: "SubtensorInterface", + wallet: "Wallet", + netuid: int, + parameter: str, + value: Union[str, bool, float, list[float]], + wait_for_inclusion: bool = False, + wait_for_finalization: bool = True, +) -> bool: + """Sets a hyperparameter for a specific subnetwork. + + :param subtensor: initialized SubtensorInterface object + :param wallet: bittensor wallet object. + :param netuid: Subnetwork `uid`. + :param parameter: Hyperparameter name. + :param value: New hyperparameter value. + :param wait_for_inclusion: 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. + :param wait_for_finalization: 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. + :param prompt: If `True`, the call waits for confirmation from the user before proceeding. + + :return: success: `True` if extrinsic was finalized or included in the block. If we did not wait for + finalization/inclusion, the response is `True`. + """ + subnet_owner = getattr(await subtensor.substrate.query( + module="SubtensorModule", + storage_function="SubnetOwner", + params=[netuid], + ), "value", None) + if subnet_owner != wallet.coldkeypub.ss58_address: + err_console.print( + ":cross_mark: [red]This wallet doesn't own the specified subnet.[/red]" + ) + return False + + wallet.unlock_coldkey() + + extrinsic = HYPERPARAMS.get(parameter) + if extrinsic is None: + err_console.print( + ":cross_mark: [red]Invalid hyperparameter specified.[/red]" + ) + return False + + with console.status( + f":satellite: Setting hyperparameter {parameter} to {value} on subnet: {netuid} ..." + ): + with subtensor.substrate as substrate: + extrinsic_params = await substrate.get_metadata_call_function( + "AdminUtils", extrinsic + ) + call_params: dict[str, Union[str, bool, float]] = {"netuid": netuid} + + # if input value is a list, iterate through the list and assign values + if isinstance(value, list): + # Ensure that there are enough values for all non-netuid parameters + non_netuid_fields = [param["name"] for param in extrinsic_params["fields"] if + "netuid" not in param["name"]] + + if len(value) < len(non_netuid_fields): + raise ValueError("Not enough values provided in the list for all parameters") + + call_params.update({str(name): val for name, val in zip(non_netuid_fields, value)}) + + else: + value_argument = extrinsic_params["fields"][ + len(extrinsic_params["fields"]) - 1 + ] + call_params[str(value_argument["name"])] = value + + # create extrinsic call + call = await substrate.compose_call( + call_module="AdminUtils", + call_function=extrinsic, + call_params=call_params, + ) + success, err_msg = await subtensor.sign_and_send_extrinsic( + call, + wallet, + wait_for_inclusion, wait_for_finalization + ) + if not success: + err_console.print( + f":cross_mark: [red]Failed[/red]: {err_msg}" + ) + await asyncio.sleep(0.5) + + # Successful registration, final check for membership + else: + console.print( + f":white_heavy_check_mark: [green]Hyper parameter {parameter} changed to {value}[/green]" + ) + return True + + +# commands + +async def sudo_set_hyperparameter( + wallet: Wallet, + subtensor: "SubtensorInterface", + netuid: int, + param_name: str, + param_value: str, +): + """Set subnet hyperparameters.""" + console.print("\n") + await hyperparameters(subtensor, netuid=netuid) + + normalized_value: Union[str, bool] + if param_name in ["network_registration_allowed", "network_pow_registration_allowed", "commit_reveal_weights_enabled", + "liquid_alpha_enabled"]: + normalized_value = (param_value.lower() in ["true", "1"]) + else: + normalized_value = param_value + + is_allowed_value, value = allowed_value(param_name, normalized_value) + if not is_allowed_value: + err_console.print( + f"Hyperparameter {param_name} value is not within bounds. Value is {normalized_value} but must be {value}" + ) + return + + await set_hyperparameter_extrinsic( + subtensor, + wallet, + netuid, + param_name, + value + ) diff --git a/src/commands/wallets.py b/src/commands/wallets.py index b0d38291..50027df5 100644 --- a/src/commands/wallets.py +++ b/src/commands/wallets.py @@ -197,12 +197,11 @@ async def wallet_balance( coldkeys = [wallet.coldkeypub.ss58_address] wallet_names = [wallet.name] - async with subtensor: - await subtensor.substrate.get_chain_head() - free_balances, staked_balances = await asyncio.gather( - subtensor.get_balance(*coldkeys, reuse_block=True), - subtensor.get_total_stake_for_coldkey(*coldkeys, reuse_block=True), - ) + await subtensor.substrate.get_chain_head() + free_balances, staked_balances = await asyncio.gather( + subtensor.get_balance(*coldkeys, reuse_block=True), + subtensor.get_total_stake_for_coldkey(*coldkeys, reuse_block=True), + ) total_free_balance = sum(free_balances.values()) total_staked_balance = sum(staked_balances.values()) @@ -496,163 +495,162 @@ async def overview( f":satellite: Synchronizing with chain [white]{subtensor.network}[/white]", spinner="aesthetic", ): - async with subtensor: - # We are printing for every coldkey. - block_hash = await subtensor.substrate.get_chain_head() - all_hotkeys, total_balance = await _get_total_balance( - total_balance, subtensor, wallet, all_wallets - ) - - # We are printing for a select number of hotkeys from all_hotkeys. - if include_hotkeys: - all_hotkeys = _get_hotkeys( - include_hotkeys, exclude_hotkeys, all_hotkeys - ) - - # Check we have keys to display. - if not all_hotkeys: - err_console.print("[red]No wallets found.[/red]") - return + # We are printing for every coldkey. + block_hash = await subtensor.substrate.get_chain_head() + all_hotkeys, total_balance = await _get_total_balance( + total_balance, subtensor, wallet, all_wallets + ) - # Pull neuron info for all keys. - neurons: dict[str, list[NeuronInfoLite]] = {} - block, all_netuids = await asyncio.gather( - subtensor.substrate.get_block_number(None), - subtensor.get_all_subnet_netuids(), + # We are printing for a select number of hotkeys from all_hotkeys. + if include_hotkeys: + all_hotkeys = _get_hotkeys( + include_hotkeys, exclude_hotkeys, all_hotkeys ) - netuids = await subtensor.filter_netuids_by_registered_hotkeys( - all_netuids, netuids_filter, all_hotkeys, reuse_block=True - ) - # bittensor.logging.debug(f"Netuids to check: {netuids}") + # Check we have keys to display. + if not all_hotkeys: + err_console.print("[red]No wallets found.[/red]") + return - for netuid in netuids: - neurons[str(netuid)] = [] + # Pull neuron info for all keys. + neurons: dict[str, list[NeuronInfoLite]] = {} + block, all_netuids = await asyncio.gather( + subtensor.substrate.get_block_number(None), + subtensor.get_all_subnet_netuids(), + ) - all_wallet_names = {wallet.name for wallet in all_hotkeys} - all_coldkey_wallets = [ - Wallet(name=wallet_name) for wallet_name in all_wallet_names - ] + netuids = await subtensor.filter_netuids_by_registered_hotkeys( + all_netuids, netuids_filter, all_hotkeys, reuse_block=True + ) + # bittensor.logging.debug(f"Netuids to check: {netuids}") - all_hotkey_addresses, hotkey_coldkey_to_hotkey_wallet = _get_key_address( - all_hotkeys - ) + for netuid in netuids: + neurons[str(netuid)] = [] - # Pull neuron info for all keys.: + all_wallet_names = {wallet.name for wallet in all_hotkeys} + all_coldkey_wallets = [ + Wallet(name=wallet_name) for wallet_name in all_wallet_names + ] - results = await _get_neurons_for_netuids( - subtensor, netuids, all_hotkey_addresses - ) - neurons = _process_neuron_results(results, neurons, netuids) - total_coldkey_stake_from_metagraph = await _calculate_total_coldkey_stake( - neurons - ) + all_hotkey_addresses, hotkey_coldkey_to_hotkey_wallet = _get_key_address( + all_hotkeys + ) - alerts_table = Table(show_header=True, header_style="bold magenta") - alerts_table.add_column("🥩 alert!") - - coldkeys_to_check = [] - for coldkey_wallet in all_coldkey_wallets: - if coldkey_wallet.coldkeypub: - # Check if we have any stake with hotkeys that are not registered. - total_coldkey_stake_from_chain = ( - # TODO gathering here may make sense or may not - await subtensor.get_total_stake_for_coldkey( - coldkey_wallet.coldkeypub.ss58_address, reuse_block=True - ) - ) - difference = ( - total_coldkey_stake_from_chain[ - coldkey_wallet.coldkeypub.ss58_address - ] - - total_coldkey_stake_from_metagraph[ - coldkey_wallet.coldkeypub.ss58_address - ] - ) - if difference == 0: - continue # We have all our stake registered. - - coldkeys_to_check.append(coldkey_wallet) - alerts_table.add_row( - "Found {} stake with coldkey {} that is not registered.".format( - difference, coldkey_wallet.coldkeypub.ss58_address - ) - ) + # Pull neuron info for all keys.: - if coldkeys_to_check: - # We have some stake that is not with a registered hotkey. - if "-1" not in neurons: - neurons["-1"] = [] + results = await _get_neurons_for_netuids( + subtensor, netuids, all_hotkey_addresses + ) + neurons = _process_neuron_results(results, neurons, netuids) + total_coldkey_stake_from_metagraph = await _calculate_total_coldkey_stake( + neurons + ) - # Check each coldkey wallet for de-registered stake. + alerts_table = Table(show_header=True, header_style="bold magenta") + alerts_table.add_column("🥩 alert!") - results = await asyncio.gather( - *[ - _get_de_registered_stake_for_coldkey_wallet( - subtensor, all_hotkey_addresses, coldkey_wallet + coldkeys_to_check = [] + for coldkey_wallet in all_coldkey_wallets: + if coldkey_wallet.coldkeypub: + # Check if we have any stake with hotkeys that are not registered. + total_coldkey_stake_from_chain = ( + # TODO gathering here may make sense or may not + await subtensor.get_total_stake_for_coldkey( + coldkey_wallet.coldkeypub.ss58_address, reuse_block=True ) - for coldkey_wallet in coldkeys_to_check - ] - ) + ) + difference = ( + total_coldkey_stake_from_chain[ + coldkey_wallet.coldkeypub.ss58_address + ] + - total_coldkey_stake_from_metagraph[ + coldkey_wallet.coldkeypub.ss58_address + ] + ) + if difference == 0: + continue # We have all our stake registered. - for result in results: - coldkey_wallet, de_registered_stake, err_msg = result - if err_msg is not None: - err_console.print(err_msg) + coldkeys_to_check.append(coldkey_wallet) + alerts_table.add_row( + "Found {} stake with coldkey {} that is not registered.".format( + difference, coldkey_wallet.coldkeypub.ss58_address + ) + ) - if len(de_registered_stake) == 0: - continue # We have no de-registered stake with this coldkey. + if coldkeys_to_check: + # We have some stake that is not with a registered hotkey. + if "-1" not in neurons: + neurons["-1"] = [] - de_registered_neurons = [] - for hotkey_addr, our_stake in de_registered_stake: - # Make a neuron info lite for this hotkey and coldkey. - de_registered_neuron = NeuronInfoLite.get_null_neuron() - de_registered_neuron.hotkey = hotkey_addr - de_registered_neuron.coldkey = ( - coldkey_wallet.coldkeypub.ss58_address - ) - de_registered_neuron.total_stake = Balance(our_stake) - de_registered_neurons.append(de_registered_neuron) - - # Add this hotkey to the wallets dict - wallet_ = Wallet(name=wallet) - wallet_.hotkey_ss58 = hotkey_addr - wallet.hotkey_str = hotkey_addr[:5] # Max length of 5 characters - # Indicates a hotkey not on local machine but exists in stake_info obj on-chain - if hotkey_coldkey_to_hotkey_wallet.get(hotkey_addr) is None: - hotkey_coldkey_to_hotkey_wallet[hotkey_addr] = {} - hotkey_coldkey_to_hotkey_wallet[hotkey_addr][ - coldkey_wallet.coldkeypub.ss58_address - ] = wallet_ + # Check each coldkey wallet for de-registered stake. - # Add neurons to overview. - neurons["-1"].extend(de_registered_neurons) + results = await asyncio.gather( + *[ + _get_de_registered_stake_for_coldkey_wallet( + subtensor, all_hotkey_addresses, coldkey_wallet + ) + for coldkey_wallet in coldkeys_to_check + ] + ) - # Setup outer table. - grid = Table.grid(pad_edge=False) + for result in results: + coldkey_wallet, de_registered_stake, err_msg = result + if err_msg is not None: + err_console.print(err_msg) + + if len(de_registered_stake) == 0: + continue # We have no de-registered stake with this coldkey. + + de_registered_neurons = [] + for hotkey_addr, our_stake in de_registered_stake: + # Make a neuron info lite for this hotkey and coldkey. + de_registered_neuron = NeuronInfoLite.get_null_neuron() + de_registered_neuron.hotkey = hotkey_addr + de_registered_neuron.coldkey = ( + coldkey_wallet.coldkeypub.ss58_address + ) + de_registered_neuron.total_stake = Balance(our_stake) + de_registered_neurons.append(de_registered_neuron) + + # Add this hotkey to the wallets dict + wallet_ = Wallet(name=wallet) + wallet_.hotkey_ss58 = hotkey_addr + wallet.hotkey_str = hotkey_addr[:5] # Max length of 5 characters + # Indicates a hotkey not on local machine but exists in stake_info obj on-chain + if hotkey_coldkey_to_hotkey_wallet.get(hotkey_addr) is None: + hotkey_coldkey_to_hotkey_wallet[hotkey_addr] = {} + hotkey_coldkey_to_hotkey_wallet[hotkey_addr][ + coldkey_wallet.coldkeypub.ss58_address + ] = wallet_ - # If there are any alerts, add them to the grid - if len(alerts_table.rows) > 0: - grid.add_row(alerts_table) + # Add neurons to overview. + neurons["-1"].extend(de_registered_neurons) - if not all_wallets: - title = f"[bold white italic]Wallet - {wallet.name}:{wallet.coldkeypub.ss58_address}" - else: - title = "[bold whit italic]All Wallets:" + # Setup outer table. + grid = Table.grid(pad_edge=False) - # Add title - grid.add_row(Align(title, vertical="middle", align="center")) + # If there are any alerts, add them to the grid + if len(alerts_table.rows) > 0: + grid.add_row(alerts_table) - # Generate rows per netuid - hotkeys_seen = set() - total_neurons = 0 - total_stake = 0.0 - tempos = await asyncio.gather( - *[ - subtensor.get_hyperparameter("Tempo", netuid, block_hash) - for netuid in netuids - ] - ) + if not all_wallets: + title = f"[bold white italic]Wallet - {wallet.name}:{wallet.coldkeypub.ss58_address}" + else: + title = "[bold whit italic]All Wallets:" + + # Add title + grid.add_row(Align(title, vertical="middle", align="center")) + + # Generate rows per netuid + hotkeys_seen = set() + total_neurons = 0 + total_stake = 0.0 + tempos = await asyncio.gather( + *[ + subtensor.get_hyperparameter("Tempo", netuid, block_hash) + for netuid in netuids + ] + ) for netuid, subnet_tempo in zip(netuids, tempos): last_subnet = netuid == netuids[-1] table_data = [] @@ -1018,16 +1016,15 @@ async def neurons_lite_for_uid(uid: int) -> dict[Any, Any]: call_definition = TYPE_REGISTRY["runtime_api"]["NeuronInfoRuntimeApi"][ "methods" ]["get_neurons_lite"] - async with subtensor: - data = await subtensor.encode_params( - call_definition=call_definition, params=[uid] - ) - block_hash = subtensor.substrate.last_block_hash - hex_bytes_result = await subtensor.substrate.rpc_request( - method="state_call", - params=["NeuronInfoRuntimeApi_get_neurons_lite", data, block_hash], - reuse_block_hash=True, - ) + data = await subtensor.encode_params( + call_definition=call_definition, params=[uid] + ) + block_hash = subtensor.substrate.last_block_hash + hex_bytes_result = await subtensor.substrate.rpc_request( + method="state_call", + params=["NeuronInfoRuntimeApi_get_neurons_lite", data, block_hash], + reuse_block_hash=True, + ) return hex_bytes_result @@ -1159,10 +1156,9 @@ async def transfer( wallet: Wallet, subtensor: SubtensorInterface, destination: str, amount: float ): """Transfer token of amount to destination.""" - async with subtensor: - await transfer_extrinsic( - subtensor, wallet, destination, Balance.from_tao(amount), prompt=True - ) + await transfer_extrinsic( + subtensor, wallet, destination, Balance.from_tao(amount), prompt=True + ) async def inspect( @@ -1217,15 +1213,14 @@ def neuron_row_maker(wallet_, all_netuids_, nsd) -> Generator[list[str]]: wallets = [wallet] all_hotkeys = get_hotkey_wallets_for_wallet(wallet) with console.status("synchronising with chain"): - async with subtensor: - block_hash = await subtensor.substrate.get_chain_head() - await subtensor.substrate.init_runtime(block_hash=block_hash) - all_netuids = await subtensor.filter_netuids_by_registered_hotkeys( - (await subtensor.get_all_subnet_netuids(block_hash)), - netuids_filter, - all_hotkeys, - block_hash=block_hash, - ) + block_hash = await subtensor.substrate.get_chain_head() + await subtensor.substrate.init_runtime(block_hash=block_hash) + all_netuids = await subtensor.filter_netuids_by_registered_hotkeys( + (await subtensor.get_all_subnet_netuids(block_hash)), + netuids_filter, + all_hotkeys, + block_hash=block_hash, + ) # bittensor.logging.debug(f"Netuids to check: {all_netuids}") with console.status("Pulling delegates info"): registered_delegate_info: dict[ @@ -1258,25 +1253,24 @@ def neuron_row_maker(wallet_, all_netuids_, nsd) -> Generator[list[str]]: ] all_delegates: list[list[tuple[DelegateInfo, Balance]]] with console.status("Pulling balance data"): - async with subtensor: - balances, all_neurons, all_delegates = await asyncio.gather( - subtensor.get_balance( - *[w.coldkeypub.ss58_address for w in wallets_with_ckp_file], - block_hash=block_hash, - ), - asyncio.gather( - *[ - subtensor.neurons_lite(netuid=netuid, block_hash=block_hash) - for netuid in all_netuids - ] - ), - asyncio.gather( - *[ - subtensor.get_delegated(w.coldkeypub.ss58_address) - for w in wallets_with_ckp_file - ] - ), - ) + balances, all_neurons, all_delegates = await asyncio.gather( + subtensor.get_balance( + *[w.coldkeypub.ss58_address for w in wallets_with_ckp_file], + block_hash=block_hash, + ), + asyncio.gather( + *[ + subtensor.neurons_lite(netuid=netuid, block_hash=block_hash) + for netuid in all_netuids + ] + ), + asyncio.gather( + *[ + subtensor.get_delegated(w.coldkeypub.ss58_address) + for w in wallets_with_ckp_file + ] + ), + ) neuron_state_dict = {} for netuid, neuron in zip(all_netuids, all_neurons): @@ -1308,20 +1302,19 @@ async def faucet( log_verbose: bool, max_successes: int = 3, ): - async with subtensor: - success = await run_faucet_extrinsic( - subtensor, - wallet, - tpb=threads_per_block, - prompt=True, - update_interval=update_interval, - num_processes=processes, - cuda=use_cuda, - dev_id=dev_id, - output_in_place=output_in_place, - log_verbose=log_verbose, - max_successes=max_successes, - ) + success = await run_faucet_extrinsic( + subtensor, + wallet, + tpb=threads_per_block, + prompt=True, + update_interval=update_interval, + num_processes=processes, + cuda=use_cuda, + dev_id=dev_id, + output_in_place=output_in_place, + log_verbose=log_verbose, + max_successes=max_successes, + ) if not success: err_console.print("Faucet run failed.") @@ -1330,15 +1323,14 @@ async def swap_hotkey( original_wallet: Wallet, new_wallet: Wallet, subtensor: SubtensorInterface ): """Swap your hotkey for all registered axons on the network.""" - async with subtensor: - return await swap_hotkey_extrinsic( - subtensor, - original_wallet, - new_wallet, - wait_for_finalization=False, - wait_for_inclusion=True, - prompt=True, - ) + return await swap_hotkey_extrinsic( + subtensor, + original_wallet, + new_wallet, + wait_for_finalization=False, + wait_for_inclusion=True, + prompt=True, + ) def set_id_prompts() -> tuple[str, str, str, str, str, str, str, str, str, bool]: @@ -1422,22 +1414,21 @@ async def set_id( wallet.unlock_coldkey() with console.status(":satellite: [bold green]Updating identity on-chain..."): - async with subtensor: - call = await subtensor.substrate.compose_call( - call_module="Registry", - call_function="set_identity", - call_params=id_dict, - ) - success, err_msg = await subtensor.sign_and_send_extrinsic(call, wallet) + call = await subtensor.substrate.compose_call( + call_module="Registry", + call_function="set_identity", + call_params=id_dict, + ) + success, err_msg = await subtensor.sign_and_send_extrinsic(call, wallet) - if not success: - err_console.print(f"[red]:cross_mark: Failed![/red] {err_msg}") - return + if not success: + err_console.print(f"[red]:cross_mark: Failed![/red] {err_msg}") + return - console.print(":white_heavy_check_mark: Success!") - identity = await subtensor.query_identity( - identified or wallet.coldkey.ss58_address - ) + console.print(":white_heavy_check_mark: Success!") + identity = await subtensor.query_identity( + identified or wallet.coldkey.ss58_address + ) table = Table( Column("Key", justify="right", style="cyan", no_wrap=True), @@ -1454,8 +1445,7 @@ async def set_id( async def get_id(subtensor: SubtensorInterface, ss58_address: str): with console.status(":satellite: [bold green]Querying chain identity..."): - async with subtensor: - identity = await subtensor.query_identity(ss58_address) + identity = await subtensor.query_identity(ss58_address) table = Table( Column("Item", justify="right", style="cyan", no_wrap=True), @@ -1471,39 +1461,38 @@ async def get_id(subtensor: SubtensorInterface, ss58_address: str): async def check_coldkey_swap(wallet: Wallet, subtensor: SubtensorInterface): - async with subtensor: - arbitration_check = len( - ( - await subtensor.substrate.query( - module="SubtensorModule", - storage_function="ColdkeySwapDestinations", - params=[wallet.coldkeypub.ss58_address], - ) - ).decode() - ) - if arbitration_check == 0: - console.print( - "[green]There has been no previous key swap initiated for your coldkey.[/green]" - ) - elif arbitration_check == 1: - arbitration_block = await subtensor.substrate.query( + arbitration_check = len( + ( + await subtensor.substrate.query( module="SubtensorModule", - storage_function="ColdkeyArbitrationBlock", + storage_function="ColdkeySwapDestinations", params=[wallet.coldkeypub.ss58_address], ) - arbitration_remaining = ( - arbitration_block.value - - await subtensor.substrate.get_block_number(None) - ) + ).decode() + ) + if arbitration_check == 0: + console.print( + "[green]There has been no previous key swap initiated for your coldkey.[/green]" + ) + elif arbitration_check == 1: + arbitration_block = await subtensor.substrate.query( + module="SubtensorModule", + storage_function="ColdkeyArbitrationBlock", + params=[wallet.coldkeypub.ss58_address], + ) + arbitration_remaining = ( + arbitration_block.value + - await subtensor.substrate.get_block_number(None) + ) - hours, minutes, seconds = convert_blocks_to_time(arbitration_remaining) - console.print( - "[yellow]There has been 1 swap request made for this coldkey already." - " By adding another swap request, the key will enter arbitration." - f" Your key swap is scheduled for {hours} hours, {minutes} minutes, {seconds} seconds" - " from now.[/yellow]" - ) - elif arbitration_check > 1: - console.print( - f"[red]This coldkey is currently in arbitration with a total swaps of {arbitration_check}.[/red]" - ) + hours, minutes, seconds = convert_blocks_to_time(arbitration_remaining) + console.print( + "[yellow]There has been 1 swap request made for this coldkey already." + " By adding another swap request, the key will enter arbitration." + f" Your key swap is scheduled for {hours} hours, {minutes} minutes, {seconds} seconds" + " from now.[/yellow]" + ) + elif arbitration_check > 1: + console.print( + f"[red]This coldkey is currently in arbitration with a total swaps of {arbitration_check}.[/red]" + ) diff --git a/src/subtensor_interface.py b/src/subtensor_interface.py index 936733c6..772e1cd8 100644 --- a/src/subtensor_interface.py +++ b/src/subtensor_interface.py @@ -15,7 +15,7 @@ custom_rpc_type_registry, StakeInfo, NeuronInfoLite, - NeuronInfo, + NeuronInfo, SubnetHyperparameters, ) from src.bittensor.balances import Balance from src import Constants, defaults, TYPE_REGISTRY @@ -813,3 +813,35 @@ async def get_children(self, hotkey, netuid) -> tuple[bool, list, str]: return True, [], "" except SubstrateRequestException as e: return False, [], str(e) + + async def get_subnet_hyperparameters( + self, netuid: int, block_hash: Optional[str] = None + ) -> Optional[Union[list, SubnetHyperparameters]]: + """ + Retrieves the hyperparameters for a specific subnet within the Bittensor network. These hyperparameters + define the operational settings and rules governing the subnet's behavior. + + :param netuid: The network UID of the subnet to query. + :param block_hash: The hash of the blockchain block number for the query. + + :return: The subnet's hyperparameters, or `None` if not available. + + Understanding the hyperparameters is crucial for comprehending how subnets are configured and + managed, and how they interact with the network's consensus and incentive mechanisms. + """ + hex_bytes_result = await self.query_runtime_api( + runtime_api="SubnetInfoRuntimeApi", + method="get_subnet_hyperparams", + params=[netuid], + block_hash=block_hash, + ) + + if hex_bytes_result is None: + return [] + + if hex_bytes_result.startswith("0x"): + bytes_result = bytes.fromhex(hex_bytes_result[2:]) + else: + bytes_result = bytes.fromhex(hex_bytes_result) + + return SubnetHyperparameters.from_vec_u8(bytes_result) # type: ignore diff --git a/src/utils.py b/src/utils.py index b0837408..11e34fb0 100644 --- a/src/utils.py +++ b/src/utils.py @@ -1,7 +1,7 @@ import os import math from pathlib import Path -from typing import Union, Any, Collection, Optional +from typing import Union, Any, Collection, Optional, TYPE_CHECKING import aiohttp import scalecodec @@ -15,6 +15,11 @@ from scalecodec.type_registry import load_type_registry_preset from src import DelegatesDetails +from src.bittensor.balances import Balance + +if TYPE_CHECKING: + from src.bittensor.chain_data import SubnetHyperparameters + console = Console() err_console = Console(stderr=True) @@ -30,6 +35,11 @@ def u16_normalized_float(x: int) -> float: return float(x) / float(U16_MAX) +def u64_normalized_float(x: int) -> float: + """Converts a u64 int to a float""" + return float(x) / float(U64_MAX) + + def float_to_u64(value: float) -> int: """Converts a float to a u64 int""" # Ensure the input is within the expected range @@ -448,3 +458,47 @@ def millify(n: int): ) return "{:.2f}{}".format(n_ / 10 ** (3 * mill_idx), mill_names[mill_idx]) + + +def normalize_hyperparameters( + subnet: "SubnetHyperparameters", +) -> list[tuple[str, str, str]]: + """ + Normalizes the hyperparameters of a subnet. + + :param subnet: The subnet hyperparameters object. + + :return: A list of tuples containing the parameter name, value, and normalized value. + """ + param_mappings = { + "adjustment_alpha": u64_normalized_float, + "min_difficulty": u64_normalized_float, + "max_difficulty": u64_normalized_float, + "difficulty": u64_normalized_float, + "bonds_moving_avg": u64_normalized_float, + "max_weight_limit": u16_normalized_float, + "kappa": u16_normalized_float, + "alpha_high": u16_normalized_float, + "alpha_low": u16_normalized_float, + "min_burn": Balance.from_rao, + "max_burn": Balance.from_rao, + } + + normalized_values: list[tuple[str, str, str]] = [] + subnet_dict = subnet.__dict__ + + for param, value in subnet_dict.items(): + try: + if param in param_mappings: + norm_value = param_mappings[param](value) + if isinstance(norm_value, float): + norm_value = f"{norm_value:.{10}g}" + else: + norm_value = value + except Exception as e: + # bittensor.logging.warning(f"Error normalizing parameter '{param}': {e}") + norm_value = "-" + + normalized_values.append((param, str(value), str(norm_value))) + + return normalized_values From fe93fb36ea8dad2ce120f34a5caa929c64794485 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Wed, 14 Aug 2024 22:39:25 +0200 Subject: [PATCH 2/4] `sudo get` command --- cli.py | 99 ++++++++++++++++++++----------------- src/bittensor/chain_data.py | 1 + src/commands/root.py | 16 +++--- src/commands/stake.py | 17 ++----- src/commands/subnets.py | 34 ------------- src/commands/sudo.py | 89 ++++++++++++++++++++++----------- src/commands/wallets.py | 11 ++--- src/subtensor_interface.py | 3 +- 8 files changed, 129 insertions(+), 141 deletions(-) delete mode 100644 src/commands/subnets.py diff --git a/cli.py b/cli.py index 496b457d..a4b30940 100755 --- a/cli.py +++ b/cli.py @@ -279,6 +279,10 @@ def __init__(self): # sudo commands self.sudo_app.command("set")(self.sudo_set) + self.sudo_app.command("get")(self.sudo_get) + + # subnets commands + # self.subnets_app.command("hyperparameters")(self.sudo_get) def initialize_chain( self, @@ -310,12 +314,14 @@ def _run_command(self, cmd: Coroutine) -> None: """ Runs the supplied coroutine with asyncio.run """ + async def _run(): if self.not_subtensor: async with self.not_subtensor: await cmd else: await cmd + try: return asyncio.run(_run()) except ConnectionRefusedError: @@ -2619,24 +2625,21 @@ def stake_revoke_children( ) ) - def sudo_set(self, - network: Optional[str] = Options.network, - chain: Optional[str] = Options.chain, - wallet_name: str = Options.wallet_name, - wallet_path: str = Options.wallet_path, - wallet_hotkey: str = Options.wallet_hotkey, - netuid: int = Options.netuid, - param_name: str = typer.Option( - "", - "--param", "--parameter", - help="The subnet hyperparameter to set" - ), - param_value: str = typer.Option( - "", - "--value", - help="The subnet hyperparameter value to set." - ) - ): + def sudo_set( + self, + network: Optional[str] = Options.network, + chain: Optional[str] = Options.chain, + wallet_name: str = Options.wallet_name, + wallet_path: str = Options.wallet_path, + wallet_hotkey: str = Options.wallet_hotkey, + netuid: int = Options.netuid, + param_name: str = typer.Option( + "", "--param", "--parameter", help="The subnet hyperparameter to set" + ), + param_value: str = typer.Option( + "", "--value", help="The subnet hyperparameter value to set." + ), + ): """ # sudo set Executes the `set` command to set hyperparameters for a specific subnet on the Bittensor network. @@ -2661,40 +2664,44 @@ def sudo_set(self, and the impact of changing these parameters. """ if not param_name: - param_name = Prompt.ask("Enter hyperparameter", choices=list(HYPERPARAMS.keys())) + param_name = Prompt.ask( + "Enter hyperparameter", choices=list(HYPERPARAMS.keys()) + ) if not param_value: param_value = Prompt.ask(f"Enter new value for {param_name}") wallet = self.wallet_ask(wallet_name, wallet_path, wallet_hotkey) - return self._run_command(sudo.sudo_set_hyperparameter( - wallet, - self.initialize_chain(network, chain), - netuid, - param_name, - param_value - )) - - def subnets_get_hyperparameters(self, - network: str = Options.network, - chain: str = Options.chain, - netuid: int = Options.netuid - ): - """ - # subnets get-hyperparameters - Executes the `hyperparameters` command to view the current hyperparameters of a specific subnet on the Bittensor - network. + return self._run_command( + sudo.sudo_set_hyperparameter( + wallet, + self.initialize_chain(network, chain), + netuid, + param_name, + param_value, + ) + ) + + def sudo_get( + self, + network: str = Options.network, + chain: str = Options.chain, + netuid: int = Options.netuid, + ): + """ + # sudo get + Executes the `get` command to retrieve the hyperparameters of a specific subnet on the Bittensor network. - This command is useful for users who wish to understand the configuration and - operational parameters of a particular subnet. + This command is used for both `sudo get` and `subnets hyperparameters`. ## Usage: - Upon invocation, the command fetches and displays a list of all hyperparameters for the specified subnet. - These include settings like tempo, emission rates, and other critical network parameters that define - the subnet's behavior. + The command connects to the Bittensor network, queries the specified subnet, and returns a detailed list + of all its hyperparameters. This includes crucial operational parameters that determine the subnet's + performance and interaction within the network. ### Example usage: ``` - $ btcli subnets hyperparameters --netuid 1 + + $ btcli sudo get --netuid 1 @@ -2738,13 +2745,15 @@ def subnets_get_hyperparameters(self, max_regs_per_block 1 - ``` #### Note: - The user must specify the subnet identifier (`netuid`) for which they want to view the hyperparameters. - This command is read-only and does not modify the network state or configurations. + Users need to provide the `netuid` of the subnet whose hyperparameters they wish to view. This command is + designed for informational purposes and does not alter any network settings or configurations. """ + return self._run_command( + sudo.get_hyperparameters(self.initialize_chain(network, chain), netuid) + ) def run(self): self.app() diff --git a/src/bittensor/chain_data.py b/src/bittensor/chain_data.py index a8a20264..0ed57293 100644 --- a/src/bittensor/chain_data.py +++ b/src/bittensor/chain_data.py @@ -109,6 +109,7 @@ def from_neuron_info(cls, neuron_info: dict) -> "AxonInfo": coldkey=neuron_info["coldkey"], ) + @dataclass class SubnetHyperparameters: """Dataclass for subnet hyperparameters.""" diff --git a/src/commands/root.py b/src/commands/root.py index 760d8e05..ade07626 100644 --- a/src/commands/root.py +++ b/src/commands/root.py @@ -700,9 +700,7 @@ async def _get_list() -> tuple: storage_function="Members", params=None, ) - sm = ( - senate_query.serialize() if hasattr(senate_query, "serialize") else None - ) + sm = senate_query.serialize() if hasattr(senate_query, "serialize") else None rn: list[NeuronInfoLite] = await subtensor.neurons_lite(netuid=0) if not rn: @@ -964,9 +962,7 @@ async def senate_vote( ) return False - if not await _is_senate_member( - subtensor, hotkey_ss58=wallet.hotkey.ss58_address - ): + if not await _is_senate_member(subtensor, hotkey_ss58=wallet.hotkey.ss58_address): err_console.print( f"Aborting: Hotkey {wallet.hotkey.ss58_address} isn't a senate member." ) @@ -998,7 +994,9 @@ async def senate_vote( async def get_senate(subtensor: SubtensorInterface): """View Bittensor's governance protocol proposals""" - with console.status(f":satellite: Syncing with chain: [white]{subtensor}[/white] ..."): + with console.status( + f":satellite: Syncing with chain: [white]{subtensor}[/white] ..." + ): senate_members = await _get_senate_members(subtensor) delegate_info: dict[ @@ -1399,9 +1397,7 @@ async def list_delegates(subtensor: SubtensorInterface): prev_block_hash = await subtensor.substrate.get_block_hash( max(0, block_number - 1200) ) - prev_delegates = await subtensor.get_delegates( - block_hash=prev_block_hash - ) + prev_delegates = await subtensor.get_delegates(block_hash=prev_block_hash) except SubstrateRequestException: prev_delegates = None diff --git a/src/commands/stake.py b/src/commands/stake.py index dfaca126..9241fab4 100644 --- a/src/commands/stake.py +++ b/src/commands/stake.py @@ -1198,10 +1198,7 @@ async def is_hotkey_registered_any(hk: str, bh: str) -> bool: final_amounts: list[Union[float, Balance]] = [] hotkey: tuple[Optional[str], str] # (hotkey_name (or None), hotkey_ss58) registered_ = asyncio.gather( - *[ - is_hotkey_registered_any(h[1], block_hash) - for h in hotkeys_to_stake_to - ] + *[is_hotkey_registered_any(h[1], block_hash) for h in hotkeys_to_stake_to] ) if max_stake: hotkey_stakes_ = asyncio.gather( @@ -1222,9 +1219,7 @@ async def null(): hotkey_stakes_ = null() registered: list[bool] hotkey_stakes: list[Optional[Balance]] - registered, hotkey_stakes = await asyncio.gather( - registered_, hotkey_stakes_ - ) + registered, hotkey_stakes = await asyncio.gather(registered_, hotkey_stakes_) for hotkey, reg, hotkey_stake in zip( hotkeys_to_stake_to, registered, hotkey_stakes @@ -1255,9 +1250,7 @@ async def null(): ): # Threshold because of fees, might create a loop otherwise # Skip hotkey if max_stake is less than current stake. continue - wallet_balance = Balance.from_tao( - wallet_balance.tao - stake_amount_tao - ) + wallet_balance = Balance.from_tao(wallet_balance.tao - stake_amount_tao) if wallet_balance.tao < 0: # No more balance to stake. @@ -1617,9 +1610,7 @@ async def revoke_children( # Validate children SS58 addresses for child in current_children: if not is_valid_ss58_address(child): - err_console.print( - f":cross_mark:[red] Invalid SS58 address: {child}[/red]" - ) + err_console.print(f":cross_mark:[red] Invalid SS58 address: {child}[/red]") return # Prepare children with zero proportions diff --git a/src/commands/subnets.py b/src/commands/subnets.py deleted file mode 100644 index 1b676d3a..00000000 --- a/src/commands/subnets.py +++ /dev/null @@ -1,34 +0,0 @@ -from typing import TYPE_CHECKING - -from rich.table import Table, Column - -from src.utils import console, normalize_hyperparameters - -if TYPE_CHECKING: - from src.subtensor_interface import SubtensorInterface - - -async def hyperparameters(subtensor: "SubtensorInterface", netuid: int): - """View hyperparameters of a subnetwork.""" - subnet = await subtensor.get_subnet_hyperparameters( - netuid - ) - - table = Table( - Column("[overline white]HYPERPARAMETER", style="white"), - Column("[overline white]VALUE", style="green"), - Column("[overline white]NORMALIZED", style="cyan"), - title=f"[white]Subnet Hyperparameters - NETUID: {netuid} - {subtensor}", - show_footer=True, - width=None, - pad_edge=True, - box=None, - show_edge=True, - ) - - normalized_values = normalize_hyperparameters(subnet) - - for param, value, norm_value in normalized_values: - table.add_row(" " + param, value, norm_value) - - console.print(table) diff --git a/src/commands/sudo.py b/src/commands/sudo.py index c8cd36aa..baae285f 100644 --- a/src/commands/sudo.py +++ b/src/commands/sudo.py @@ -2,10 +2,10 @@ from typing import TYPE_CHECKING, Union from bittensor_wallet import Wallet +from rich.table import Table, Column from src import HYPERPARAMS -from src.commands.subnets import hyperparameters -from src.utils import console, err_console +from src.utils import console, err_console, normalize_hyperparameters if TYPE_CHECKING: from src.subtensor_interface import SubtensorInterface @@ -13,6 +13,7 @@ # helpers and extrinsics + def allowed_value( param: str, value: Union[str, bool] ) -> tuple[bool, Union[str, list[float], float, bool]]: @@ -78,11 +79,15 @@ async def set_hyperparameter_extrinsic( :return: success: `True` if extrinsic was finalized or included in the block. If we did not wait for finalization/inclusion, the response is `True`. """ - subnet_owner = getattr(await subtensor.substrate.query( - module="SubtensorModule", - storage_function="SubnetOwner", - params=[netuid], - ), "value", None) + subnet_owner = getattr( + await subtensor.substrate.query( + module="SubtensorModule", + storage_function="SubnetOwner", + params=[netuid], + ), + "value", + None, + ) if subnet_owner != wallet.coldkeypub.ss58_address: err_console.print( ":cross_mark: [red]This wallet doesn't own the specified subnet.[/red]" @@ -93,9 +98,7 @@ async def set_hyperparameter_extrinsic( extrinsic = HYPERPARAMS.get(parameter) if extrinsic is None: - err_console.print( - ":cross_mark: [red]Invalid hyperparameter specified.[/red]" - ) + err_console.print(":cross_mark: [red]Invalid hyperparameter specified.[/red]") return False with console.status( @@ -110,13 +113,20 @@ async def set_hyperparameter_extrinsic( # if input value is a list, iterate through the list and assign values if isinstance(value, list): # Ensure that there are enough values for all non-netuid parameters - non_netuid_fields = [param["name"] for param in extrinsic_params["fields"] if - "netuid" not in param["name"]] + non_netuid_fields = [ + param["name"] + for param in extrinsic_params["fields"] + if "netuid" not in param["name"] + ] if len(value) < len(non_netuid_fields): - raise ValueError("Not enough values provided in the list for all parameters") + raise ValueError( + "Not enough values provided in the list for all parameters" + ) - call_params.update({str(name): val for name, val in zip(non_netuid_fields, value)}) + call_params.update( + {str(name): val for name, val in zip(non_netuid_fields, value)} + ) else: value_argument = extrinsic_params["fields"][ @@ -131,14 +141,10 @@ async def set_hyperparameter_extrinsic( call_params=call_params, ) success, err_msg = await subtensor.sign_and_send_extrinsic( - call, - wallet, - wait_for_inclusion, wait_for_finalization + call, wallet, wait_for_inclusion, wait_for_finalization ) if not success: - err_console.print( - f":cross_mark: [red]Failed[/red]: {err_msg}" - ) + err_console.print(f":cross_mark: [red]Failed[/red]: {err_msg}") await asyncio.sleep(0.5) # Successful registration, final check for membership @@ -151,6 +157,7 @@ async def set_hyperparameter_extrinsic( # commands + async def sudo_set_hyperparameter( wallet: Wallet, subtensor: "SubtensorInterface", @@ -160,12 +167,16 @@ async def sudo_set_hyperparameter( ): """Set subnet hyperparameters.""" console.print("\n") - await hyperparameters(subtensor, netuid=netuid) + await get_hyperparameters(subtensor, netuid=netuid) normalized_value: Union[str, bool] - if param_name in ["network_registration_allowed", "network_pow_registration_allowed", "commit_reveal_weights_enabled", - "liquid_alpha_enabled"]: - normalized_value = (param_value.lower() in ["true", "1"]) + if param_name in [ + "network_registration_allowed", + "network_pow_registration_allowed", + "commit_reveal_weights_enabled", + "liquid_alpha_enabled", + ]: + normalized_value = param_value.lower() in ["true", "1"] else: normalized_value = param_value @@ -176,10 +187,28 @@ async def sudo_set_hyperparameter( ) return - await set_hyperparameter_extrinsic( - subtensor, - wallet, - netuid, - param_name, - value + await set_hyperparameter_extrinsic(subtensor, wallet, netuid, param_name, value) + + +async def get_hyperparameters(subtensor: "SubtensorInterface", netuid: int): + """View hyperparameters of a subnetwork.""" + subnet = await subtensor.get_subnet_hyperparameters(netuid) + + table = Table( + Column("[overline white]HYPERPARAMETER", style="white"), + Column("[overline white]VALUE", style="green"), + Column("[overline white]NORMALIZED", style="cyan"), + title=f"[white]Subnet Hyperparameters - NETUID: {netuid} - {subtensor}", + show_footer=True, + width=None, + pad_edge=True, + box=None, + show_edge=True, ) + + normalized_values = normalize_hyperparameters(subnet) + + for param, value, norm_value in normalized_values: + table.add_row(" " + param, value, norm_value) + + console.print(table) diff --git a/src/commands/wallets.py b/src/commands/wallets.py index 50027df5..232a64db 100644 --- a/src/commands/wallets.py +++ b/src/commands/wallets.py @@ -503,9 +503,7 @@ async def overview( # We are printing for a select number of hotkeys from all_hotkeys. if include_hotkeys: - all_hotkeys = _get_hotkeys( - include_hotkeys, exclude_hotkeys, all_hotkeys - ) + all_hotkeys = _get_hotkeys(include_hotkeys, exclude_hotkeys, all_hotkeys) # Check we have keys to display. if not all_hotkeys: @@ -606,9 +604,7 @@ async def overview( # Make a neuron info lite for this hotkey and coldkey. de_registered_neuron = NeuronInfoLite.get_null_neuron() de_registered_neuron.hotkey = hotkey_addr - de_registered_neuron.coldkey = ( - coldkey_wallet.coldkeypub.ss58_address - ) + de_registered_neuron.coldkey = coldkey_wallet.coldkeypub.ss58_address de_registered_neuron.total_stake = Balance(our_stake) de_registered_neurons.append(de_registered_neuron) @@ -1481,8 +1477,7 @@ async def check_coldkey_swap(wallet: Wallet, subtensor: SubtensorInterface): params=[wallet.coldkeypub.ss58_address], ) arbitration_remaining = ( - arbitration_block.value - - await subtensor.substrate.get_block_number(None) + arbitration_block.value - await subtensor.substrate.get_block_number(None) ) hours, minutes, seconds = convert_blocks_to_time(arbitration_remaining) diff --git a/src/subtensor_interface.py b/src/subtensor_interface.py index 772e1cd8..59850a7e 100644 --- a/src/subtensor_interface.py +++ b/src/subtensor_interface.py @@ -15,7 +15,8 @@ custom_rpc_type_registry, StakeInfo, NeuronInfoLite, - NeuronInfo, SubnetHyperparameters, + NeuronInfo, + SubnetHyperparameters, ) from src.bittensor.balances import Balance from src import Constants, defaults, TYPE_REGISTRY From 72adb53a5c2702d6c318b65ef6e27a791b91500b Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Wed, 14 Aug 2024 22:48:12 +0200 Subject: [PATCH 3/4] Gus PR review --- src/commands/root.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/commands/root.py b/src/commands/root.py index ade07626..ec670978 100644 --- a/src/commands/root.py +++ b/src/commands/root.py @@ -1064,16 +1064,16 @@ async def register(wallet: Wallet, subtensor: SubtensorInterface, netuid: int): ) return False - # if not cli.config.no_prompt: - if not ( - Confirm.ask( - f"Your balance is: [bold green]{balance}[/bold green]\n" - f"The cost to register by recycle is [bold red]{current_recycle}[/bold red]\n" - f"Do you want to continue?", - default=False, - ) - ): - return False + if not False: # TODO no-prompt + if not ( + Confirm.ask( + f"Your balance is: [bold green]{balance}[/bold green]\n" + f"The cost to register by recycle is [bold red]{current_recycle}[/bold red]\n" + f"Do you want to continue?", + default=False, + ) + ): + return False await burned_register_extrinsic( subtensor, From 1ced208dd8fac0ee6c2b9ea79488bd0eda098c9e Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Wed, 14 Aug 2024 22:52:34 +0200 Subject: [PATCH 4/4] Gus PR review --- src/commands/stake.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/commands/stake.py b/src/commands/stake.py index 9241fab4..acc241db 100644 --- a/src/commands/stake.py +++ b/src/commands/stake.py @@ -1524,11 +1524,8 @@ async def render_table( success, children, err_mg = await subtensor.get_children(wallet.hotkey, netuid) if not success: err_console.print( - f"Failed to get children from subtensor. {children[0]}: {err_mg}" + f"Failed to get children from subtensor: {err_mg}" ) - if not children: - console.print("[yellow]No children found.[/yellow]") - await render_table(wallet.hotkey, children, netuid) return children