diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index c82bb0ae2e..fc59fc121a 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -1403,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( @@ -1462,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/tests/unit_tests/test_async_subtensor.py b/tests/unit_tests/test_async_subtensor.py index b4affe0ccd..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.""" @@ -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