diff --git a/.github/workflows/monitor_requirements_size_master.yml b/.github/workflows/monitor_requirements_size_master.yml index 301767ce7e..e9efa8dcd3 100644 --- a/.github/workflows/monitor_requirements_size_master.yml +++ b/.github/workflows/monitor_requirements_size_master.yml @@ -6,8 +6,8 @@ name: Monitor SDK Requirements Size on: pull_request: - types: [opened] - branches: [master] + types: [opened, labeled] + branches: [master, staging] permissions: pull-requests: write @@ -15,6 +15,7 @@ permissions: jobs: measure-venv: + if: github.event_name == 'pull_request' && github.base_ref == 'master' || contains( github.event.pull_request.labels.*.name, 'show-venv-size') runs-on: ubuntu-latest strategy: matrix: @@ -54,7 +55,6 @@ jobs: esac comment-on-pr: - if: github.event_name == 'pull_request' && github.base_ref == 'master' needs: measure-venv runs-on: ubuntu-latest steps: diff --git a/CHANGELOG.md b/CHANGELOG.md index 69844e7def..1e7ccd6c02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## 9.5.0 /2025-05-15 + +* Add manual way to show the size of virtual environments in the PR by @basfroman in https://github.com/opentensor/bittensor/pull/2875 +* Improve `Monitor SDK Requirements Size` workflow by @basfroman in https://github.com/opentensor/bittensor/pull/2878 +* Add subtensor.is_subnet_active method by @basfroman in https://github.com/opentensor/bittensor/pull/2877 +* Using `hotkey` instead of `coldkey` to sign extrinsic in `serve_axon` by @basfroman in https://github.com/opentensor/bittensor/pull/2879 +* Rename argument `fallback_chains` to `fallback_endpoints` by @basfroman in https://github.com/opentensor/bittensor/pull/2880 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v9.5.0...v9.6.0 + ## 9.5.0 /2025-05-12 ## What's Changed diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index 1402e4c683..0d99e887c3 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -115,7 +115,7 @@ def __init__( network: Optional[str] = None, config: Optional["Config"] = None, log_verbose: bool = False, - fallback_chains: Optional[list[str]] = None, + fallback_endpoints: Optional[list[str]] = None, retry_forever: bool = False, _mock: bool = False, ): @@ -123,11 +123,11 @@ def __init__( Initializes an instance of the AsyncSubtensor class. Arguments: - network (str): The network name or type to connect to. - config (Optional[Config]): Configuration object for the AsyncSubtensor instance. - log_verbose (bool): Enables or disables verbose logging. - fallback_chains (list): List of fallback chains endpoints to use if no network is specified. Defaults to `None`. - retry_forever (bool): Whether to retry forever on connection errors. Defaults to `False`. + network: The network name or type to connect to. + config: Configuration object for the AsyncSubtensor instance. + log_verbose: Enables or disables verbose logging. + fallback_endpoints: List of fallback endpoints to use if default or provided network is not available. Defaults to `None`. + retry_forever: Whether to retry forever on connection errors. Defaults to `False`. _mock: Whether this is a mock instance. Mainly just for use in testing. Raises: @@ -148,7 +148,9 @@ def __init__( f"chain_endpoint: [blue]{self.chain_endpoint}[/blue]..." ) self.substrate = self._get_substrate( - fallback_chains=fallback_chains, retry_forever=retry_forever, _mock=_mock + fallback_endpoints=fallback_endpoints, + retry_forever=retry_forever, + _mock=_mock, ) if self.log_verbose: logging.info( @@ -284,24 +286,24 @@ async def get_hyperparameter( def _get_substrate( self, - fallback_chains: Optional[list[str]] = None, + fallback_endpoints: Optional[list[str]] = None, retry_forever: bool = False, _mock: bool = False, ) -> Union[AsyncSubstrateInterface, RetryAsyncSubstrate]: """Creates the Substrate instance based on provided arguments. Arguments: - fallback_chains (list): List of fallback chains endpoints to use if no network is specified. Defaults to `None`. - retry_forever (bool): Whether to retry forever on connection errors. Defaults to `False`. + fallback_endpoints: List of fallback endpoints to use if default or provided network is not available. Defaults to `None`. + retry_forever: Whether to retry forever on connection errors. Defaults to `False`. _mock: Whether this is a mock instance. Mainly just for use in testing. Returns: the instance of the SubstrateInterface or RetrySyncSubstrate class. """ - if fallback_chains or retry_forever: + if fallback_endpoints or retry_forever: return RetryAsyncSubstrate( url=self.chain_endpoint, - fallback_chains=fallback_chains, + fallback_chains=fallback_endpoints, ss58_format=SS58_FORMAT, type_registry=TYPE_REGISTRY, retry_forever=retry_forever, @@ -2582,6 +2584,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/extrinsics/asyncex/serving.py b/bittensor/core/extrinsics/asyncex/serving.py index c37c612191..b942a88dc1 100644 --- a/bittensor/core/extrinsics/asyncex/serving.py +++ b/bittensor/core/extrinsics/asyncex/serving.py @@ -61,6 +61,7 @@ async def do_serve_axon( wallet=wallet, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + sign_with="hotkey", period=period, ) return success, message @@ -140,7 +141,7 @@ async def serve_extrinsic( f"Serving axon with: [blue]AxonInfo({wallet.hotkey.ss58_address}, {ip}:{port})[/blue] -> " f"[green]{subtensor.network}:{netuid}[/green]" ) - success, message = do_serve_axon( + success, message = await do_serve_axon( subtensor=subtensor, wallet=wallet, call_params=params, diff --git a/bittensor/core/extrinsics/serving.py b/bittensor/core/extrinsics/serving.py index d004fd7dba..b49196bf89 100644 --- a/bittensor/core/extrinsics/serving.py +++ b/bittensor/core/extrinsics/serving.py @@ -60,6 +60,7 @@ def do_serve_axon( wallet=wallet, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + sign_with="hotkey", period=period, ) return success, message diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 63634d9053..5e356f9666 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -117,7 +117,7 @@ def __init__( network: Optional[str] = None, config: Optional["Config"] = None, log_verbose: bool = False, - fallback_chains: Optional[list[str]] = None, + fallback_endpoints: Optional[list[str]] = None, retry_forever: bool = False, _mock: bool = False, ): @@ -125,11 +125,11 @@ def __init__( Initializes an instance of the Subtensor class. Arguments: - network (str): The network name or type to connect to. - config (Optional[Config]): Configuration object for the AsyncSubtensor instance. - log_verbose (bool): Enables or disables verbose logging. - fallback_chains (list): List of fallback chains endpoints to use if no network is specified. Defaults to `None`. - retry_forever (bool): Whether to retry forever on connection errors. Defaults to `False`. + network: The network name or type to connect to. + config: Configuration object for the AsyncSubtensor instance. + log_verbose: Enables or disables verbose logging. + fallback_endpoints: List of fallback endpoints to use if default or provided network is not available. Defaults to `None`. + retry_forever: Whether to retry forever on connection errors. Defaults to `False`. _mock: Whether this is a mock instance. Mainly just for use in testing. Raises: @@ -148,7 +148,9 @@ def __init__( f"chain_endpoint: [blue]{self.chain_endpoint}[/blue]> ..." ) self.substrate = self._get_substrate( - fallback_chains=fallback_chains, retry_forever=retry_forever, _mock=_mock + fallback_endpoints=fallback_endpoints, + retry_forever=retry_forever, + _mock=_mock, ) if self.log_verbose: logging.info( @@ -167,28 +169,28 @@ def close(self): def _get_substrate( self, - fallback_chains: Optional[list[str]] = None, + fallback_endpoints: Optional[list[str]] = None, retry_forever: bool = False, _mock: bool = False, ) -> Union[SubstrateInterface, RetrySyncSubstrate]: """Creates the Substrate instance based on provided arguments. Arguments: - fallback_chains (list): List of fallback chains endpoints to use if no network is specified. Defaults to `None`. - retry_forever (bool): Whether to retry forever on connection errors. Defaults to `False`. + fallback_endpoints: List of fallback chains endpoints to use if main network isn't available. Defaults to `None`. + retry_forever: Whether to retry forever on connection errors. Defaults to `False`. _mock: Whether this is a mock instance. Mainly just for use in testing. Returns: the instance of the SubstrateInterface or RetrySyncSubstrate class. """ - if fallback_chains or retry_forever: + if fallback_endpoints or retry_forever: return RetrySyncSubstrate( url=self.chain_endpoint, ss58_format=SS58_FORMAT, type_registry=TYPE_REGISTRY, use_remote_preset=True, chain_name="Bittensor", - fallback_chains=fallback_chains, + fallback_chains=fallback_endpoints, retry_forever=retry_forever, _mock=_mock, ) @@ -2023,6 +2025,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/__init__.py b/bittensor/core/subtensor_api/__init__.py index be5a61638a..2d021cd1ce 100644 --- a/bittensor/core/subtensor_api/__init__.py +++ b/bittensor/core/subtensor_api/__init__.py @@ -25,9 +25,9 @@ class SubtensorApi: network: The network to connect to. Defaults to `None` -> "finney". config: Bittensor configuration object. Defaults to `None`. legacy_methods: If `True`, all methods from the Subtensor class will be added to the root level of this class. - fallback_chains (list): List of fallback chains to use if no network is specified. Defaults to `None`. - retry_forever (bool): Whether to retry forever on connection errors. Defaults to `False`. - log_verbose (bool): Enables or disables verbose logging. + fallback_endpoints: List of fallback endpoints to use if default or provided network is not available. Defaults to `None`. + retry_forever: Whether to retry forever on connection errors. Defaults to `False`. + log_verbose: Enables or disables verbose logging. mock: Whether this is a mock instance. Mainly just for use in testing. Example: @@ -54,10 +54,15 @@ class SubtensorApi: subtensor = bt.SubtensorApi(legacy_methods=True) print(subtensor.bonds(0)) - # using `fallback_chains` or `retry_forever` + # using `fallback_endpoints` or `retry_forever` import bittensor as bt - + subtensor = bt.SubtensorApi( + network="finney", + fallback_endpoints=["wss://localhost:9945", "wss://some-other-endpoint:9945"], + retry_forever=True, + ) + print(subtensor.block) """ def __init__( @@ -66,13 +71,13 @@ def __init__( config: Optional["Config"] = None, async_subtensor: bool = False, legacy_methods: bool = False, - fallback_chains: Optional[list[str]] = None, + fallback_endpoints: Optional[list[str]] = None, retry_forever: bool = False, log_verbose: bool = False, mock: bool = False, ): self.network = network - self._fallback_chains = fallback_chains + self._fallback_endpoints = fallback_endpoints self._retry_forever = retry_forever self._mock = mock self.log_verbose = log_verbose @@ -111,7 +116,7 @@ def _get_subtensor(self) -> Union["_Subtensor", "_AsyncSubtensor"]: network=self.network, config=self._config, log_verbose=self.log_verbose, - fallback_chains=self._fallback_chains, + fallback_endpoints=self._fallback_endpoints, retry_forever=self._retry_forever, _mock=self._mock, ) @@ -122,7 +127,7 @@ def _get_subtensor(self) -> Union["_Subtensor", "_AsyncSubtensor"]: network=self.network, config=self._config, log_verbose=self.log_verbose, - fallback_chains=self._fallback_chains, + fallback_endpoints=self._fallback_endpoints, retry_forever=self._retry_forever, _mock=self._mock, ) 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/pyproject.toml b/pyproject.toml index a9b9db6a01..4b3d8fbe99 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "bittensor" -version = "9.5.0" +version = "9.6.0" description = "Bittensor" readme = "README.md" authors = [ 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..b1020d7648 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -1492,6 +1492,7 @@ def test_do_serve_axon_is_success( wallet=fake_wallet, wait_for_inclusion=fake_wait_for_inclusion, wait_for_finalization=fake_wait_for_finalization, + sign_with="hotkey", period=None, ) @@ -1530,6 +1531,7 @@ def test_do_serve_axon_is_not_success(subtensor, fake_wallet, mocker, fake_call_ wallet=fake_wallet, wait_for_inclusion=fake_wait_for_inclusion, wait_for_finalization=fake_wait_for_finalization, + sign_with="hotkey", period=None, ) @@ -1568,6 +1570,7 @@ def test_do_serve_axon_no_waits(subtensor, fake_wallet, mocker, fake_call_params wallet=fake_wallet, wait_for_inclusion=fake_wait_for_inclusion, wait_for_finalization=fake_wait_for_finalization, + sign_with="hotkey", period=None, ) assert result == (True, "") @@ -3633,3 +3636,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