diff --git a/.circleci/config.yml b/.circleci/config.yml index 90f49d54eb..80bdc8c318 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -275,19 +275,6 @@ jobs: command: | [[ $(docker manifest inspect opentensorfdn/bittensor:`cat VERSION` > /dev/null 2> /dev/null ; echo $?) == 1 ]] && echo "Docker image 'opentensorfdn/bittensor:`cat VERSION`' does not exist in dockerhub" - release-dry-run: - docker: - - image: cimg/python:3.10 - steps: - - checkout - - setup_remote_docker: - version: 20.10.14 - docker_layer_caching: true - - run: - name: Executing release script - command: | - ./scripts/release/release.sh --github-token ${GH_API_ACCESS_TOKEN} - workflows: compatibility_checks: jobs: @@ -339,11 +326,6 @@ workflows: branches: only: - /^(release|hotfix)/.*/ - - release-dry-run: - filters: - branches: - only: - - /^(release|hotfix)/.*/ release-requirements: jobs: @@ -352,8 +334,3 @@ workflows: branches: only: - master - - release-dry-run: - filters: - branches: - only: - - master diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c8cb25c2a..7c2b308cf9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,48 @@ # Changelog +## 8.3.1 /2024-11-14 + +## What's Changed +* Fixes broken Subtensor methods by @thewhaleking in https://github.com/opentensor/bittensor/pull/2420 +* [Tests] AsyncSubtensor (Part 7: The final race) by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2418 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v8.3.0...v8.3.1 + +## 8.3.0 /2024-11-13 + +## What's Changed +* Expands the type registry to include all the available options by @thewhaleking in https://github.com/opentensor/bittensor/pull/2353 +* add `Subtensor.register`, `Subtensor.difficulty` and related staff with tests by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2352 +* added to Subtensor: `burned_register`, `get_subnet_burn_cost`, `recycle` and related extrinsics by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2359 +* Poem "Risen from the Past". Act 3. by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2363 +* default port from 9946 to 9944 by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2376 +* remove unused prometheus extrinsic by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2378 +* Replace rich.console to btlogging.loggin by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2377 +* SDK (AsyncSubtensor) Part 1 by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2374 +* SDK (AsyncSubtensor) Part 2 by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2380 +* Handle SSL Error on Connection by @thewhaleking in https://github.com/opentensor/bittensor/pull/2384 +* Avoid using `prompt` in SDK by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2382 +* Backmerge/8.2.0 by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2389 +* Remove `retry` and fix tests by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2392 +* fix: logging weights correctly in utils/weight_utils.py by @grantdfoster in https://github.com/opentensor/bittensor/pull/2362 +* Add `subvortex` subnet and tests by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2395 +* Release/8.2.1 by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2397 +* [Tests] AsyncSubtensor (Part 1) by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2398 +* Extend period for fastblock e2e tests_incentive.py by @opendansor in https://github.com/opentensor/bittensor/pull/2400 +* Remove unused import by @thewhaleking in https://github.com/opentensor/bittensor/pull/2401 +* `Reconnection substrate...` as debug by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2403 +* Handles websockets v14+ in async by @thewhaleking in https://github.com/opentensor/bittensor/pull/2404 +* [Tests] AsyncSubtensor (Part 2) by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2407 +* [Tests] AsyncSubtensor (Part 3) by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2409 +* Handle new PasswordError from btwallet by @thewhaleking in https://github.com/opentensor/bittensor/pull/2406 +* [Tests] AsyncSubtensor (Part 4) by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2410 +* [Tests] AsyncSubtensor (Part 5) by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2411 +* Bringing back lost methods for setting weights by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2412 +* Update bt-decode requirement by @thewhaleking in https://github.com/opentensor/bittensor/pull/2413 +* [Tests] AsyncSubtensor (Part 6) by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2414 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v8.2.1...v8.3.0 + ## 8.2.1 /2024-11-06 ## What's Changed @@ -1143,4 +1186,4 @@ This release refactors the registration code for CPU registration to improve sol ### Synapse update -## \ No newline at end of file +## diff --git a/VERSION b/VERSION index 9f4a0fbc18..905c243928 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.3.0 \ No newline at end of file +8.3.1 \ No newline at end of file diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index ae912d5dca..fc59fc121a 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, @@ -1422,15 +1403,15 @@ async def register( async def pow_register( self: "AsyncSubtensor", - wallet: Wallet, - netuid, - processors, - update_interval, - output_in_place, - verbose, - use_cuda, - dev_id, - threads_per_block, + wallet: "Wallet", + netuid: int, + processors: int, + update_interval: int, + output_in_place: bool, + verbose: bool, + use_cuda: bool, + dev_id: Union[list[int], int], + threads_per_block: int, ): """Register neuron.""" return await register_extrinsic( @@ -1481,11 +1462,9 @@ async def set_weights( retries = 0 success = False message = "No attempt made. Perhaps it is too soon to set weights!" - while ( - await self.blocks_since_last_update(netuid, uid) - > await self.weights_rate_limit(netuid) - and retries < max_retries - ): + while retries < max_retries and await self.blocks_since_last_update( + netuid, uid + ) > await self.weights_rate_limit(netuid): try: logging.info( f"Setting weights for subnet #{netuid}. Attempt {retries + 1} of {max_retries}." diff --git a/bittensor/core/settings.py b/bittensor/core/settings.py index d3a8a43a3c..8e8843b2e8 100644 --- a/bittensor/core/settings.py +++ b/bittensor/core/settings.py @@ -15,7 +15,7 @@ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # DEALINGS IN THE SOFTWARE. -__version__ = "8.3.0" +__version__ = "8.3.1" import os import re diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 67888cd999..efaff369a4 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 @@ -523,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 @@ -1227,12 +1231,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. @@ -1308,10 +1307,13 @@ def get_commitment(self, netuid: int, uid: int, block: Optional[int] = None) -> hotkey = metagraph.hotkeys[uid] # type: ignore 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( @@ -1367,7 +1369,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"], @@ -1407,17 +1409,13 @@ 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 [] - - return SubnetInfo.list_from_vec_u8(result) + else: + return SubnetInfo.list_from_vec_u8(hex_to_bytes(hex_bytes_result)) # Metagraph uses this method def bonds( @@ -1561,12 +1559,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( @@ -1923,7 +1916,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 diff --git a/bittensor/utils/__init__.py b/bittensor/utils/__init__.py index 5c89382987..7a42dff0cf 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. + """ + if hex_str.startswith("0x"): + bytes_result = bytes.fromhex(hex_str[2:]) + else: + bytes_result = bytes.fromhex(hex_str) + return bytes_result diff --git a/tests/unit_tests/test_async_subtensor.py b/tests/unit_tests/test_async_subtensor.py index 4db725da39..87813761dc 100644 --- a/tests/unit_tests/test_async_subtensor.py +++ b/tests/unit_tests/test_async_subtensor.py @@ -1,5 +1,6 @@ import pytest +from bittensor import AsyncSubtensor from bittensor.core import async_subtensor @@ -69,6 +70,53 @@ def test_decode_hex_identity_dict_with_nested_dict(): assert result["identity"] == "41 4243" +@pytest.mark.asyncio +async def test_init_if_unknown_network_is_valid(mocker): + """Tests __init__ if passed network unknown and is valid.""" + # Preps + fake_valid_endpoint = "wss://blabla.net" + mocker.patch.object(async_subtensor, "AsyncSubstrateInterface") + + # Call + subtensor = AsyncSubtensor(fake_valid_endpoint) + + # Asserts + assert subtensor.chain_endpoint == fake_valid_endpoint + assert subtensor.network == "custom" + + +@pytest.mark.asyncio +async def test_init_if_unknown_network_is_known_endpoint(mocker): + """Tests __init__ if passed network unknown and is valid.""" + # Preps + fake_valid_endpoint = "ws://127.0.0.1:9944" + mocker.patch.object(async_subtensor, "AsyncSubstrateInterface") + + # Call + subtensor = AsyncSubtensor(fake_valid_endpoint) + + # Asserts + assert subtensor.chain_endpoint == fake_valid_endpoint + assert subtensor.network == "local" + + +@pytest.mark.asyncio +async def test_init_if_unknown_network_is_not_valid(mocker): + """Tests __init__ if passed network unknown and isn't valid.""" + # Preps + mocker.patch.object(async_subtensor, "AsyncSubstrateInterface") + + # Call + subtensor = AsyncSubtensor("blabla-net") + + # Asserts + assert ( + subtensor.chain_endpoint + == async_subtensor.NETWORK_MAP[async_subtensor.DEFAULTS.subtensor.network] + ) + assert subtensor.network == async_subtensor.DEFAULTS.subtensor.network + + def test__str__return(subtensor): """Simply tests the result if printing subtensor instance.""" # Asserts @@ -100,6 +148,38 @@ async def test_async_subtensor_magic_methods(mocker): fake_async_substrate.close.assert_awaited_once() +@pytest.mark.parametrize( + "error", + [ + ConnectionRefusedError, + async_subtensor.ssl.SSLError, + async_subtensor.TimeoutException, + ], +) +@pytest.mark.asyncio +async def test_async_subtensor_aenter_connection_refused_error( + subtensor, mocker, error +): + """Tests __aenter__ method handling all errors.""" + # Preps + fake_async_substrate = mocker.AsyncMock( + autospec=async_subtensor.AsyncSubstrateInterface, + __aenter__=mocker.AsyncMock(side_effect=error), + ) + mocker.patch.object( + async_subtensor, "AsyncSubstrateInterface", return_value=fake_async_substrate + ) + # Call + subtensor = async_subtensor.AsyncSubtensor(network="local") + + with pytest.raises(ConnectionError): + async with subtensor: + pass + + # Asserts + fake_async_substrate.__aenter__.assert_called_once() + + @pytest.mark.asyncio async def test_encode_params(subtensor, mocker): """Tests encode_params happy path.""" @@ -351,7 +431,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( @@ -2150,3 +2230,436 @@ async def test_blocks_since_last_update_no_last_update(subtensor, mocker): param_name="LastUpdate", netuid=fake_netuid ) assert result is None + + +@pytest.mark.asyncio +async def test_transfer_success(subtensor, mocker): + """Tests transfer when the transfer is successful.""" + # Preps + fake_wallet = mocker.Mock() + fake_destination = "destination_address" + fake_amount = 100.0 + fake_transfer_all = False + + mocked_transfer_extrinsic = mocker.AsyncMock(return_value=True) + mocker.patch.object( + async_subtensor, "transfer_extrinsic", mocked_transfer_extrinsic + ) + + mocked_balance_from_tao = mocker.Mock() + mocker.patch.object( + async_subtensor.Balance, "from_tao", return_value=mocked_balance_from_tao + ) + + # Call + result = await subtensor.transfer( + wallet=fake_wallet, + destination=fake_destination, + amount=fake_amount, + transfer_all=fake_transfer_all, + ) + + # Asserts + mocked_transfer_extrinsic.assert_awaited_once() + mocked_transfer_extrinsic.assert_called_once_with( + subtensor, + fake_wallet, + fake_destination, + mocked_balance_from_tao, + fake_transfer_all, + ) + assert result == mocked_transfer_extrinsic.return_value + + +@pytest.mark.asyncio +async def test_register_success(subtensor, mocker): + """Tests register when there is enough balance and registration succeeds.""" + # Preps + fake_wallet = mocker.Mock(autospec=async_subtensor.Wallet) + fake_wallet.coldkeypub.ss58_address = "wallet_address" + fake_netuid = 1 + fake_block_hash = "block_hash" + fake_recycle_amount = 100 + fake_balance = 200 + + mocked_get_block_hash = mocker.AsyncMock(return_value=fake_block_hash) + subtensor.get_block_hash = mocked_get_block_hash + + mocked_get_hyperparameter = mocker.AsyncMock(return_value=str(fake_recycle_amount)) + subtensor.get_hyperparameter = mocked_get_hyperparameter + + mocked_get_balance = mocker.AsyncMock( + return_value={fake_wallet.coldkeypub.ss58_address: fake_balance} + ) + subtensor.get_balance = mocked_get_balance + + mocked_balance_from_rao = mocker.Mock(return_value=fake_recycle_amount) + mocker.patch.object(async_subtensor.Balance, "from_rao", mocked_balance_from_rao) + + # Call + result = await subtensor.register(wallet=fake_wallet, netuid=fake_netuid) + + # Asserts + mocked_get_block_hash.assert_called_once() + mocked_get_hyperparameter.assert_called_once_with( + param_name="Burn", netuid=fake_netuid, reuse_block=True + ) + mocked_get_balance.assert_called_once_with( + fake_wallet.coldkeypub.ss58_address, block_hash=fake_block_hash + ) + assert result is True + + +@pytest.mark.asyncio +async def test_register_insufficient_balance(subtensor, mocker): + """Tests register when the wallet balance is insufficient.""" + # Preps + fake_wallet = mocker.Mock(autospec=async_subtensor.Wallet) + fake_wallet.coldkeypub.ss58_address = "wallet_address" + fake_netuid = 1 + fake_block_hash = "block_hash" + fake_recycle_amount = 200 + fake_balance = 100 + + mocked_get_block_hash = mocker.AsyncMock(return_value=fake_block_hash) + subtensor.get_block_hash = mocked_get_block_hash + + mocked_get_hyperparameter = mocker.AsyncMock(return_value=str(fake_recycle_amount)) + subtensor.get_hyperparameter = mocked_get_hyperparameter + + mocked_get_balance = mocker.AsyncMock( + return_value={fake_wallet.coldkeypub.ss58_address: fake_balance} + ) + subtensor.get_balance = mocked_get_balance + + mocked_balance_from_rao = mocker.Mock(return_value=fake_recycle_amount) + mocker.patch.object(async_subtensor.Balance, "from_rao", mocked_balance_from_rao) + + # Call + result = await subtensor.register(wallet=fake_wallet, netuid=fake_netuid) + + # Asserts + mocked_get_block_hash.assert_called_once() + mocked_get_hyperparameter.assert_called_once_with( + param_name="Burn", netuid=fake_netuid, reuse_block=True + ) + mocked_get_balance.assert_called_once_with( + fake_wallet.coldkeypub.ss58_address, block_hash=fake_block_hash + ) + assert result is False + + +@pytest.mark.asyncio +async def test_register_balance_retrieval_error(subtensor, mocker): + """Tests register when there is an error retrieving the balance.""" + # Preps + fake_wallet = mocker.Mock(autospec=async_subtensor.Wallet) + fake_wallet.coldkeypub.ss58_address = "wallet_address" + fake_netuid = 1 + fake_block_hash = "block_hash" + fake_recycle_amount = 100 + + mocked_get_block_hash = mocker.AsyncMock(return_value=fake_block_hash) + subtensor.get_block_hash = mocked_get_block_hash + + mocked_get_hyperparameter = mocker.AsyncMock(return_value=str(fake_recycle_amount)) + subtensor.get_hyperparameter = mocked_get_hyperparameter + + mocked_get_balance = mocker.AsyncMock(return_value={}) + subtensor.get_balance = mocked_get_balance + + # Call + result = await subtensor.register(wallet=fake_wallet, netuid=fake_netuid) + + # Asserts + mocked_get_block_hash.assert_called_once() + mocked_get_hyperparameter.assert_called_once_with( + param_name="Burn", netuid=fake_netuid, reuse_block=True + ) + mocked_get_balance.assert_called_once_with( + fake_wallet.coldkeypub.ss58_address, block_hash=fake_block_hash + ) + assert result is False + + +@pytest.mark.asyncio +async def test_pow_register_success(subtensor, mocker): + """Tests pow_register when the registration is successful.""" + # Preps + fake_wallet = mocker.Mock(autospec=async_subtensor.Wallet) + fake_netuid = 1 + fake_processors = 4 + fake_update_interval = 10 + fake_output_in_place = True + fake_verbose = True + fake_use_cuda = False + fake_dev_id = 0 + fake_threads_per_block = 128 + + mocked_register_extrinsic = mocker.AsyncMock(return_value=True) + mocker.patch.object( + async_subtensor, "register_extrinsic", mocked_register_extrinsic + ) + + # Call + result = await subtensor.pow_register( + wallet=fake_wallet, + netuid=fake_netuid, + processors=fake_processors, + update_interval=fake_update_interval, + output_in_place=fake_output_in_place, + verbose=fake_verbose, + use_cuda=fake_use_cuda, + dev_id=fake_dev_id, + threads_per_block=fake_threads_per_block, + ) + + # Asserts + mocked_register_extrinsic.assert_awaited_once() + mocked_register_extrinsic.assert_called_once_with( + subtensor=subtensor, + wallet=fake_wallet, + netuid=fake_netuid, + tpb=fake_threads_per_block, + update_interval=fake_update_interval, + num_processes=fake_processors, + cuda=fake_use_cuda, + dev_id=fake_dev_id, + output_in_place=fake_output_in_place, + log_verbose=fake_verbose, + ) + assert result == mocked_register_extrinsic.return_value + + +@pytest.mark.asyncio +async def test_set_weights_success(subtensor, mocker): + """Tests set_weights with successful weight setting on the first try.""" + # Preps + fake_wallet = mocker.Mock(autospec=async_subtensor.Wallet) + fake_netuid = 1 + fake_uids = [1, 2, 3] + fake_weights = [0.3, 0.5, 0.2] + max_retries = 1 + + mocked_get_uid_for_hotkey_on_subnet = mocker.AsyncMock(return_value=fake_netuid) + subtensor.get_uid_for_hotkey_on_subnet = mocked_get_uid_for_hotkey_on_subnet + + mocked_blocks_since_last_update = mocker.AsyncMock(return_value=2) + subtensor.blocks_since_last_update = mocked_blocks_since_last_update + + mocked_weights_rate_limit = mocker.AsyncMock(return_value=1) + subtensor.weights_rate_limit = mocked_weights_rate_limit + + mocked_set_weights_extrinsic = mocker.AsyncMock(return_value=(True, "Success")) + mocker.patch.object( + async_subtensor, "set_weights_extrinsic", mocked_set_weights_extrinsic + ) + + # Call + result, message = await subtensor.set_weights( + wallet=fake_wallet, + netuid=fake_netuid, + uids=fake_uids, + weights=fake_weights, + max_retries=max_retries, + ) + + # Asserts + mocked_get_uid_for_hotkey_on_subnet.assert_called_once_with( + fake_wallet.hotkey.ss58_address, fake_netuid + ) + mocked_blocks_since_last_update.assert_called_once_with( + fake_netuid, mocked_get_uid_for_hotkey_on_subnet.return_value + ) + mocked_weights_rate_limit.assert_called_once_with(fake_netuid) + mocked_set_weights_extrinsic.assert_called_once_with( + subtensor=subtensor, + wallet=fake_wallet, + netuid=fake_netuid, + uids=fake_uids, + version_key=async_subtensor.version_as_int, + wait_for_finalization=False, + wait_for_inclusion=False, + weights=fake_weights, + ) + mocked_weights_rate_limit.assert_called_once_with(fake_netuid) + assert result is True + assert message == "Success" + + +@pytest.mark.asyncio +async def test_set_weights_with_exception(subtensor, mocker): + """Tests set_weights when set_weights_extrinsic raises an exception.""" + # Preps + fake_wallet = mocker.Mock(autospec=async_subtensor.Wallet) + fake_netuid = 1 + fake_uids = [1, 2, 3] + fake_weights = [0.3, 0.5, 0.2] + fake_uid = 10 + max_retries = 1 + + mocked_get_uid_for_hotkey_on_subnet = mocker.AsyncMock(return_value=fake_uid) + subtensor.get_uid_for_hotkey_on_subnet = mocked_get_uid_for_hotkey_on_subnet + + mocked_blocks_since_last_update = mocker.AsyncMock(return_value=10) + subtensor.blocks_since_last_update = mocked_blocks_since_last_update + + mocked_weights_rate_limit = mocker.AsyncMock(return_value=5) + subtensor.weights_rate_limit = mocked_weights_rate_limit + + mocked_set_weights_extrinsic = mocker.AsyncMock( + side_effect=Exception("Test exception") + ) + mocker.patch.object( + async_subtensor, "set_weights_extrinsic", mocked_set_weights_extrinsic + ) + + # Call + result, message = await subtensor.set_weights( + wallet=fake_wallet, + netuid=fake_netuid, + uids=fake_uids, + weights=fake_weights, + max_retries=max_retries, + ) + + # Asserts + assert mocked_get_uid_for_hotkey_on_subnet.call_count == 1 + assert mocked_blocks_since_last_update.call_count == 1 + assert mocked_weights_rate_limit.call_count == 1 + assert mocked_set_weights_extrinsic.call_count == max_retries + assert result is False + assert message == "No attempt made. Perhaps it is too soon to set weights!" + + +@pytest.mark.asyncio +async def test_root_set_weights_success(subtensor, mocker): + """Tests root_set_weights when the setting of weights is successful.""" + # Preps + fake_wallet = mocker.Mock(autospec=async_subtensor.Wallet) + fake_netuids = [1, 2, 3] + fake_weights = [0.3, 0.5, 0.2] + + mocked_set_root_weights_extrinsic = mocker.AsyncMock() + mocker.patch.object( + async_subtensor, "set_root_weights_extrinsic", mocked_set_root_weights_extrinsic + ) + + mocked_np_array_netuids = mocker.Mock(autospec=async_subtensor.np.ndarray) + mocked_np_array_weights = mocker.Mock(autospec=async_subtensor.np.ndarray) + mocker.patch.object( + async_subtensor.np, + "array", + side_effect=[mocked_np_array_netuids, mocked_np_array_weights], + ) + + # Call + result = await subtensor.root_set_weights( + wallet=fake_wallet, + netuids=fake_netuids, + weights=fake_weights, + ) + + # Asserts + mocked_set_root_weights_extrinsic.assert_awaited_once() + mocked_set_root_weights_extrinsic.assert_called_once_with( + subtensor=subtensor, + wallet=fake_wallet, + netuids=mocked_np_array_netuids, + weights=mocked_np_array_weights, + version_key=0, + wait_for_finalization=True, + wait_for_inclusion=True, + ) + assert result == mocked_set_root_weights_extrinsic.return_value + + +@pytest.mark.asyncio +async def test_commit_weights_success(subtensor, mocker): + """Tests commit_weights when the weights are committed successfully.""" + # Preps + fake_wallet = mocker.Mock(autospec=async_subtensor.Wallet) + fake_netuid = 1 + fake_salt = [12345, 67890] + fake_uids = [1, 2, 3] + fake_weights = [100, 200, 300] + max_retries = 3 + + mocked_generate_weight_hash = mocker.Mock(return_value="fake_commit_hash") + mocker.patch.object( + async_subtensor, "generate_weight_hash", mocked_generate_weight_hash + ) + + mocked_commit_weights_extrinsic = mocker.AsyncMock(return_value=(True, "Success")) + mocker.patch.object( + async_subtensor, "commit_weights_extrinsic", mocked_commit_weights_extrinsic + ) + + # Call + result, message = await subtensor.commit_weights( + wallet=fake_wallet, + netuid=fake_netuid, + salt=fake_salt, + uids=fake_uids, + weights=fake_weights, + max_retries=max_retries, + ) + + # Asserts + mocked_generate_weight_hash.assert_called_once_with( + address=fake_wallet.hotkey.ss58_address, + netuid=fake_netuid, + uids=fake_uids, + values=fake_weights, + salt=fake_salt, + version_key=async_subtensor.version_as_int, + ) + mocked_commit_weights_extrinsic.assert_called_once_with( + subtensor=subtensor, + wallet=fake_wallet, + netuid=fake_netuid, + commit_hash="fake_commit_hash", + wait_for_inclusion=False, + wait_for_finalization=False, + ) + assert result is True + assert message == "Success" + + +@pytest.mark.asyncio +async def test_commit_weights_with_exception(subtensor, mocker): + """Tests commit_weights when an exception is raised during weight commitment.""" + # Preps + fake_wallet = mocker.Mock(autospec=async_subtensor.Wallet) + fake_netuid = 1 + fake_salt = [12345, 67890] + fake_uids = [1, 2, 3] + fake_weights = [100, 200, 300] + max_retries = 1 + + mocked_generate_weight_hash = mocker.Mock(return_value="fake_commit_hash") + mocker.patch.object( + async_subtensor, "generate_weight_hash", mocked_generate_weight_hash + ) + + mocked_commit_weights_extrinsic = mocker.AsyncMock( + side_effect=Exception("Test exception") + ) + mocker.patch.object( + async_subtensor, "commit_weights_extrinsic", mocked_commit_weights_extrinsic + ) + + # Call + result, message = await subtensor.commit_weights( + wallet=fake_wallet, + netuid=fake_netuid, + salt=fake_salt, + uids=fake_uids, + weights=fake_weights, + max_retries=max_retries, + ) + + # Asserts + assert mocked_commit_weights_extrinsic.call_count == max_retries + assert result is False + assert "No attempt made. Perhaps it is too soon to commit weights!" in message diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index e3f573c67f..602a3027d8 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() diff --git a/tests/unit_tests/utils/test_utils.py b/tests/unit_tests/utils/test_utils.py index a01b42f31d..eda2eeb100 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