diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index 1402e4c683..ea3b852b3d 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -2582,6 +2582,35 @@ async def is_hotkey_registered_on_subnet( is not None ) + async def is_subnet_active( + self, + netuid: int, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> bool: + """Verify if subnet with provided netuid is active. + + Args: + netuid (int): The unique identifier of the subnet. + block (Optional[int]): The blockchain block number for the query. + block_hash (Optional[str]): The blockchain block_hash representation of block id. + reuse_block (bool): Whether to reuse the last-used block hash. + + Returns: + True if subnet is active, False otherwise. + + This means whether the `start_call` was initiated or not. + """ + query = await self.query_subtensor( + name="FirstEmissionBlockNumber", + block=block, + block_hash=block_hash, + reuse_block=reuse_block, + params=[netuid], + ) + return True if query and query.value > 0 else False + async def last_drand_round(self) -> Optional[int]: """ Retrieves the last drand round emitted in bittensor. This corresponds when committed weights will be revealed. diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 63634d9053..5c347774cf 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -2023,6 +2023,25 @@ def is_hotkey_registered_on_subnet( is not None ) + def is_subnet_active(self, netuid: int, block: Optional[int] = None) -> bool: + """Verify if subnet with provided netuid is active. + + Args: + netuid (int): The unique identifier of the subnet. + block (Optional[int]): The blockchain block number for the query. + + Returns: + True if subnet is active, False otherwise. + + This means whether the `start_call` was initiated or not. + """ + query = self.query_subtensor( + name="FirstEmissionBlockNumber", + block=block, + params=[netuid], + ) + return True if query and query.value > 0 else False + def last_drand_round(self) -> Optional[int]: """ Retrieves the last drand round emitted in bittensor. This corresponds when committed weights will be revealed. diff --git a/bittensor/core/subtensor_api/subnets.py b/bittensor/core/subtensor_api/subnets.py index c3333daf30..8b8c9121e7 100644 --- a/bittensor/core/subtensor_api/subnets.py +++ b/bittensor/core/subtensor_api/subnets.py @@ -32,6 +32,7 @@ def __init__(self, subtensor: Union["_Subtensor", "_AsyncSubtensor"]): self.get_uid_for_hotkey_on_subnet = subtensor.get_uid_for_hotkey_on_subnet self.immunity_period = subtensor.immunity_period self.is_hotkey_registered_on_subnet = subtensor.is_hotkey_registered_on_subnet + self.is_subnet_active = subtensor.is_subnet_active self.max_weight_limit = subtensor.max_weight_limit self.min_allowed_weights = subtensor.min_allowed_weights self.recycle = subtensor.recycle diff --git a/bittensor/core/subtensor_api/utils.py b/bittensor/core/subtensor_api/utils.py index fdfde50697..3f8cc7d36d 100644 --- a/bittensor/core/subtensor_api/utils.py +++ b/bittensor/core/subtensor_api/utils.py @@ -108,6 +108,7 @@ def add_legacy_methods(subtensor: "SubtensorApi"): subtensor.is_hotkey_registered_on_subnet = ( subtensor._subtensor.is_hotkey_registered_on_subnet ) + subtensor.is_subnet_active = subtensor._subtensor.is_subnet_active subtensor.last_drand_round = subtensor._subtensor.last_drand_round subtensor.log_verbose = subtensor._subtensor.log_verbose subtensor.max_weight_limit = subtensor._subtensor.max_weight_limit diff --git a/tests/e2e_tests/utils/e2e_test_utils.py b/tests/e2e_tests/utils/e2e_test_utils.py index 745c8977ab..8e5254535f 100644 --- a/tests/e2e_tests/utils/e2e_test_utils.py +++ b/tests/e2e_tests/utils/e2e_test_utils.py @@ -229,7 +229,7 @@ def validator(self, wallet, netuid): def wait_to_start_call( - subtensor: "bittensor.Subtensor", + subtensor: "bittensor.SubtensorApi", subnet_owner_wallet: "bittensor.Wallet", netuid: int, in_blocks: int = 10, @@ -242,6 +242,11 @@ def wait_to_start_call( f"Current block: [blue]{subtensor.block}[/blue]." ) + # make sure subnet isn't active + assert subtensor.subnets.is_subnet_active(netuid) is False, ( + "Subnet is already active." + ) + # make sure we passed start_call limit subtensor.wait_for_block(subtensor.block + in_blocks + 1) status, message = subtensor.start_call( @@ -251,6 +256,11 @@ def wait_to_start_call( wait_for_finalization=True, ) assert status, message + # make sure subnet is active + assert subtensor.subnets.is_subnet_active(netuid), ( + "Subnet did not activated after start call." + ) + return True diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index 9085bf1122..0d902f0c73 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -3633,3 +3633,32 @@ def test_get_subnet_validator_permits_is_none(subtensor, mocker): ) assert result is None + + +@pytest.mark.parametrize( + "query_return, expected", + [ + [111, True], + [0, False], + ], +) +def test_is_subnet_active(subtensor, mocker, query_return, expected): + # preps + netuid = mocker.Mock() + block = mocker.Mock() + mocked_query_subtensor = mocker.MagicMock( + return_value=mocker.Mock(value=query_return) + ) + subtensor.query_subtensor = mocked_query_subtensor + + # call + result = subtensor.is_subnet_active(netuid=netuid, block=block) + + # Asserts + mocked_query_subtensor.assert_called_once_with( + name="FirstEmissionBlockNumber", + block=block, + params=[netuid], + ) + + assert result == expected