diff --git a/bittensor/commands/stake.py b/bittensor/commands/stake.py index 132529a131..eff415d1a1 100644 --- a/bittensor/commands/stake.py +++ b/bittensor/commands/stake.py @@ -44,19 +44,25 @@ def get_netuid( - cli: "bittensor.cli", subtensor: "bittensor.subtensor" + cli: "bittensor.cli", subtensor: "bittensor.subtensor", prompt: bool = True ) -> Tuple[bool, int]: """Retrieve and validate the netuid from the user or configuration.""" console = Console() - if not cli.config.is_set("netuid"): - try: - cli.config.netuid = int(Prompt.ask("Enter netuid")) - except ValueError: - console.print( - "[red]Invalid input. Please enter a valid integer for netuid.[/red]" - ) - return False, -1 + if not cli.config.is_set("netuid") and prompt: + cli.config.netuid = Prompt.ask("Enter netuid") + try: + cli.config.netuid = int(cli.config.netuid) + except ValueError: + console.print( + "[red]Invalid input. Please enter a valid integer for netuid.[/red]" + ) + return False, -1 netuid = cli.config.netuid + if netuid < 0 or netuid > 65535: + console.print( + "[red]Invalid input. Please enter a valid integer for netuid in subnet range.[/red]" + ) + return False, -1 if not subtensor.subnet_exists(netuid=netuid): console.print( "[red]Network with netuid {} does not exist. Please try again.[/red]".format( @@ -1136,10 +1142,27 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): wallet = bittensor.wallet(config=cli.config) # check all - if not cli.config.is_set("all"): - exists, netuid = get_netuid(cli, subtensor) - if not exists: - return + if cli.config.is_set("all"): + cli.config.netuid = None + cli.config.all = True + elif cli.config.is_set("netuid"): + if cli.config.netuid == "all": + cli.config.all = True + else: + cli.config.netuid = int(cli.config.netuid) + exists, netuid = get_netuid(cli, subtensor) + if not exists: + return + else: + netuid_input = Prompt.ask("Enter netuid or 'all'", default="all") + if netuid_input == "all": + cli.config.netuid = None + cli.config.all = True + else: + cli.config.netuid = int(netuid_input) + exists, netuid = get_netuid(cli, subtensor, False) + if not exists: + return # get parent hotkey hotkey = get_hotkey(wallet, cli.config) @@ -1148,11 +1171,7 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): return try: - netuids = ( - subtensor.get_all_subnet_netuids() - if cli.config.is_set("all") - else [netuid] - ) + netuids = subtensor.get_all_subnet_netuids() if cli.config.all else [netuid] hotkey_stake = GetChildrenCommand.get_parent_stake_info( console, subtensor, hotkey ) @@ -1236,7 +1255,7 @@ def add_args(parser: argparse.ArgumentParser): parser = parser.add_parser( "get_children", help="""Get child hotkeys on subnet.""" ) - parser.add_argument("--netuid", dest="netuid", type=int, required=False) + parser.add_argument("--netuid", dest="netuid", type=str, required=False) parser.add_argument("--hotkey", dest="hotkey", type=str, required=False) parser.add_argument( "--all", @@ -1294,7 +1313,7 @@ def render_table( # Add columns to the table with specific styles table.add_column("Index", style="bold yellow", no_wrap=True, justify="center") - table.add_column("ChildHotkey", style="bold green") + table.add_column("Child Hotkey", style="bold green") table.add_column("Proportion", style="bold cyan", no_wrap=True, justify="right") table.add_column( "Childkey Take", style="bold blue", no_wrap=True, justify="right" diff --git a/bittensor/extrinsics/serving.py b/bittensor/extrinsics/serving.py index bba5367de1..734561835f 100644 --- a/bittensor/extrinsics/serving.py +++ b/bittensor/extrinsics/serving.py @@ -25,6 +25,7 @@ import bittensor import bittensor.utils.networking as net from bittensor.utils import format_error_message +from bittensor.utils.networking import ensure_connected from ..errors import MetadataError @@ -269,6 +270,7 @@ def publish_metadata( raise MetadataError(format_error_message(response.error_message)) +@ensure_connected def get_metadata(self, netuid: int, hotkey: str, block: Optional[int] = None) -> str: @retry(delay=2, tries=3, backoff=2, max_delay=4) def make_substrate_call_with_retry(): diff --git a/bittensor/metagraph.py b/bittensor/metagraph.py index 8d7e97bcc0..eb60ab7754 100644 --- a/bittensor/metagraph.py +++ b/bittensor/metagraph.py @@ -467,6 +467,7 @@ def state_dict(self): "bonds": self.bonds, "uids": self.uids, "axons": self.axons, + "neurons": self.neurons, } def sync( @@ -782,6 +783,7 @@ def save(self) -> "metagraph": # type: ignore graph_filename = f"{save_directory}/block-{self.block.item()}.pt" state_dict = self.state_dict() state_dict["axons"] = self.axons + state_dict["neurons"] = self.neurons torch.save(state_dict, graph_filename) state_dict = torch.load( graph_filename @@ -1029,6 +1031,7 @@ def load_from_path(self, dir_path: str) -> "metagraph": # type: ignore ) self.uids = torch.nn.Parameter(state_dict["uids"], requires_grad=False) self.axons = state_dict["axons"] + self.neurons = state_dict["neurons"] if "weights" in state_dict: self.weights = torch.nn.Parameter( state_dict["weights"], requires_grad=False @@ -1173,6 +1176,7 @@ def load_from_path(self, dir_path: str) -> "metagraph": # type: ignore self.last_update = state_dict["last_update"] self.validator_permit = state_dict["validator_permit"] self.axons = state_dict["axons"] + self.neurons = state_dict["neurons"] if "weights" in state_dict: self.weights = state_dict["weights"] if "bonds" in state_dict: diff --git a/bittensor/subtensor.py b/bittensor/subtensor.py index ac22a3a14d..ac7e751a7f 100644 --- a/bittensor/subtensor.py +++ b/bittensor/subtensor.py @@ -188,6 +188,7 @@ def __init__( config: Optional[bittensor.config] = None, _mock: bool = False, log_verbose: bool = True, + connection_timeout: int = 600, ) -> None: """ Initializes a Subtensor interface for interacting with the Bittensor blockchain. @@ -251,7 +252,25 @@ def __init__( "To get ahead of this change, please run a local subtensor node and point to it." ) - # Attempt to connect to chosen endpoint. Fallback to finney if local unavailable. + self.log_verbose = log_verbose + self._connection_timeout = connection_timeout + self._get_substrate() + + self._subtensor_errors: Dict[str, Dict[str, str]] = {} + + def __str__(self) -> str: + if self.network == self.chain_endpoint: + # Connecting to chain endpoint without network known. + return "subtensor({})".format(self.chain_endpoint) + else: + # Connecting to network with endpoint known. + return "subtensor({}, {})".format(self.network, self.chain_endpoint) + + def __repr__(self) -> str: + return self.__str__() + + def _get_substrate(self): + """Establishes a connection to the Substrate node using configured parameters.""" try: # Set up params. self.substrate = SubstrateInterface( @@ -260,6 +279,11 @@ def __init__( url=self.chain_endpoint, type_registry=bittensor.__type_registry__, ) + if self.log_verbose: + _logger.info( + f"Connected to {self.network} network and {self.chain_endpoint}." + ) + except ConnectionRefusedError: _logger.error( f"Could not connect to {self.network} network with {self.chain_endpoint} chain endpoint. Exiting...", @@ -268,13 +292,10 @@ def __init__( "You can check if you have connectivity by running this command: nc -vz localhost " f"{self.chain_endpoint.split(':')[2]}" ) - exit(1) - # TODO (edu/phil): Advise to run local subtensor and point to dev docs. + return try: - self.substrate.websocket.settimeout(600) - # except: - # bittensor.logging.warning("Could not set websocket timeout.") + self.substrate.websocket.settimeout(self._connection_timeout) except AttributeError as e: _logger.warning(f"AttributeError: {e}") except TypeError as e: @@ -282,24 +303,6 @@ def __init__( except (socket.error, OSError) as e: _logger.warning(f"Socket error: {e}") - if log_verbose: - _logger.info( - f"Connected to {self.network} network and {self.chain_endpoint}." - ) - - self._subtensor_errors: Dict[str, Dict[str, str]] = {} - - def __str__(self) -> str: - if self.network == self.chain_endpoint: - # Connecting to chain endpoint without network known. - return "subtensor({})".format(self.chain_endpoint) - else: - # Connecting to network with endpoint known. - return "subtensor({}, {})".format(self.network, self.chain_endpoint) - - def __repr__(self) -> str: - return self.__str__() - @staticmethod def config() -> "bittensor.config": """ @@ -670,6 +673,7 @@ def set_take( wait_for_finalization=wait_for_finalization, ) + @networking.ensure_connected def send_extrinsic( self, wallet: "bittensor.wallet", @@ -839,6 +843,7 @@ def set_weights( return success, message + @networking.ensure_connected def _do_set_weights( self, wallet: "bittensor.wallet", @@ -986,6 +991,7 @@ def commit_weights( return success, message + @networking.ensure_connected def _do_commit_weights( self, wallet: "bittensor.wallet", @@ -1110,6 +1116,7 @@ def reveal_weights( return success, message + @networking.ensure_connected def _do_reveal_weights( self, wallet: "bittensor.wallet", @@ -1372,6 +1379,7 @@ def burned_register( prompt=prompt, ) + @networking.ensure_connected def _do_pow_register( self, netuid: int, @@ -1434,6 +1442,7 @@ def make_substrate_call_with_retry(): return make_substrate_call_with_retry() + @networking.ensure_connected def _do_burned_register( self, netuid: int, @@ -1491,6 +1500,7 @@ def make_substrate_call_with_retry(): return make_substrate_call_with_retry() + @networking.ensure_connected def _do_swap_hotkey( self, wallet: "bittensor.wallet", @@ -1588,6 +1598,7 @@ def transfer( prompt=prompt, ) + @networking.ensure_connected def get_transfer_fee( self, wallet: "bittensor.wallet", dest: str, value: Union["Balance", float, int] ) -> "Balance": @@ -1645,6 +1656,7 @@ def get_transfer_fee( ) return fee + @networking.ensure_connected def _do_transfer( self, wallet: "bittensor.wallet", @@ -1880,6 +1892,7 @@ def serve_axon( self, netuid, axon, wait_for_inclusion, wait_for_finalization ) + @networking.ensure_connected def _do_serve_axon( self, wallet: "bittensor.wallet", @@ -1947,6 +1960,7 @@ def serve_prometheus( wait_for_finalization=wait_for_finalization, ) + @networking.ensure_connected def _do_serve_prometheus( self, wallet: "bittensor.wallet", @@ -1992,6 +2006,7 @@ def make_substrate_call_with_retry(): return make_substrate_call_with_retry() + @networking.ensure_connected def _do_associate_ips( self, wallet: "bittensor.wallet", @@ -2122,6 +2137,7 @@ def add_stake_multiple( prompt, ) + @networking.ensure_connected def _do_stake( self, wallet: "bittensor.wallet", @@ -2249,6 +2265,7 @@ def unstake( prompt, ) + @networking.ensure_connected def _do_unstake( self, wallet: "bittensor.wallet", @@ -2339,6 +2356,7 @@ def set_childkey_take( prompt=prompt, ) + @networking.ensure_connected def _do_set_childkey_take( self, wallet: "bittensor.wallet", @@ -2430,6 +2448,7 @@ def set_children( prompt=prompt, ) + @networking.ensure_connected def _do_set_children( self, wallet: "bittensor.wallet", @@ -2806,6 +2825,7 @@ def root_register( prompt=prompt, ) + @networking.ensure_connected def _do_root_register( self, wallet: "bittensor.wallet", @@ -2886,6 +2906,7 @@ def root_set_weights( prompt=prompt, ) + @networking.ensure_connected def _do_set_root_weights( self, wallet: "bittensor.wallet", @@ -2958,6 +2979,7 @@ def make_substrate_call_with_retry(): ################## # Queries subtensor registry named storage with params and block. + @networking.ensure_connected def query_identity( self, key: str, @@ -3000,6 +3022,7 @@ def make_substrate_call_with_retry() -> "ScaleType": identity_info.value["info"] ) + @networking.ensure_connected def update_identity( self, wallet: "bittensor.wallet", @@ -3103,6 +3126,7 @@ def get_commitment(self, netuid: int, uid: int, block: Optional[int] = None) -> ################## # Queries subtensor named storage with params and block. + @networking.ensure_connected def query_subtensor( self, name: str, @@ -3139,6 +3163,7 @@ def make_substrate_call_with_retry() -> "ScaleType": return make_substrate_call_with_retry() # Queries subtensor map storage with params and block. + @networking.ensure_connected def query_map_subtensor( self, name: str, @@ -3175,6 +3200,7 @@ def make_substrate_call_with_retry(): return make_substrate_call_with_retry() + @networking.ensure_connected def query_constant( self, module_name: str, constant_name: str, block: Optional[int] = None ) -> Optional["ScaleType"]: @@ -3209,6 +3235,7 @@ def make_substrate_call_with_retry(): return make_substrate_call_with_retry() # Queries any module storage with params and block. + @networking.ensure_connected def query_module( self, module: str, @@ -3248,6 +3275,7 @@ def make_substrate_call_with_retry() -> "ScaleType": return make_substrate_call_with_retry() # Queries any module map storage with params and block. + @networking.ensure_connected def query_map( self, module: str, @@ -3286,6 +3314,7 @@ def make_substrate_call_with_retry() -> "QueryMapResult": return make_substrate_call_with_retry() + @networking.ensure_connected def state_call( self, method: str, @@ -3374,6 +3403,7 @@ def query_runtime_api( return obj.decode() + @networking.ensure_connected def _encode_params( self, call_definition: List["ParamWithTypes"], @@ -4345,12 +4375,17 @@ def get_subnets(self, block: Optional[int] = None) -> List[int]: available for neuron participation and collaboration. """ result = self.query_map_subtensor("NetworksAdded", block) - return ( - [network[0].value for network in result.records] - if result and hasattr(result, "records") - else [] - ) + subnets = [] + + for data in result: + continue + # Check if the 'records' attribute exists and is not None + if hasattr(result, "records") and result.records: + subnets = [subnet[0].value for subnet in result.records] + + return subnets + @networking.ensure_connected def get_all_subnets_info(self, block: Optional[int] = None) -> List[SubnetInfo]: """ Retrieves detailed information about all subnets within the Bittensor network. This function @@ -4382,6 +4417,7 @@ def make_substrate_call_with_retry(): return SubnetInfo.list_from_vec_u8(result) + @networking.ensure_connected def get_subnet_info( self, netuid: int, block: Optional[int] = None ) -> Optional[SubnetInfo]: @@ -4540,6 +4576,7 @@ def get_nominators_for_hotkey( else 0 ) + @networking.ensure_connected def get_delegate_by_hotkey( self, hotkey_ss58: str, block: Optional[int] = None ) -> Optional[DelegateInfo]: @@ -4577,6 +4614,7 @@ def make_substrate_call_with_retry(encoded_hotkey_: List[int]): return DelegateInfo.from_vec_u8(result) + @networking.ensure_connected def get_delegates_lite(self, block: Optional[int] = None) -> List[DelegateInfoLite]: """ Retrieves a lighter list of all delegate neurons within the Bittensor network. This function provides an @@ -4611,6 +4649,7 @@ def make_substrate_call_with_retry(): return [DelegateInfoLite(**d) for d in result] + @networking.ensure_connected def get_delegates(self, block: Optional[int] = None) -> List[DelegateInfo]: """ Retrieves a list of all delegate neurons within the Bittensor network. This function provides an overview of the @@ -4643,6 +4682,7 @@ def make_substrate_call_with_retry(): return DelegateInfo.list_from_vec_u8(result) + @networking.ensure_connected def get_delegated( self, coldkey_ss58: str, block: Optional[int] = None ) -> List[Tuple[DelegateInfo, Balance]]: @@ -4715,6 +4755,7 @@ def get_childkey_take( return None return None + @networking.ensure_connected def get_children(self, hotkey, netuid) -> list[tuple[int, str]] | list[Any] | None: """ Get the children of a hotkey on a specific network. @@ -4741,6 +4782,7 @@ def get_children(self, hotkey, netuid) -> list[tuple[int, str]] | list[Any] | No print(f"Unexpected error in get_children: {e}") return None + @networking.ensure_connected def get_parents(self, child_hotkey, netuid): """ Get the parents of a child hotkey on a specific network. @@ -4852,6 +4894,7 @@ def get_stake_info_for_coldkeys( return StakeInfo.list_of_tuple_from_vec_u8(bytes_result) # type: ignore + @networking.ensure_connected def get_minimum_required_stake( self, ) -> Balance: @@ -5104,6 +5147,7 @@ def neuron_for_wallet( wallet.hotkey.ss58_address, netuid=netuid, block=block ) + @networking.ensure_connected def neuron_for_uid( self, uid: Optional[int], netuid: int, block: Optional[int] = None ) -> NeuronInfo: @@ -5396,6 +5440,7 @@ def get_subnet_burn_cost(self, block: Optional[int] = None) -> Optional[str]: # Extrinsics # ############## + @networking.ensure_connected def _do_delegation( self, wallet: "bittensor.wallet", @@ -5447,6 +5492,7 @@ def make_substrate_call_with_retry(): return make_substrate_call_with_retry() + @networking.ensure_connected def _do_undelegation( self, wallet: "bittensor.wallet", @@ -5501,6 +5547,7 @@ def make_substrate_call_with_retry(): return make_substrate_call_with_retry() + @networking.ensure_connected def _do_nominate( self, wallet: "bittensor.wallet", @@ -5548,6 +5595,7 @@ def make_substrate_call_with_retry(): return make_substrate_call_with_retry() + @networking.ensure_connected def _do_increase_take( self, wallet: "bittensor.wallet", @@ -5603,6 +5651,7 @@ def make_substrate_call_with_retry(): return make_substrate_call_with_retry() + @networking.ensure_connected def _do_decrease_take( self, wallet: "bittensor.wallet", @@ -5662,6 +5711,7 @@ def make_substrate_call_with_retry(): # Legacy # ########## + @networking.ensure_connected def get_balance(self, address: str, block: Optional[int] = None) -> Balance: """ Retrieves the token balance of a specific address within the Bittensor network. This function queries @@ -5698,6 +5748,7 @@ def make_substrate_call_with_retry(): return Balance(1000) return Balance(result.value["data"]["free"]) + @networking.ensure_connected def get_current_block(self) -> int: """ Returns the current block number on the Bittensor blockchain. This function provides the latest block @@ -5716,6 +5767,7 @@ def make_substrate_call_with_retry(): return make_substrate_call_with_retry() + @networking.ensure_connected def get_balances(self, block: Optional[int] = None) -> Dict[str, Balance]: """ Retrieves the token balances of all accounts within the Bittensor network as of a specific blockchain block. @@ -5775,6 +5827,7 @@ def _null_neuron() -> NeuronInfo: ) # type: ignore return neuron + @networking.ensure_connected def get_block_hash(self, block_id: int) -> str: """ Retrieves the hash of a specific block on the Bittensor blockchain. The block hash is a unique diff --git a/bittensor/utils/networking.py b/bittensor/utils/networking.py index 4d1af585c3..f4b729fe97 100644 --- a/bittensor/utils/networking.py +++ b/bittensor/utils/networking.py @@ -19,15 +19,17 @@ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # DEALINGS IN THE SOFTWARE. -# Standard Lib +import json import os +import socket import urllib -import json -import netaddr +from functools import wraps -# 3rd party +import netaddr import requests +from bittensor.btlogging import logging + def int_to_ip(int_val: int) -> str: r"""Maps an integer to a unique ip-string @@ -171,3 +173,26 @@ def get_formatted_ws_endpoint_url(endpoint_url: str) -> str: endpoint_url = "ws://{}".format(endpoint_url) return endpoint_url + + +def ensure_connected(func): + """Decorator ensuring the function executes with an active substrate connection.""" + + @wraps(func) + def wrapper(self, *args, **kwargs): + # Check the socket state before method execution + if ( + # connection was closed correctly + self.substrate.websocket.sock is None + # connection has a broken pipe + or self.substrate.websocket.sock.getsockopt( + socket.SOL_SOCKET, socket.SO_ERROR + ) + != 0 + ): + logging.info("Reconnection substrate...") + self._get_substrate() + # Execute the method if the connection is active or after reconnecting + return func(self, *args, **kwargs) + + return wrapper diff --git a/tests/e2e_tests/subcommands/subnet/test_metagraph.py b/tests/e2e_tests/subcommands/subnet/test_metagraph.py index e8e18ef617..a77876c2bf 100644 --- a/tests/e2e_tests/subcommands/subnet/test_metagraph.py +++ b/tests/e2e_tests/subcommands/subnet/test_metagraph.py @@ -80,9 +80,8 @@ def test_metagraph_command(local_chain, capsys): captured = capsys.readouterr() # Assert the neuron is registered and displayed - assert ( - "Metagraph: net: local:1" and "N: 1/1" in captured.out - ), "Neuron isn't displayed in metagraph" + assert "Metagraph: net: local:1" in captured.out + assert "N: 1/1" in captured.out, "Neuron isn't displayed in metagraph" # Register Dave as neuron to the subnet dave_keypair, dave_exec_command, dave_wallet = setup_wallet("//Dave") @@ -117,6 +116,7 @@ def test_metagraph_command(local_chain, capsys): captured = capsys.readouterr() # Assert the neuron is registered and displayed - assert "Metagraph: net: local:1" and "N: 2/2" in captured.out + assert "Metagraph: net: local:1" in captured.out + assert "N: 2/2" in captured.out logging.info("Passed test_metagraph_command") diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index c651eaa57f..c361791adc 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -1914,6 +1914,24 @@ def test_get_subnets_no_block_specified(mocker, subtensor): subtensor.query_map_subtensor.assert_called_once_with("NetworksAdded", None) +def test_get_subnets_correct_length(mocker, subtensor): + """Test get_subnets returns a list of the correct length.""" + # Prep + block = 123 + num_records = 500 + mock_records = [(mocker.MagicMock(value=i), True) for i in range(num_records)] + mock_result = mocker.MagicMock() + mock_result.records = mock_records + mocker.patch.object(subtensor, "query_map_subtensor", return_value=mock_result) + + # Call + result = subtensor.get_subnets(block) + + # Asserts + assert len(result) == num_records + subtensor.query_map_subtensor.assert_called_once_with("NetworksAdded", block) + + # `get_all_subnets_info` tests def test_get_all_subnets_info_success(mocker, subtensor): """Test get_all_subnets_info returns correct data when subnet information is found.""" @@ -2315,3 +2333,39 @@ def test_get_remaining_arbitration_period_happy(subtensor, mocker): ) # if we change the methods logic in the future we have to be make sure the returned type is correct assert result == 1800 # 2000 - 200 + + +def test_connect_without_substrate(mocker): + """Ensure re-connection is called when using an alive substrate.""" + # Prep + fake_substrate = mocker.MagicMock() + fake_substrate.websocket.sock.getsockopt.return_value = 1 + mocker.patch.object( + subtensor_module, "SubstrateInterface", return_value=fake_substrate + ) + fake_subtensor = Subtensor() + spy_get_substrate = mocker.spy(Subtensor, "_get_substrate") + + # Call + _ = fake_subtensor.block + + # Assertions + assert spy_get_substrate.call_count == 1 + + +def test_connect_with_substrate(mocker): + """Ensure re-connection is non called when using an alive substrate.""" + # Prep + fake_substrate = mocker.MagicMock() + fake_substrate.websocket.sock.getsockopt.return_value = 0 + mocker.patch.object( + subtensor_module, "SubstrateInterface", return_value=fake_substrate + ) + fake_subtensor = Subtensor() + spy_get_substrate = mocker.spy(Subtensor, "_get_substrate") + + # Call + _ = fake_subtensor.block + + # Assertions + assert spy_get_substrate.call_count == 0