From 65b35a4fc2cd98c16a3d24578cdc6d4bb79504e1 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Thu, 14 Nov 2024 15:19:07 +0200 Subject: [PATCH 01/11] Fixes Subtensor.get_all_subnets_info() --- bittensor/core/subtensor.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 67888cd99..428ee39d7 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -1407,17 +1407,18 @@ def get_all_subnets_info(self, block: Optional[int] = None) -> list[SubnetInfo]: Gaining insights into the subnets' details assists in understanding the network's composition, the roles of different subnets, and their unique features. """ - block_hash = None if block is None else self.substrate.get_block_hash(block) - - json_body = self.substrate.rpc_request( - method="subnetInfo_getSubnetsInfo", # custom rpc method - params=[block_hash] if block_hash else [], + hex_bytes_result = self.query_runtime_api( + "SubnetInfoRuntimeApi", "get_subnets_info", params=[], block=block ) - - if not (result := json_body.get("result", None)): + if not hex_bytes_result: return [] + else: + try: + bytes_result = bytes.fromhex(hex_bytes_result[2:]) + except ValueError: + bytes_result = bytes.fromhex(hex_bytes_result) - return SubnetInfo.list_from_vec_u8(result) + return SubnetInfo.list_from_vec_u8(bytes_result) # Metagraph uses this method def bonds( From 86b589a4ed298c1c3989d346c5e15fa3420488d5 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Thu, 14 Nov 2024 15:31:53 +0200 Subject: [PATCH 02/11] Adds reusable hex_to_bytes function --- bittensor/core/subtensor.py | 29 +++++++++++------------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 428ee39d7..a2667e68f 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -85,6 +85,14 @@ class ParamWithTypes(TypedDict): type: str # ScaleType string of the parameter. +def hex_to_bytes(hex_str: str) -> bytes: + try: + bytes_result = bytes.fromhex(hex_str[2:]) + except ValueError: + bytes_result = bytes.fromhex(hex_str) + return bytes_result + + class Subtensor: """ The Subtensor class in Bittensor serves as a crucial interface for interacting with the Bittensor blockchain, @@ -1227,12 +1235,7 @@ def get_subnet_hyperparameters( 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) + return SubnetHyperparameters.from_vec_u8(hex_to_bytes(hex_bytes_result)) # Community uses this method # Returns network ImmunityPeriod hyper parameter. @@ -1413,12 +1416,7 @@ def get_all_subnets_info(self, block: Optional[int] = None) -> list[SubnetInfo]: if not hex_bytes_result: return [] else: - try: - bytes_result = bytes.fromhex(hex_bytes_result[2:]) - except ValueError: - bytes_result = bytes.fromhex(hex_bytes_result) - - return SubnetInfo.list_from_vec_u8(bytes_result) + return SubnetInfo.list_from_vec_u8(hex_to_bytes(hex_bytes_result)) # Metagraph uses this method def bonds( @@ -1562,12 +1560,7 @@ def neurons_lite( 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 NeuronInfoLite.list_from_vec_u8(bytes_result) # type: ignore + return NeuronInfoLite.list_from_vec_u8(hex_to_bytes(hex_bytes_result)) # type: ignore # Used in the `neurons` method which is used in metagraph.py def weights( From c503cae62c2204ed62e631eb999362c1db0242bf Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Thu, 14 Nov 2024 15:57:48 +0200 Subject: [PATCH 03/11] Added TODO --- bittensor/core/subtensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index a2667e68f..62cde3d7f 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -1310,6 +1310,7 @@ def get_commitment(self, netuid: int, uid: int, block: Optional[int] = None) -> metagraph = self.metagraph(netuid) hotkey = metagraph.hotkeys[uid] # type: ignore + # TODO this breaks easily, should be improved. get_metadata frequently returns None metadata = get_metadata(self, netuid, hotkey, block) commitment = metadata["info"]["fields"][0] # type: ignore hex_data = commitment[list(commitment.keys())[0]][2:] # type: ignore From 31a0197347b9ea0d27b20fe62c6752bb9045ed35 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Thu, 14 Nov 2024 16:00:30 +0200 Subject: [PATCH 04/11] Fix get_prometheus_info --- bittensor/core/subtensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 62cde3d7f..4b85a3694 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -1371,7 +1371,7 @@ def get_prometheus_info( Optional[bittensor.core.chain_data.prometheus_info.PrometheusInfo]: A PrometheusInfo object containing the prometheus information, or ``None`` if the prometheus information is not found. """ result = self.query_subtensor("Prometheus", block, [netuid, hotkey_ss58]) - if result is not None and hasattr(result, "value"): + if result is not None and getattr(result, "value", None) is not None: return PrometheusInfo( ip=networking.int_to_ip(result.value["ip"]), ip_type=result.value["ip_type"], From bd423113fa1724ecd0bc4d26b865243f4345f4f9 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Thu, 14 Nov 2024 16:02:27 +0200 Subject: [PATCH 05/11] Fix get_delegate_by_hotkey --- bittensor/core/subtensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 4b85a3694..d3b748a12 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -1918,7 +1918,7 @@ def get_delegate_by_hotkey( if not (result := json_body.get("result", None)): return None - return DelegateInfo.from_vec_u8(result) + return DelegateInfo.from_vec_u8(bytes(result)) # Subnet 27 uses this method name _do_serve_axon = do_serve_axon From 387fdce8ca7202f56f568c3ad7e1eb05553f6d86 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Thu, 14 Nov 2024 16:09:11 +0200 Subject: [PATCH 06/11] Moved hex_to_bytes fn to utils. --- bittensor/core/subtensor.py | 16 +++++++--------- bittensor/utils/__init__.py | 11 +++++++++++ 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index d3b748a12..f9f8e8771 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -71,7 +71,13 @@ transfer_extrinsic, ) from bittensor.core.metagraph import Metagraph -from bittensor.utils import networking, torch, ss58_to_vec_u8, u16_normalized_float +from bittensor.utils import ( + networking, + torch, + ss58_to_vec_u8, + u16_normalized_float, + hex_to_bytes, +) from bittensor.utils.balance import Balance from bittensor.utils.btlogging import logging from bittensor.utils.registration import legacy_torch_api_compat @@ -85,14 +91,6 @@ class ParamWithTypes(TypedDict): type: str # ScaleType string of the parameter. -def hex_to_bytes(hex_str: str) -> bytes: - try: - bytes_result = bytes.fromhex(hex_str[2:]) - except ValueError: - bytes_result = bytes.fromhex(hex_str) - return bytes_result - - class Subtensor: """ The Subtensor class in Bittensor serves as a crucial interface for interacting with the Bittensor blockchain, diff --git a/bittensor/utils/__init__.py b/bittensor/utils/__init__.py index 5c8938298..ea5c792af 100644 --- a/bittensor/utils/__init__.py +++ b/bittensor/utils/__init__.py @@ -403,3 +403,14 @@ def unlock_key(wallet: "Wallet", unlock_type="coldkey") -> "UnlockStatus": except KeyFileError: err_msg = f"{unlock_type.capitalize()} keyfile is corrupt, non-writable, or non-readable, or non-existent." return UnlockStatus(False, err_msg) + + +def hex_to_bytes(hex_str: str) -> bytes: + """ + Converts a hex-encoded string into bytes. Handles 0x-prefixed and non-prefixed hex-encoded strings. + """ + try: + bytes_result = bytes.fromhex(hex_str[2:]) + except ValueError: + bytes_result = bytes.fromhex(hex_str) + return bytes_result From c304e5b8803d4330b27a6fa650695ff9de0af6c2 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Thu, 14 Nov 2024 16:15:27 +0200 Subject: [PATCH 07/11] Added unit test --- bittensor/utils/__init__.py | 4 ++-- tests/unit_tests/utils/test_utils.py | 12 ++++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/bittensor/utils/__init__.py b/bittensor/utils/__init__.py index ea5c792af..7a42dff0c 100644 --- a/bittensor/utils/__init__.py +++ b/bittensor/utils/__init__.py @@ -409,8 +409,8 @@ def hex_to_bytes(hex_str: str) -> bytes: """ Converts a hex-encoded string into bytes. Handles 0x-prefixed and non-prefixed hex-encoded strings. """ - try: + if hex_str.startswith("0x"): bytes_result = bytes.fromhex(hex_str[2:]) - except ValueError: + else: bytes_result = bytes.fromhex(hex_str) return bytes_result diff --git a/tests/unit_tests/utils/test_utils.py b/tests/unit_tests/utils/test_utils.py index a01b42f31..eda2eeb10 100644 --- a/tests/unit_tests/utils/test_utils.py +++ b/tests/unit_tests/utils/test_utils.py @@ -223,3 +223,15 @@ def test_unlock_key_errors(mocker, side_effect, response): result = utils.unlock_key(wallet=mock_wallet) assert result == response + + +@pytest.mark.parametrize( + "hex_str, response", + [ + ("5461796c6f72205377696674", b"Taylor Swift"), + ("0x5461796c6f72205377696674", b"Taylor Swift"), + ], +) +def test_hex_to_bytes(hex_str, response): + result = utils.hex_to_bytes(hex_str) + assert result == response From f0e7dd0043cc34a521849804e2bf2bf07a4cd7b0 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Thu, 14 Nov 2024 16:16:56 +0200 Subject: [PATCH 08/11] Fixes get_commitment --- bittensor/core/subtensor.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index f9f8e8771..ae66293b2 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -1308,12 +1308,14 @@ def get_commitment(self, netuid: int, uid: int, block: Optional[int] = None) -> metagraph = self.metagraph(netuid) hotkey = metagraph.hotkeys[uid] # type: ignore - # TODO this breaks easily, should be improved. get_metadata frequently returns None metadata = get_metadata(self, netuid, hotkey, block) - commitment = metadata["info"]["fields"][0] # type: ignore - hex_data = commitment[list(commitment.keys())[0]][2:] # type: ignore + try: + commitment = metadata["info"]["fields"][0] # type: ignore + hex_data = commitment[list(commitment.keys())[0]][2:] # type: ignore + return bytes.fromhex(hex_data).decode() - return bytes.fromhex(hex_data).decode() + except TypeError: + return "" # Community uses this via `bittensor.utils.weight_utils.process_weights_for_netuid` function. def min_allowed_weights( From b364228663e74246e3a93936bd6cd26b5df908e6 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Thu, 14 Nov 2024 17:04:04 +0200 Subject: [PATCH 09/11] Use `hex_to_bytes` function in async_subtensor --- bittensor/core/async_subtensor.py | 29 +++++------------------------ 1 file changed, 5 insertions(+), 24 deletions(-) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index ae912d5dc..c82bb0ae2 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -46,6 +46,7 @@ format_error_message, decode_hex_identity_dict, validate_chain_endpoint, + hex_to_bytes, ) from bittensor.utils.async_substrate_interface import ( AsyncSubstrateInterface, @@ -351,12 +352,7 @@ async def get_delegates( reuse_block=reuse_block, ) if hex_bytes_result is not None: - try: - bytes_result = bytes.fromhex(hex_bytes_result[2:]) - except ValueError: - bytes_result = bytes.fromhex(hex_bytes_result) - - return DelegateInfo.list_from_vec_u8(bytes_result) + return DelegateInfo.list_from_vec_u8(hex_to_bytes(hex_bytes_result)) else: return [] @@ -392,12 +388,7 @@ async def get_stake_info_for_coldkey( if hex_bytes_result is None: return [] - try: - bytes_result = bytes.fromhex(hex_bytes_result[2:]) - except ValueError: - bytes_result = bytes.fromhex(hex_bytes_result) - - return StakeInfo.list_from_vec_u8(bytes_result) + return StakeInfo.list_from_vec_u8(hex_to_bytes(hex_bytes_result)) async def get_stake_for_coldkey_and_hotkey( self, hotkey_ss58: str, coldkey_ss58: str, block_hash: Optional[str] = None @@ -853,12 +844,7 @@ async def neurons_lite( if hex_bytes_result is None: return [] - try: - bytes_result = bytes.fromhex(hex_bytes_result[2:]) - except ValueError: - bytes_result = bytes.fromhex(hex_bytes_result) - - return NeuronInfoLite.list_from_vec_u8(bytes_result) + return NeuronInfoLite.list_from_vec_u8(hex_to_bytes(hex_bytes_result)) async def neuron_for_uid( self, uid: Optional[int], netuid: int, block_hash: Optional[str] = None @@ -1170,12 +1156,7 @@ async def get_subnet_hyperparameters( 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) + return SubnetHyperparameters.from_vec_u8(hex_to_bytes(hex_bytes_result)) async def get_vote_data( self, From a266e79ee4f5e646bd312191f15d83e9fb345456 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Thu, 14 Nov 2024 17:52:40 +0200 Subject: [PATCH 10/11] Fixed one test. --- tests/unit_tests/test_async_subtensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit_tests/test_async_subtensor.py b/tests/unit_tests/test_async_subtensor.py index 4db725da3..b4affe0cc 100644 --- a/tests/unit_tests/test_async_subtensor.py +++ b/tests/unit_tests/test_async_subtensor.py @@ -351,7 +351,7 @@ async def test_get_delegates(subtensor, mocker, fake_hex_bytes_result, response) @pytest.mark.parametrize( - "fake_hex_bytes_result, response", [(None, []), ("zz001122", b"\xaa\xbb\xcc\xdd")] + "fake_hex_bytes_result, response", [(None, []), ("0x001122", b"\xaa\xbb\xcc\xdd")] ) @pytest.mark.asyncio async def test_get_stake_info_for_coldkey( From 18e0d9cf7328ee5d42a82d6176070563703fad2f Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Thu, 14 Nov 2024 18:26:47 +0200 Subject: [PATCH 11/11] Fixed additional tests --- bittensor/core/subtensor.py | 2 -- tests/unit_tests/test_subtensor.py | 24 +++++++++++------------- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index ae66293b2..efaff369a 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -529,13 +529,11 @@ def query_runtime_api( return None return_type = call_definition["type"] - as_scale_bytes = scalecodec.ScaleBytes(json_result["result"]) rpc_runtime_config = RuntimeConfiguration() rpc_runtime_config.update_type_registry(load_type_registry_preset("legacy")) rpc_runtime_config.update_type_registry(custom_rpc_type_registry) - obj = rpc_runtime_config.create_scale_object(return_type, as_scale_bytes) if obj.data.to_hex() == "0x0400": # RPC returned None result return None diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index e3f573c67..602a3027d 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -2022,12 +2022,12 @@ def test_get_all_subnets_info_success(mocker, subtensor): """Test get_all_subnets_info returns correct data when subnet information is found.""" # Prep block = 123 - subnet_data = [1, 2, 3] # Mocked response data mocker.patch.object( subtensor.substrate, "get_block_hash", return_value="mock_block_hash" ) - mock_response = {"result": subnet_data} - mocker.patch.object(subtensor.substrate, "rpc_request", return_value=mock_response) + hex_bytes_result = "0x010203" + bytes_result = bytes.fromhex(hex_bytes_result[2:]) + mocker.patch.object(subtensor, "query_runtime_api", return_value=hex_bytes_result) mocker.patch.object( subtensor_module.SubnetInfo, "list_from_vec_u8", @@ -2035,14 +2035,13 @@ def test_get_all_subnets_info_success(mocker, subtensor): ) # Call - result = subtensor.get_all_subnets_info(block) + subtensor.get_all_subnets_info(block) # Asserts - subtensor.substrate.get_block_hash.assert_called_once_with(block) - subtensor.substrate.rpc_request.assert_called_once_with( - method="subnetInfo_getSubnetsInfo", params=["mock_block_hash"] + subtensor.query_runtime_api.assert_called_once_with( + "SubnetInfoRuntimeApi", "get_subnets_info", params=[], block=block ) - subtensor_module.SubnetInfo.list_from_vec_u8.assert_called_once_with(subnet_data) + subtensor_module.SubnetInfo.list_from_vec_u8.assert_called_once_with(bytes_result) @pytest.mark.parametrize("result_", [[], None]) @@ -2053,18 +2052,17 @@ def test_get_all_subnets_info_no_data(mocker, subtensor, result_): mocker.patch.object( subtensor.substrate, "get_block_hash", return_value="mock_block_hash" ) - mock_response = {"result": result_} - mocker.patch.object(subtensor.substrate, "rpc_request", return_value=mock_response) mocker.patch.object(subtensor_module.SubnetInfo, "list_from_vec_u8") + mocker.patch.object(subtensor, "query_runtime_api", return_value=result_) + # Call result = subtensor.get_all_subnets_info(block) # Asserts assert result == [] - subtensor.substrate.get_block_hash.assert_called_once_with(block) - subtensor.substrate.rpc_request.assert_called_once_with( - method="subnetInfo_getSubnetsInfo", params=["mock_block_hash"] + subtensor.query_runtime_api.assert_called_once_with( + "SubnetInfoRuntimeApi", "get_subnets_info", params=[], block=block ) subtensor_module.SubnetInfo.list_from_vec_u8.assert_not_called()