From d1a4c462fbc16bc5b08b9c2aa3840f5eb8729a95 Mon Sep 17 00:00:00 2001 From: Orochi Date: Thu, 19 Feb 2026 23:37:52 +0700 Subject: [PATCH 001/118] Fix logging.info and other state transitions in LoggingMachine. Resolved issue #1836 by explicitly setting logger level and using methods for transitions to ensure state consistency. --- bittensor/utils/btlogging/loggingmachine.py | 22 +++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/bittensor/utils/btlogging/loggingmachine.py b/bittensor/utils/btlogging/loggingmachine.py index 0feacf4f93..f72b9c591c 100644 --- a/bittensor/utils/btlogging/loggingmachine.py +++ b/bittensor/utils/btlogging/loggingmachine.py @@ -120,13 +120,21 @@ class LoggingMachine(StateMachine, Logger): | Info.to(Warning) ) - disable_trace = Trace.to(Default) + def disable_trace(self): + """Disables trace logging by transitioning back to Default.""" + self.enable_default() - disable_debug = Debug.to(Default) + def disable_debug(self): + """Disables debug logging by transitioning back to Default.""" + self.enable_default() - disable_warning = Warning.to(Default) + def disable_warning(self): + """Disables warning logging by transitioning back to Default.""" + self.enable_default() - disable_info = Info.to(Default) + def disable_info(self): + """Disables info logging by transitioning back to Default.""" + self.enable_default() disable_logging = ( Trace.to(Disabled) @@ -405,6 +413,7 @@ def before_enable_default(self): """Logs status before enable Default.""" self._logger.info("Enabling default logging (Warning level)") self._logger.setLevel(stdlogging.WARNING) + self._stream_formatter.set_trace(False) for logger in all_loggers(): if logger.name in self._primary_loggers: continue @@ -417,6 +426,7 @@ def after_enable_default(self): def before_enable_warning(self): """Logs status before enable Warning.""" self._logger.info("Enabling warning.") + self._logger.setLevel(stdlogging.WARNING) self._stream_formatter.set_trace(True) for logger in all_loggers(): logger.setLevel(stdlogging.WARNING) @@ -430,6 +440,7 @@ def before_enable_info(self): """Logs status before enable info.""" self._logger.info("Enabling info logging.") self._logger.setLevel(stdlogging.INFO) + self._stream_formatter.set_trace(True) for logger in all_loggers(): if logger.name in self._primary_loggers: continue @@ -443,6 +454,7 @@ def after_enable_info(self): def before_enable_trace(self): """Logs status before enable Trace.""" self._logger.info("Enabling trace.") + self._logger.setLevel(stdlogging.TRACE) self._stream_formatter.set_trace(True) for logger in all_loggers(): logger.setLevel(stdlogging.TRACE) @@ -465,12 +477,14 @@ def after_disable_trace(self): def before_enable_debug(self): """Logs status before enable Debug.""" self._logger.info("Enabling debug.") + self._logger.setLevel(stdlogging.DEBUG) self._stream_formatter.set_trace(True) for logger in all_loggers(): logger.setLevel(stdlogging.DEBUG) def before_enable_console(self): """Logs status before enable Console.""" + self._logger.setLevel(stdlogging.DEBUG) self._stream_formatter.set_trace(True) for logger in all_loggers(): logger.setLevel(stdlogging.DEBUG) From 330fa0220f5b9b6e01d1498b674010af8d55e8e0 Mon Sep 17 00:00:00 2001 From: ionodeionode Date: Thu, 5 Mar 2026 19:37:50 +0700 Subject: [PATCH 002/118] fix: add set_trace(False) in before_enable_default to reset formatter state (scoped down) --- bittensor/utils/btlogging/loggingmachine.py | 23 +++++---------------- 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/bittensor/utils/btlogging/loggingmachine.py b/bittensor/utils/btlogging/loggingmachine.py index f72b9c591c..63955f4b82 100644 --- a/bittensor/utils/btlogging/loggingmachine.py +++ b/bittensor/utils/btlogging/loggingmachine.py @@ -120,21 +120,13 @@ class LoggingMachine(StateMachine, Logger): | Info.to(Warning) ) - def disable_trace(self): - """Disables trace logging by transitioning back to Default.""" - self.enable_default() + disable_trace = Trace.to(Default) - def disable_debug(self): - """Disables debug logging by transitioning back to Default.""" - self.enable_default() + disable_debug = Debug.to(Default) - def disable_warning(self): - """Disables warning logging by transitioning back to Default.""" - self.enable_default() + disable_warning = Warning.to(Default) - def disable_info(self): - """Disables info logging by transitioning back to Default.""" - self.enable_default() + disable_info = Info.to(Default) disable_logging = ( Trace.to(Disabled) @@ -410,10 +402,10 @@ def after_transition(self, event, state): # Default Logging def before_enable_default(self): + self.set_trace(False) """Logs status before enable Default.""" self._logger.info("Enabling default logging (Warning level)") self._logger.setLevel(stdlogging.WARNING) - self._stream_formatter.set_trace(False) for logger in all_loggers(): if logger.name in self._primary_loggers: continue @@ -426,7 +418,6 @@ def after_enable_default(self): def before_enable_warning(self): """Logs status before enable Warning.""" self._logger.info("Enabling warning.") - self._logger.setLevel(stdlogging.WARNING) self._stream_formatter.set_trace(True) for logger in all_loggers(): logger.setLevel(stdlogging.WARNING) @@ -440,7 +431,6 @@ def before_enable_info(self): """Logs status before enable info.""" self._logger.info("Enabling info logging.") self._logger.setLevel(stdlogging.INFO) - self._stream_formatter.set_trace(True) for logger in all_loggers(): if logger.name in self._primary_loggers: continue @@ -454,7 +444,6 @@ def after_enable_info(self): def before_enable_trace(self): """Logs status before enable Trace.""" self._logger.info("Enabling trace.") - self._logger.setLevel(stdlogging.TRACE) self._stream_formatter.set_trace(True) for logger in all_loggers(): logger.setLevel(stdlogging.TRACE) @@ -477,14 +466,12 @@ def after_disable_trace(self): def before_enable_debug(self): """Logs status before enable Debug.""" self._logger.info("Enabling debug.") - self._logger.setLevel(stdlogging.DEBUG) self._stream_formatter.set_trace(True) for logger in all_loggers(): logger.setLevel(stdlogging.DEBUG) def before_enable_console(self): """Logs status before enable Console.""" - self._logger.setLevel(stdlogging.DEBUG) self._stream_formatter.set_trace(True) for logger in all_loggers(): logger.setLevel(stdlogging.DEBUG) From 58c07e7821515f21dfb7dd6d8ea5493fe8c7832a Mon Sep 17 00:00:00 2001 From: ionodeionode Date: Fri, 6 Mar 2026 12:45:39 +0700 Subject: [PATCH 003/118] fix: correct indentation and formatting for set_trace(False) --- bittensor/utils/btlogging/loggingmachine.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor/utils/btlogging/loggingmachine.py b/bittensor/utils/btlogging/loggingmachine.py index 63955f4b82..1575bd745c 100644 --- a/bittensor/utils/btlogging/loggingmachine.py +++ b/bittensor/utils/btlogging/loggingmachine.py @@ -402,7 +402,7 @@ def after_transition(self, event, state): # Default Logging def before_enable_default(self): - self.set_trace(False) + self.set_trace(False) """Logs status before enable Default.""" self._logger.info("Enabling default logging (Warning level)") self._logger.setLevel(stdlogging.WARNING) From 6e15b8235d3f52ae3b71124b55bd3ce4ebfb51d4 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 24 Mar 2026 14:39:20 -0700 Subject: [PATCH 004/118] clear_coldkey_swap_announcement async --- bittensor/core/async_subtensor.py | 45 +++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index d199c029fc..d3b6d9bac7 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -56,6 +56,7 @@ ) from bittensor.core.extrinsics.asyncex.coldkey_swap import ( announce_coldkey_swap_extrinsic, + clear_coldkey_swap_announcement_extrinsic, dispute_coldkey_swap_extrinsic, swap_coldkey_announced_extrinsic, ) @@ -7115,6 +7116,50 @@ async def dispute_coldkey_swap( wait_for_revealed_execution=wait_for_revealed_execution, ) + async def clear_coldkey_swap_announcement( + self, + wallet: "Wallet", + *, + mev_protection: bool = DEFAULT_MEV_PROTECTION, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + wait_for_revealed_execution: bool = True, + ) -> ExtrinsicResponse: + """ + Clears (withdraws) a pending coldkey swap announcement. + + Callable by the coldkey that has an active, undisputed swap announcement. The reannouncement delay must have + elapsed past the execution block before the announcement can be cleared. + + Parameters: + wallet: Bittensor wallet object (should be the current coldkey with an active announcement). + mev_protection: If ``True``, encrypts and submits the transaction through the MEV Shield pallet. + period: The number of blocks during which the transaction will remain valid. + raise_error: Raises a relevant exception rather than returning ``False`` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + wait_for_revealed_execution: Whether to wait for the revealed execution if mev_protection used. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + Notes: + - The coldkey must have an active, undisputed swap announcement. + - The reannouncement delay must have elapsed past the execution block. + """ + return await clear_coldkey_swap_announcement_extrinsic( + subtensor=self, + wallet=wallet, + mev_protection=mev_protection, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + async def dissolve_crowdloan( self, wallet: "Wallet", From 02cd91c87c46b653c66d1a2125cd885e30df4874 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 24 Mar 2026 14:40:36 -0700 Subject: [PATCH 005/118] clear_coldkey_swap_announcement subtensor --- bittensor/core/subtensor.py | 45 +++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 53f594e4f8..4a0994cdba 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -55,6 +55,7 @@ ) from bittensor.core.extrinsics.coldkey_swap import ( announce_coldkey_swap_extrinsic, + clear_coldkey_swap_announcement_extrinsic, dispute_coldkey_swap_extrinsic, swap_coldkey_announced_extrinsic, ) @@ -5924,6 +5925,50 @@ def dispute_coldkey_swap( wait_for_revealed_execution=wait_for_revealed_execution, ) + def clear_coldkey_swap_announcement( + self, + wallet: "Wallet", + *, + mev_protection: bool = DEFAULT_MEV_PROTECTION, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + wait_for_revealed_execution: bool = True, + ) -> ExtrinsicResponse: + """ + Clears (withdraws) a pending coldkey swap announcement. + + Callable by the coldkey that has an active, undisputed swap announcement. The reannouncement delay must have + elapsed past the execution block before the announcement can be cleared. + + Parameters: + wallet: Bittensor wallet object (should be the current coldkey with an active announcement). + mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet. + period: The number of blocks during which the transaction will remain valid. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + wait_for_revealed_execution: Whether to wait for the revealed execution if mev_protection used. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + Notes: + - The coldkey must have an active, undisputed swap announcement. + - The reannouncement delay must have elapsed past the execution block. + """ + return clear_coldkey_swap_announcement_extrinsic( + subtensor=self, + wallet=wallet, + mev_protection=mev_protection, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + def dissolve_crowdloan( self, wallet: "Wallet", From a3512781c2df7043fd74b62bf8951c22364bc36f Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 24 Mar 2026 14:41:27 -0700 Subject: [PATCH 006/118] update subtensor_module --- bittensor/core/extrinsics/pallets/subtensor_module.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/bittensor/core/extrinsics/pallets/subtensor_module.py b/bittensor/core/extrinsics/pallets/subtensor_module.py index 0da15461cb..a54d73b13a 100644 --- a/bittensor/core/extrinsics/pallets/subtensor_module.py +++ b/bittensor/core/extrinsics/pallets/subtensor_module.py @@ -366,6 +366,17 @@ def dispute_coldkey_swap(self) -> Call: """ return self.create_composed_call() + def clear_coldkey_swap_announcement(self) -> Call: + """Returns GenericCall instance for Subtensor function SubtensorModule.clear_coldkey_swap_announcement. + + Callable by the coldkey that has an active swap announcement. Withdraws the announcement + after the reannouncement delay has elapsed past the execution block. + + Returns: + GenericCall instance. + """ + return self.create_composed_call() + def reset_coldkey_swap(self, coldkey: str) -> Call: """Returns GenericCall instance for Subtensor function SubtensorModule.reset_coldkey_swap. From 24e2f3d4c317938d31a512aded260f04fd82d335 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 24 Mar 2026 14:41:46 -0700 Subject: [PATCH 007/118] update extrinsics --- bittensor/extras/subtensor_api/extrinsics.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bittensor/extras/subtensor_api/extrinsics.py b/bittensor/extras/subtensor_api/extrinsics.py index 43fafc79ad..d9a3e59f84 100644 --- a/bittensor/extras/subtensor_api/extrinsics.py +++ b/bittensor/extras/subtensor_api/extrinsics.py @@ -13,6 +13,7 @@ def __init__(self, subtensor: Union["_Subtensor", "_AsyncSubtensor"]): self.add_stake_burn = subtensor.add_stake_burn self.add_stake_multiple = subtensor.add_stake_multiple self.announce_coldkey_swap = subtensor.announce_coldkey_swap + self.clear_coldkey_swap_announcement = subtensor.clear_coldkey_swap_announcement self.dispute_coldkey_swap = subtensor.dispute_coldkey_swap self.burned_register = subtensor.burned_register self.claim_root = subtensor.claim_root From d027d93d7341e54f39366ba4bda26b854c88b118 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 24 Mar 2026 14:43:36 -0700 Subject: [PATCH 008/118] add clear_coldkey_swap_announcement_extrinsic sync --- bittensor/core/extrinsics/coldkey_swap.py | 69 +++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/bittensor/core/extrinsics/coldkey_swap.py b/bittensor/core/extrinsics/coldkey_swap.py index dcd695075f..d391261730 100644 --- a/bittensor/core/extrinsics/coldkey_swap.py +++ b/bittensor/core/extrinsics/coldkey_swap.py @@ -186,6 +186,75 @@ def dispute_coldkey_swap_extrinsic( return ExtrinsicResponse.from_exception(raise_error=raise_error, error=error) +def clear_coldkey_swap_announcement_extrinsic( + subtensor: "Subtensor", + wallet: "Wallet", + *, + mev_protection: bool = DEFAULT_MEV_PROTECTION, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + wait_for_revealed_execution: bool = True, +) -> ExtrinsicResponse: + """ + Clears (withdraws) a pending coldkey swap announcement. + + Callable by the coldkey that has an active, undisputed swap announcement. The reannouncement delay must have + elapsed past the execution block before the announcement can be cleared. + + Parameters: + subtensor: Subtensor instance with the connection to the chain. + wallet: Bittensor wallet object (should be the current coldkey with an active announcement). + mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet. + period: The number of blocks during which the transaction will remain valid. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + wait_for_revealed_execution: Whether to wait for the revealed execution if mev_protection used. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + Notes: + - The coldkey must have an active, undisputed swap announcement. + - The reannouncement delay must have elapsed past the execution block. + """ + try: + if not ( + unlocked := ExtrinsicResponse.unlock_wallet(wallet, raise_error) + ).success: + return unlocked + + call = SubtensorModule(subtensor).clear_coldkey_swap_announcement() + + if mev_protection: + response = submit_encrypted_extrinsic( + subtensor=subtensor, + wallet=wallet, + call=call, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + else: + response = subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + period=period, + raise_error=raise_error, + ) + + return response + + except Exception as error: + return ExtrinsicResponse.from_exception(raise_error=raise_error, error=error) + + def swap_coldkey_announced_extrinsic( subtensor: "Subtensor", wallet: "Wallet", From 6c5680115e2fb62906cbba63a5ec80034fcb7855 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 24 Mar 2026 14:44:26 -0700 Subject: [PATCH 009/118] clear_coldkey_swap_announcement_extrinsic asyncx --- .../core/extrinsics/asyncex/coldkey_swap.py | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/bittensor/core/extrinsics/asyncex/coldkey_swap.py b/bittensor/core/extrinsics/asyncex/coldkey_swap.py index db44be4253..67ac802051 100644 --- a/bittensor/core/extrinsics/asyncex/coldkey_swap.py +++ b/bittensor/core/extrinsics/asyncex/coldkey_swap.py @@ -184,6 +184,75 @@ async def dispute_coldkey_swap_extrinsic( return ExtrinsicResponse.from_exception(raise_error=raise_error, error=error) +async def clear_coldkey_swap_announcement_extrinsic( + subtensor: "AsyncSubtensor", + wallet: "Wallet", + *, + mev_protection: bool = DEFAULT_MEV_PROTECTION, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + wait_for_revealed_execution: bool = True, +) -> ExtrinsicResponse: + """ + Clears (withdraws) a pending coldkey swap announcement. + + Callable by the coldkey that has an active, undisputed swap announcement. The reannouncement delay must have + elapsed past the execution block before the announcement can be cleared. + + Parameters: + subtensor: AsyncSubtensor instance with the connection to the chain. + wallet: Bittensor wallet object (should be the current coldkey with an active announcement). + mev_protection: If ``True``, encrypts and submits the transaction through the MEV Shield pallet. + period: The number of blocks during which the transaction will remain valid. + raise_error: Raises a relevant exception rather than returning ``False`` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + wait_for_revealed_execution: Whether to wait for the revealed execution if mev_protection used. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + Notes: + - The coldkey must have an active, undisputed swap announcement. + - The reannouncement delay must have elapsed past the execution block. + """ + try: + if not ( + unlocked := ExtrinsicResponse.unlock_wallet(wallet, raise_error) + ).success: + return unlocked + + call = await SubtensorModule(subtensor).clear_coldkey_swap_announcement() + + if mev_protection: + response = await submit_encrypted_extrinsic( + subtensor=subtensor, + wallet=wallet, + call=call, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + else: + response = await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + period=period, + raise_error=raise_error, + ) + + return response + + except Exception as error: + return ExtrinsicResponse.from_exception(raise_error=raise_error, error=error) + + async def swap_coldkey_announced_extrinsic( subtensor: "AsyncSubtensor", wallet: "Wallet", From 2801a179dfc8a431f1479362d1a5191f04ccaf3e Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 24 Mar 2026 14:46:27 -0700 Subject: [PATCH 010/118] add sync unit test --- .../extrinsics/test_coldkey_swap.py | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/tests/unit_tests/extrinsics/test_coldkey_swap.py b/tests/unit_tests/extrinsics/test_coldkey_swap.py index 9ef59d549d..28b280124c 100644 --- a/tests/unit_tests/extrinsics/test_coldkey_swap.py +++ b/tests/unit_tests/extrinsics/test_coldkey_swap.py @@ -371,6 +371,50 @@ def test_dispute_coldkey_swap_extrinsic(subtensor, mocker): assert response == mocked_sign_and_send_extrinsic.return_value +def test_clear_coldkey_swap_announcement_extrinsic(subtensor, mocker): + """Verify that sync clear_coldkey_swap_announcement_extrinsic calls pallet and sign_and_send_extrinsic.""" + wallet = mocker.MagicMock(spec=Wallet) + + mocked_unlock_wallet = mocker.patch.object( + ExtrinsicResponse, + "unlock_wallet", + return_value=ExtrinsicResponse(success=True, message="Unlocked"), + ) + mocked_subtensor_module = mocker.patch.object( + coldkey_swap, "SubtensorModule", return_value=mocker.MagicMock() + ) + mocked_pallet_instance = mocked_subtensor_module.return_value + mocked_pallet_instance.clear_coldkey_swap_announcement.return_value = ( + mocker.MagicMock() + ) + mocked_sign_and_send_extrinsic = mocker.patch.object( + subtensor, + "sign_and_send_extrinsic", + return_value=ExtrinsicResponse(True, "Success"), + ) + + # Call + response = coldkey_swap.clear_coldkey_swap_announcement_extrinsic( + subtensor=subtensor, + wallet=wallet, + mev_protection=False, + ) + + # Asserts + mocked_unlock_wallet.assert_called_once_with(wallet, False) + mocked_subtensor_module.assert_called_once_with(subtensor) + mocked_pallet_instance.clear_coldkey_swap_announcement.assert_called_once_with() + mocked_sign_and_send_extrinsic.assert_called_once_with( + call=mocked_pallet_instance.clear_coldkey_swap_announcement.return_value, + wallet=wallet, + wait_for_inclusion=True, + wait_for_finalization=True, + period=None, + raise_error=False, + ) + assert response == mocked_sign_and_send_extrinsic.return_value + + def test_reset_coldkey_swap_extrinsic(subtensor, mocker): """Verify that sync reset_coldkey_swap_extrinsic uses sudo_call_extrinsic.""" wallet = mocker.MagicMock(spec=Wallet) @@ -457,6 +501,14 @@ def test_subtensor_module_dispute_reset_swap_coldkey_call_names(subtensor, mocke call_params={}, ) + mocked_compose_call.reset_mock() + pallet.clear_coldkey_swap_announcement() + mocked_compose_call.assert_called_with( + call_module="SubtensorModule", + call_function="clear_coldkey_swap_announcement", + call_params={}, + ) + mocked_compose_call.reset_mock() pallet.reset_coldkey_swap( coldkey="5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY" From 93dcf8c2942624bbaef6426d5d189baf35aa1ea5 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 24 Mar 2026 15:05:32 -0700 Subject: [PATCH 011/118] extend test_coldkey_swap e2e --- tests/e2e_tests/test_coldkey_swap.py | 98 ++++++++++++++++++++++++++-- 1 file changed, 91 insertions(+), 7 deletions(-) diff --git a/tests/e2e_tests/test_coldkey_swap.py b/tests/e2e_tests/test_coldkey_swap.py index 926c7f6ff0..6a7cdedd2e 100644 --- a/tests/e2e_tests/test_coldkey_swap.py +++ b/tests/e2e_tests/test_coldkey_swap.py @@ -38,15 +38,24 @@ def test_coldkey_swap(subtensor, alice_wallet, bob_wallet, charlie_wallet, dave_ - Step 2: Attempt to execute other transaction (transfer) from announced coldkey - Step 3: Verify transaction is blocked (except swap_coldkey_announced) - 5. Dispute and root reset: + 5. Clear announcement: + - Step 1: Announce swap + - Step 2: Attempt to clear too early (should fail) + - Step 3: Wait for clear block (execution_block + reannouncement_delay) + - Step 4: Clear the announcement + - Step 5: Verify announcement is removed + - Step 6: Verify transfers are unblocked after clear + + 6. Dispute and root reset: - Step 1: Dave announces swap, then disputes it (dispute_coldkey_swap) - Step 2: Verify dispute is recorded (get_coldkey_swap_dispute) - Step 3: Verify account is blocked (transfer fails) - - Step 4: Root resets coldkey swap (reset_coldkey_swap) - - Step 5: Verify dispute and announcement are cleared - - Step 6: Verify transfers are unblocked after reset + - Step 4: Verify clear is blocked while disputed + - Step 5: Root resets coldkey swap (reset_coldkey_swap) + - Step 6: Verify dispute and announcement are cleared + - Step 7: Verify transfers are unblocked after reset - 6. Root swap override: + 7. Root swap override: - Step 1: Root swaps Dave to Charlie without announcement - Step 2: Verify announcement and dispute are cleared - Step 3: Verify old coldkey is reaped @@ -434,7 +443,74 @@ def assert_coldkey_reaped(coldkey_ss58: str, label: str) -> None: ) assert response.success, f"Failed to fund Dave: {response.message}" - # === 5. Dispute and root reset === + # === 5. Clear announcement === + logging.console.info("Testing clear announcement") + + # Step 1: Alice announces swap to Bob + logging.console.info("Step 1: Alice announces swap to Bob") + existing_announcement = subtensor.wallets.get_coldkey_swap_announcement( + coldkey_ss58=alice_wallet.coldkeypub.ss58_address + ) + assert existing_announcement is None, ( + "No announcement should exist before clear test" + ) + response = subtensor.extrinsics.announce_coldkey_swap( + wallet=alice_wallet, + new_coldkey_ss58=bob_wallet.coldkeypub.ss58_address, + ) + assert response.success, f"Failed to announce swap: {response.message}" + announcement = subtensor.wallets.get_coldkey_swap_announcement( + coldkey_ss58=alice_wallet.coldkeypub.ss58_address + ) + assert announcement is not None, "Announcement should exist" + + # Step 2: Attempt to clear too early (before clear_block) + logging.console.info("Step 2: Attempting clear too early (should fail)") + clear_block = announcement.execution_block + reannouncement_delay + current_block = subtensor.chain.get_current_block() + assert current_block < clear_block, "Current block should be before clear block" + response = subtensor.extrinsics.clear_coldkey_swap_announcement( + wallet=alice_wallet, + raise_error=False, + ) + assert not response.success, "Clear should fail before clear block" + logging.console.info("Clear too early correctly rejected") + + # Step 3: Wait for clear_block (execution_block + reannouncement_delay) + logging.console.info( + f"Step 3: Waiting for clear block {clear_block} " + f"(execution={announcement.execution_block} + reannounce_delay={reannouncement_delay})" + ) + subtensor.wait_for_block(clear_block) + + # Step 4: Clear the announcement + logging.console.info("Step 4: Clearing announcement") + response = subtensor.extrinsics.clear_coldkey_swap_announcement( + wallet=alice_wallet, + ) + assert response.success, f"Failed to clear announcement: {response.message}" + + # Step 5: Verify announcement is removed + logging.console.info("Step 5: Verify announcement is removed") + announcement_after_clear = subtensor.wallets.get_coldkey_swap_announcement( + coldkey_ss58=alice_wallet.coldkeypub.ss58_address + ) + assert announcement_after_clear is None, ( + "Announcement should be removed after clear" + ) + + # Step 6: Verify transfers work again + logging.console.info("Step 6: Verify transfers are unblocked after clear") + response = subtensor.extrinsics.transfer( + wallet=alice_wallet, + destination_ss58=charlie_wallet.coldkeypub.ss58_address, + amount=Balance.from_tao(1), + raise_error=False, + ) + assert response.success, "Transfer should be allowed after clear" + logging.console.info("Clear announcement test completed successfully") + + # === 6. Dispute and root reset === logging.console.info("Testing dispute and root reset") # Step 1: Dave announces swap to Charlie @@ -478,6 +554,14 @@ def assert_coldkey_reaped(coldkey_ss58: str, label: str) -> None: raise_error=False, ) assert not response.success, "Transfer should be blocked while disputed" + + # Step 4b: Verify clear is also blocked while disputed + logging.console.info("Step 4b: Verify clear is blocked while disputed") + response = subtensor.extrinsics.clear_coldkey_swap_announcement( + wallet=dave_wallet, + raise_error=False, + ) + assert not response.success, "Clear should be blocked while disputed" logging.console.info("Account blocking verified") # Step 5: Root resets the coldkey swap (alice_wallet is //Alice, root) @@ -513,7 +597,7 @@ def assert_coldkey_reaped(coldkey_ss58: str, label: str) -> None: assert response.success, "Transfer should be allowed after reset" logging.console.info("Dispute scenario completed successfully") - # === 6. Root swap override === + # === 7. Root swap override === logging.console.info("Testing root swap override") # Ensure Dave has enough balance for root swap cost From 2fd38e0fcf84865e2447af65f2706b5f55645124 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 24 Mar 2026 15:09:54 -0700 Subject: [PATCH 012/118] extend async e2e test --- tests/e2e_tests/test_coldkey_swap.py | 100 +++++++++++++++++++++++++-- 1 file changed, 93 insertions(+), 7 deletions(-) diff --git a/tests/e2e_tests/test_coldkey_swap.py b/tests/e2e_tests/test_coldkey_swap.py index 6a7cdedd2e..0cd3613176 100644 --- a/tests/e2e_tests/test_coldkey_swap.py +++ b/tests/e2e_tests/test_coldkey_swap.py @@ -668,15 +668,24 @@ async def test_coldkey_swap_async( - Step 2: Attempt to execute other transaction (transfer) from announced coldkey - Step 3: Verify transaction is blocked (except swap_coldkey_announced) - 5. Dispute and root reset: + 5. Clear announcement: + - Step 1: Announce swap + - Step 2: Attempt to clear too early (should fail) + - Step 3: Wait for clear block (execution_block + reannouncement_delay) + - Step 4: Clear the announcement + - Step 5: Verify announcement is removed + - Step 6: Verify transfers are unblocked after clear + + 6. Dispute and root reset: - Step 1: Dave announces swap, then disputes it (dispute_coldkey_swap) - Step 2: Verify dispute is recorded (get_coldkey_swap_dispute) - Step 3: Verify account is blocked (transfer fails) - - Step 4: Root resets coldkey swap (reset_coldkey_swap) - - Step 5: Verify dispute and announcement are cleared - - Step 6: Verify transfers are unblocked after reset + - Step 4: Verify clear is blocked while disputed + - Step 5: Root resets coldkey swap (reset_coldkey_swap) + - Step 6: Verify dispute and announcement are cleared + - Step 7: Verify transfers are unblocked after reset - 6. Root swap override: + 7. Root swap override: - Step 1: Root swaps Dave to Charlie without announcement - Step 2: Verify announcement and dispute are cleared - Step 3: Verify old coldkey is reaped @@ -1080,7 +1089,76 @@ async def assert_coldkey_reaped(coldkey_ss58: str, label: str) -> None: ) assert response.success, f"Failed to fund Dave: {response.message}" - # === 5. Dispute and root reset === + # === 5. Clear announcement === + logging.console.info("Testing clear announcement") + + # Step 1: Alice announces swap to Bob + logging.console.info("Step 1: Alice announces swap to Bob") + existing_announcement = await async_subtensor.wallets.get_coldkey_swap_announcement( + coldkey_ss58=alice_wallet.coldkeypub.ss58_address + ) + assert existing_announcement is None, ( + "No announcement should exist before clear test" + ) + response = await async_subtensor.extrinsics.announce_coldkey_swap( + wallet=alice_wallet, + new_coldkey_ss58=bob_wallet.coldkeypub.ss58_address, + ) + assert response.success, f"Failed to announce swap: {response.message}" + announcement = await async_subtensor.wallets.get_coldkey_swap_announcement( + coldkey_ss58=alice_wallet.coldkeypub.ss58_address + ) + assert announcement is not None, "Announcement should exist" + + # Step 2: Attempt to clear too early (before clear_block) + logging.console.info("Step 2: Attempting clear too early (should fail)") + clear_block = announcement.execution_block + reannouncement_delay + current_block = await async_subtensor.chain.get_current_block() + assert current_block < clear_block, "Current block should be before clear block" + response = await async_subtensor.extrinsics.clear_coldkey_swap_announcement( + wallet=alice_wallet, + raise_error=False, + ) + assert not response.success, "Clear should fail before clear block" + logging.console.info("Clear too early correctly rejected") + + # Step 3: Wait for clear_block (execution_block + reannouncement_delay) + logging.console.info( + f"Step 3: Waiting for clear block {clear_block} " + f"(execution={announcement.execution_block} + reannounce_delay={reannouncement_delay})" + ) + await async_subtensor.wait_for_block(clear_block) + + # Step 4: Clear the announcement + logging.console.info("Step 4: Clearing announcement") + response = await async_subtensor.extrinsics.clear_coldkey_swap_announcement( + wallet=alice_wallet, + ) + assert response.success, f"Failed to clear announcement: {response.message}" + + # Step 5: Verify announcement is removed + logging.console.info("Step 5: Verify announcement is removed") + announcement_after_clear = ( + await async_subtensor.wallets.get_coldkey_swap_announcement( + coldkey_ss58=alice_wallet.coldkeypub.ss58_address + ) + ) + assert announcement_after_clear is None, ( + "Announcement should be removed after clear" + ) + + # Step 6: Verify transfers work again + logging.console.info("Step 6: Verify transfers are unblocked after clear") + response = await async_subtensor.extrinsics.transfer( + wallet=alice_wallet, + destination_ss58=charlie_wallet.coldkeypub.ss58_address, + amount=Balance.from_tao(1), + raise_error=False, + ) + assert response.success, "Transfer should be allowed after clear" + logging.console.info("Clear announcement test completed successfully") + + # === 6. Dispute and root reset === logging.console.info("Testing dispute and root reset") # Step 1: Dave announces swap to Charlie @@ -1124,6 +1202,14 @@ async def assert_coldkey_reaped(coldkey_ss58: str, label: str) -> None: raise_error=False, ) assert not response.success, "Transfer should be blocked while disputed" + + # Step 4b: Verify clear is also blocked while disputed + logging.console.info("Step 4b: Verify clear is blocked while disputed") + response = await async_subtensor.extrinsics.clear_coldkey_swap_announcement( + wallet=dave_wallet, + raise_error=False, + ) + assert not response.success, "Clear should be blocked while disputed" logging.console.info("Account blocking verified") # Step 5: Root resets the coldkey swap (alice_wallet is //Alice, root) @@ -1161,7 +1247,7 @@ async def assert_coldkey_reaped(coldkey_ss58: str, label: str) -> None: assert response.success, "Transfer should be allowed after reset" logging.console.info("Dispute scenario completed successfully") - # === 6. Root swap override === + # === 7. Root swap override === logging.console.info("Testing root swap override") # Ensure Dave has enough balance for root swap cost From d0f62aa5613358cb6df565daa7eac45f076e3674 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Wed, 25 Mar 2026 13:50:46 +0200 Subject: [PATCH 013/118] Removes munch --- bittensor/core/config.py | 46 +++++++++++++++- bittensor/core/settings.py | 84 ++++++++++++++---------------- pyproject.toml | 1 - tests/e2e_tests/test_root_claim.py | 8 ++- 4 files changed, 90 insertions(+), 49 deletions(-) diff --git a/bittensor/core/config.py b/bittensor/core/config.py index 2f979e2b65..3aace9913b 100644 --- a/bittensor/core/config.py +++ b/bittensor/core/config.py @@ -23,7 +23,51 @@ from typing import Any, Optional from bittensor.core.settings import DEFAULTS import yaml -from munch import DefaultMunch + + +class DefaultMunch(dict): + """ + Dict with attribute-style access and a configurable default value. + + Drop-in replacement for munch.DefaultMunch using only the stdlib. + The default value (returned for missing keys) is stored on the instance + via ``object.__setattr__`` so it never collides with dict entries. + """ + + def __init__(self, default=None, *args, **kwargs): + object.__setattr__(self, "_munch_default", default) + super().__init__(*args, **kwargs) + + def __getattr__(self, key): + try: + return self[key] + except KeyError: + return object.__getattribute__(self, "_munch_default") + + def __setattr__(self, key, value): + self[key] = value + + def __delattr__(self, key): + try: + del self[key] + except KeyError: + raise AttributeError(key) + + def toDict(self): + """Recursively converts this object to a plain dict.""" + + def _convert(v): + return v.toDict() if isinstance(v, DefaultMunch) else v + + return {k: _convert(v) for k, v in self.items()} + + @classmethod + def fromDict(cls, d, _default=None): + """Recursively creates a DefaultMunch from a plain dict.""" + result = cls(_default) + for k, v in d.items(): + result[k] = cls.fromDict(v, _default) if isinstance(v, dict) else v + return result def _filter_keys(obj): diff --git a/bittensor/core/settings.py b/bittensor/core/settings.py index 06c900062b..0fb2bc9b55 100644 --- a/bittensor/core/settings.py +++ b/bittensor/core/settings.py @@ -3,8 +3,6 @@ import re from pathlib import Path -from munch import munchify - ROOT_TAO_STAKE_WEIGHT = 0.18 READ_ONLY = os.getenv("READ_ONLY") == "1" @@ -114,50 +112,46 @@ _BT_PRIORITY_MAX_WORKERS = os.getenv("BT_PRIORITY_MAX_WORKERS") _BT_PRIORITY_MAXSIZE = os.getenv("BT_PRIORITY_MAXSIZE") -DEFAULTS = munchify( - { - "axon": { - "port": int(_BT_AXON_PORT) if _BT_AXON_PORT else 8091, - "ip": os.getenv("BT_AXON_IP") or "[::]", - "external_port": os.getenv("BT_AXON_EXTERNAL_PORT") or None, - "external_ip": os.getenv("BT_AXON_EXTERNAL_IP") or None, - "max_workers": int(_BT_AXON_MAX_WORKERS) if _BT_AXON_MAX_WORKERS else 10, - }, - "logging": { - "debug": bool(os.getenv("BT_LOGGING_DEBUG")) or False, - "trace": bool(os.getenv("BT_LOGGING_TRACE")) or False, - "info": bool(os.getenv("BT_LOGGING_INFO")) or False, - "record_log": bool(os.getenv("BT_LOGGING_RECORD_LOG")) or False, - "logging_dir": None + +class DEFAULTS: + config = False + strict = False + no_version_checking = False + + class axon: + port = int(_BT_AXON_PORT) if _BT_AXON_PORT else 8091 + ip = os.getenv("BT_AXON_IP") or "[::]" + external_port = os.getenv("BT_AXON_EXTERNAL_PORT") or None + external_ip = os.getenv("BT_AXON_EXTERNAL_IP") or None + max_workers = int(_BT_AXON_MAX_WORKERS) if _BT_AXON_MAX_WORKERS else 10 + + class logging: + debug = bool(os.getenv("BT_LOGGING_DEBUG")) or False + trace = bool(os.getenv("BT_LOGGING_TRACE")) or False + info = bool(os.getenv("BT_LOGGING_INFO")) or False + record_log = bool(os.getenv("BT_LOGGING_RECORD_LOG")) or False + logging_dir = ( + None if READ_ONLY - else os.getenv("BT_LOGGING_LOGGING_DIR") or str(MINERS_DIR), - "enable_third_party_loggers": os.getenv( - "BT_LOGGING_ENABLE_THIRD_PARTY_LOGGERS" - ) - or False, - }, - "priority": { - "max_workers": int(_BT_PRIORITY_MAX_WORKERS) - if _BT_PRIORITY_MAX_WORKERS - else 5, - "maxsize": int(_BT_PRIORITY_MAXSIZE) if _BT_PRIORITY_MAXSIZE else 10, - }, - "subtensor": { - "chain_endpoint": os.getenv("BT_SUBTENSOR_CHAIN_ENDPOINT") - or DEFAULT_ENDPOINT, - "network": os.getenv("BT_SUBTENSOR_NETWORK") or DEFAULT_NETWORK, - "_mock": False, - }, - "wallet": { - "name": os.getenv("BT_WALLET_NAME") or "default", - "hotkey": os.getenv("BT_WALLET_HOTKEY") or "default", - "path": os.getenv("BT_WALLET_PATH") or str(WALLETS_DIR), - }, - "config": False, - "strict": False, - "no_version_checking": False, - } -) + else os.getenv("BT_LOGGING_LOGGING_DIR") or str(MINERS_DIR) + ) + enable_third_party_loggers = ( + os.getenv("BT_LOGGING_ENABLE_THIRD_PARTY_LOGGERS") or False + ) + + class priority: + max_workers = int(_BT_PRIORITY_MAX_WORKERS) if _BT_PRIORITY_MAX_WORKERS else 5 + maxsize = int(_BT_PRIORITY_MAXSIZE) if _BT_PRIORITY_MAXSIZE else 10 + + class subtensor: + chain_endpoint = os.getenv("BT_SUBTENSOR_CHAIN_ENDPOINT") or DEFAULT_ENDPOINT + network = os.getenv("BT_SUBTENSOR_NETWORK") or DEFAULT_NETWORK + _mock = False + + class wallet: + name = os.getenv("BT_WALLET_NAME") or "default" + hotkey = os.getenv("BT_WALLET_HOTKEY") or "default" + path = os.getenv("BT_WALLET_PATH") or str(WALLETS_DIR) # Parsing version without any literals. diff --git a/pyproject.toml b/pyproject.toml index dd45115f98..fde38124c3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,6 @@ dependencies = [ "asyncstdlib~=3.13.0", "colorama~=0.4.6", "fastapi>=0.110.1", - "munch>=4.0.0", "numpy>=2.0.1,<3.0.0", "msgpack-numpy-opentensor~=0.5", "netaddr==1.3.0", diff --git a/tests/e2e_tests/test_root_claim.py b/tests/e2e_tests/test_root_claim.py index 012a0858ac..fa541d6d70 100644 --- a/tests/e2e_tests/test_root_claim.py +++ b/tests/e2e_tests/test_root_claim.py @@ -382,7 +382,9 @@ def test_root_claim_keep_with_zero_num_root_auto_claims( hotkey_ss58=alice_wallet.hotkey.ss58_address, netuid=sn2.netuid, ) - assert stake_after_charlie >= claimable_stake_before_charlie - Balance.from_rao(1, sn2.netuid) + assert stake_after_charlie >= claimable_stake_before_charlie - Balance.from_rao( + 1, sn2.netuid + ) logging.console.info(f"[blue]Charlie after:[/blue]") logging.console.info(f"RootClaimed: {claimed_after_charlie}") @@ -559,7 +561,9 @@ async def test_root_claim_keep_with_zero_num_root_auto_claims_async( hotkey_ss58=alice_wallet.hotkey.ss58_address, netuid=sn2.netuid, ) - assert stake_after_charlie >= claimable_stake_before_charlie - Balance.from_rao(1, sn2.netuid) + assert stake_after_charlie >= claimable_stake_before_charlie - Balance.from_rao( + 1, sn2.netuid + ) logging.console.info(f"[blue]Charlie after:[/blue]") logging.console.info(f"RootClaimed: {claimed_after_charlie}") From 30f00be9ae2b6a67ad6d91b2233b5cf43b8bb2be Mon Sep 17 00:00:00 2001 From: BD Himes Date: Wed, 25 Mar 2026 14:04:45 +0200 Subject: [PATCH 014/118] Fixes --- bittensor/core/config.py | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/bittensor/core/config.py b/bittensor/core/config.py index 3aace9913b..383c74236a 100644 --- a/bittensor/core/config.py +++ b/bittensor/core/config.py @@ -36,7 +36,11 @@ class DefaultMunch(dict): def __init__(self, default=None, *args, **kwargs): object.__setattr__(self, "_munch_default", default) - super().__init__(*args, **kwargs) + super().__init__() + if args or kwargs: + source = dict(*args, **kwargs) + for k, v in source.items(): + self[k] = DefaultMunch.fromDict(v) if isinstance(v, dict) else v def __getattr__(self, key): try: @@ -70,6 +74,21 @@ def fromDict(cls, d, _default=None): return result +def _class_to_dict(cls) -> dict: + """ + Recursively converts a class with class-level attributes to a plain dict. + + Skips private/dunder attributes. Nested classes become nested dicts. + Used to convert the `DEFAULTS` class from settings into a plain dict. + """ + result = {} + for k, v in vars(cls).items(): + if k.startswith("_"): + continue + result[k] = _class_to_dict(v) if isinstance(v, type) else v + return result + + def _filter_keys(obj): """Filters keys from an object, excluding private and certain internal properties.""" if isinstance(obj, dict): @@ -108,11 +127,14 @@ def __init__( default = deepcopy(default or DEFAULTS) if isinstance(default, DefaultMunch): - # Initialize Munch with defaults (dict-safe) super().__init__(None, default.toDict()) - else: - # if defaults passed as dict + elif isinstance(default, dict): super().__init__(None, default) + elif isinstance(default, type): + # DEFAULTS is a class with nested classes; convert to plain dict + super().__init__(None, _class_to_dict(default)) + else: + super().__init__(None) self.__is_set = {} From 4ea80f2d9bc638e37a546fdca8af067b0dd36009 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Wed, 25 Mar 2026 14:16:59 +0200 Subject: [PATCH 015/118] Handle Python 3.10, add unit test --- bittensor/core/config.py | 14 +++++++++++ tests/unit_tests/test_config.py | 44 +++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/bittensor/core/config.py b/bittensor/core/config.py index 383c74236a..483e15828e 100644 --- a/bittensor/core/config.py +++ b/bittensor/core/config.py @@ -65,6 +65,20 @@ def _convert(v): return {k: _convert(v) for k, v in self.items()} + def __deepcopy__(self, memo): + """ + Explicit deepcopy so Python 3.10 never falls through to __reduce_ex__. + + Without this, ``copy.deepcopy`` on a dict subclass tries + ``getattr(x, "__reduce_ex__", None)``, which hits our ``__getattr__`` + and returns ``None`` (the default value) instead of the real method. + """ + new = DefaultMunch(object.__getattribute__(self, "_munch_default")) + memo[id(self)] = new + for k, v in self.items(): + new[deepcopy(k, memo)] = deepcopy(v, memo) + return new + @classmethod def fromDict(cls, d, _default=None): """Recursively creates a DefaultMunch from a plain dict.""" diff --git a/tests/unit_tests/test_config.py b/tests/unit_tests/test_config.py index 18b8ee36d5..b7bdbc61df 100644 --- a/tests/unit_tests/test_config.py +++ b/tests/unit_tests/test_config.py @@ -1,5 +1,7 @@ +import copy import bittensor import argparse +from bittensor.core.config import Config, DefaultMunch def test_py_config_parsed_successfully_rust_wallet(): @@ -29,3 +31,45 @@ def test_py_config_parsed_successfully_rust_wallet(): assert wallet_two.name == config.wallet.name assert wallet_two.hotkey_str == config.wallet.hotkey assert wallet_two.path == config.wallet.path + + +def test_deepcopy_default_munch(): + """ + deepcopy of a nested DefaultMunch must not fail via __reduce_ex__. + + In Python 3.10, copy.deepcopy falls back to getattr(x, "__reduce_ex__") for + dict subclasses that are not in _deepcopy_dispatch. DefaultMunch.__getattr__ + intercepts that lookup and returns None (the configured default value) instead + of the real method, causing TypeError: 'NoneType' object is not callable. + DefaultMunch.__deepcopy__ prevents this by giving deepcopy an explicit path. + """ + original = DefaultMunch.fromDict( + {"port": 8091, "ip": "[::]", "external_port": None, "nested": {"x": 1}} + ) + cloned = copy.deepcopy(original) + + assert cloned.port == 8091 + assert cloned.ip == "[::]" + assert cloned.external_port is None + assert cloned.nested.x == 1 + + # Mutations to the clone must not affect the original + cloned.port = 9999 + cloned.nested.x = 42 + assert original.port == 8091 + assert original.nested.x == 1 + + +def test_deepcopy_config_with_nested_defaults(): + """deepcopy of a full Config (including nested DefaultMunch values) works.""" + parser = argparse.ArgumentParser() + bittensor.Subtensor.add_args(parser) + config = Config(parser) + + cloned = copy.deepcopy(config) + + assert cloned.subtensor.network == config.subtensor.network + + # Mutations to the clone must not affect the original + cloned.subtensor.network = "mutated" + assert config.subtensor.network != "mutated" From 86e7e644ad509cdc937b56a719469f456d4d9392 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Mon, 30 Mar 2026 15:35:58 +0200 Subject: [PATCH 016/118] There is not Balances.transfer --- bittensor/core/async_subtensor.py | 2 +- bittensor/core/subtensor.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index d3b6d9bac7..4ef14ebd9a 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -6221,7 +6221,7 @@ async def get_extrinsic_fee( # Estimate fee before sending a transfer call = await subtensor.compose_call( call_module="Balances", - call_function="transfer", + call_function="transfer_allow_death", call_params={"dest": destination_ss58, "value": amount.rao} ) fee = await subtensor.get_extrinsic_fee(call=call, keypair=wallet.coldkey) diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 4a0994cdba..3f84614253 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -5047,7 +5047,7 @@ def get_extrinsic_fee( # Estimate fee before sending a transfer call = subtensor.compose_call( call_module="Balances", - call_function="transfer", + call_function="transfer_allow_death", call_params={"dest": destination_ss58, "value": amount.rao} ) fee = subtensor.get_extrinsic_fee(call=call, keypair=wallet.coldkey) From d91dcd164f27fd367b4b622fb9461038607debf5 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Mon, 30 Mar 2026 18:40:23 +0200 Subject: [PATCH 017/118] Bumps all workflows versions --- .github/workflows/_run-e2e-single.yaml | 10 +++++----- .github/workflows/changelog-checker.yml | 6 +++--- .github/workflows/compatibility.yml | 6 +++--- .github/workflows/docker_release.yml | 2 +- .github/workflows/e2e-subtensor-tests.yaml | 10 +++++----- .github/workflows/flake8-and-mypy.yml | 8 ++++---- .../monitor_requirements_size_master.yml | 8 ++++---- .../nightly-e2e-tests-subtensor-main.yml | 12 ++++++------ .github/workflows/release.yml | 8 ++++---- .github/workflows/ruff.yml | 4 ++-- .../workflows/subtensor-consistency-tests.yaml | 16 ++++++++-------- .github/workflows/unit-and-integration-tests.yml | 8 ++++---- 12 files changed, 49 insertions(+), 49 deletions(-) diff --git a/.github/workflows/_run-e2e-single.yaml b/.github/workflows/_run-e2e-single.yaml index d86da50d2d..aa6d67563b 100644 --- a/.github/workflows/_run-e2e-single.yaml +++ b/.github/workflows/_run-e2e-single.yaml @@ -40,22 +40,22 @@ jobs: steps: - name: Check-out repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: ref: ${{ inputs.ref != '' && inputs.ref || github.ref }} - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Install uv - uses: astral-sh/setup-uv@v4 + uses: astral-sh/setup-uv@v8 with: enable-cache: false - name: Cache uv and venv - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: | ~/.cache/uv @@ -70,7 +70,7 @@ jobs: run: uv sync --extra dev --dev - name: Download Cached Docker Image - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: ${{ inputs.artifact-name }} diff --git a/.github/workflows/changelog-checker.yml b/.github/workflows/changelog-checker.yml index aae9580609..a5038e860b 100644 --- a/.github/workflows/changelog-checker.yml +++ b/.github/workflows/changelog-checker.yml @@ -14,11 +14,11 @@ jobs: if: startsWith(github.head_ref, 'release/') || startsWith(github.head_ref, 'hotfix/') runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: tj-actions/changed-files@v46 + - uses: actions/checkout@v6 + - uses: tj-actions/changed-files@v47.0.5 id: changed - name: Ensure CHANGELOG.md updated if: contains(steps.changed.outputs.all_changed_files, 'CHANGELOG.md') == false - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: script: core.setFailed('CHANGELOG.md must be updated.') diff --git a/.github/workflows/compatibility.yml b/.github/workflows/compatibility.yml index eb333b4126..b192f7834d 100644 --- a/.github/workflows/compatibility.yml +++ b/.github/workflows/compatibility.yml @@ -13,7 +13,7 @@ jobs: outputs: python-versions: ${{ steps.read-versions.outputs.versions }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - id: read-versions run: | versions=$(cat .github/supported-python-versions.json) @@ -29,8 +29,8 @@ jobs: python-version: ${{ fromJson(needs.read-python-versions.outputs.python-versions) }} steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} diff --git a/.github/workflows/docker_release.yml b/.github/workflows/docker_release.yml index 58f94c0571..0f0962a714 100644 --- a/.github/workflows/docker_release.yml +++ b/.github/workflows/docker_release.yml @@ -18,7 +18,7 @@ jobs: steps: - name: Check out code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Install cosign uses: sigstore/cosign-installer@v3 diff --git a/.github/workflows/e2e-subtensor-tests.yaml b/.github/workflows/e2e-subtensor-tests.yaml index f19bc2d454..4a965067fa 100644 --- a/.github/workflows/e2e-subtensor-tests.yaml +++ b/.github/workflows/e2e-subtensor-tests.yaml @@ -42,10 +42,10 @@ jobs: test-files: ${{ steps.get-tests.outputs.test-files }} steps: - name: Check-out repository under $GITHUB_WORKSPACE - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.10' @@ -57,7 +57,7 @@ jobs: ignore-nothing-to-cache: true - name: Cache uv and venv - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: | ~/.cache/uv @@ -92,7 +92,7 @@ jobs: outputs: python-versions: ${{ steps.read-versions.outputs.versions }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - id: read-versions run: | versions=$(cat .github/supported-python-versions.json) @@ -201,7 +201,7 @@ jobs: run: docker save -o subtensor-localnet.tar ${{ steps.set-image.outputs.image }} - name: Upload Docker Image as Artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: subtensor-localnet path: subtensor-localnet.tar diff --git a/.github/workflows/flake8-and-mypy.yml b/.github/workflows/flake8-and-mypy.yml index 9638d08ff8..37633494e4 100644 --- a/.github/workflows/flake8-and-mypy.yml +++ b/.github/workflows/flake8-and-mypy.yml @@ -12,7 +12,7 @@ jobs: outputs: python-versions: ${{ steps.read-versions.outputs.versions }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - id: read-versions run: | versions=$(cat .github/supported-python-versions.json) @@ -31,10 +31,10 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} @@ -44,7 +44,7 @@ jobs: enable-cache: false - name: Cache uv and .venv - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: | ~/.cache/uv diff --git a/.github/workflows/monitor_requirements_size_master.yml b/.github/workflows/monitor_requirements_size_master.yml index af87292257..bb879689c1 100644 --- a/.github/workflows/monitor_requirements_size_master.yml +++ b/.github/workflows/monitor_requirements_size_master.yml @@ -20,7 +20,7 @@ jobs: outputs: python-versions: ${{ steps.read-versions.outputs.versions }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - id: read-versions run: | versions=$(cat .github/supported-python-versions.json) @@ -40,8 +40,8 @@ jobs: py313: ${{ steps.set-output.outputs.py313 }} steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} @@ -70,7 +70,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Post venv size summary to PR - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | diff --git a/.github/workflows/nightly-e2e-tests-subtensor-main.yml b/.github/workflows/nightly-e2e-tests-subtensor-main.yml index d8c8d44419..6249501095 100644 --- a/.github/workflows/nightly-e2e-tests-subtensor-main.yml +++ b/.github/workflows/nightly-e2e-tests-subtensor-main.yml @@ -33,10 +33,10 @@ jobs: test-files: ${{ steps.get-tests.outputs.test-files }} steps: - name: Check-out repository under $GITHUB_WORKSPACE - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.10' @@ -48,7 +48,7 @@ jobs: ignore-nothing-to-cache: true - name: Cache uv and venv - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: | ~/.cache/uv @@ -83,7 +83,7 @@ jobs: outputs: python-versions: ${{ steps.read-versions.outputs.versions }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - id: read-versions run: | versions=$(cat .github/supported-python-versions.json) @@ -110,13 +110,13 @@ jobs: docker save -o subtensor-localnet-devnet-ready.tar ghcr.io/opentensor/subtensor-localnet:devnet-ready - name: Upload main Docker Image as Artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: subtensor-localnet-main path: subtensor-localnet-main.tar - name: Upload devnet-ready Docker Image as Artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: subtensor-localnet-devnet-ready path: subtensor-localnet-devnet-ready.tar diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f5a988b42a..d7e7f8e9af 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,10 +13,10 @@ jobs: name: Build Python distribution runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v6 with: python-version: '3.11' @@ -45,7 +45,7 @@ jobs: fi - name: Upload artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: dist path: dist/ @@ -60,7 +60,7 @@ jobs: steps: - name: Download artifact - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: dist path: dist/ diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml index 7dfaf2eda5..fdf1141a7b 100644 --- a/.github/workflows/ruff.yml +++ b/.github/workflows/ruff.yml @@ -13,10 +13,10 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: "3.11" diff --git a/.github/workflows/subtensor-consistency-tests.yaml b/.github/workflows/subtensor-consistency-tests.yaml index b7e02abf4d..4f48de954d 100644 --- a/.github/workflows/subtensor-consistency-tests.yaml +++ b/.github/workflows/subtensor-consistency-tests.yaml @@ -27,10 +27,10 @@ jobs: test-files: ${{ steps.get-tests.outputs.test-files }} steps: - name: Check-out repository under $GITHUB_WORKSPACE - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.10' @@ -42,7 +42,7 @@ jobs: ignore-nothing-to-cache: true - name: Cache uv and venv - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: | ~/.cache/uv @@ -132,7 +132,7 @@ jobs: run: docker save -o subtensor-localnet.tar ${{ steps.set-image.outputs.image }} - name: Upload Docker Image as Artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: subtensor-localnet path: subtensor-localnet.tar @@ -161,10 +161,10 @@ jobs: test-file: ${{ fromJson(needs.find-tests.outputs.test-files) }} steps: - name: Check-out repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.10' @@ -172,7 +172,7 @@ jobs: uses: astral-sh/setup-uv@v4 - name: Cache uv and venv - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: | ~/.cache/uv @@ -184,7 +184,7 @@ jobs: run: uv sync --extra dev --dev - name: Download Cached Docker Image - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: subtensor-localnet diff --git a/.github/workflows/unit-and-integration-tests.yml b/.github/workflows/unit-and-integration-tests.yml index a6a525981e..4461200b93 100644 --- a/.github/workflows/unit-and-integration-tests.yml +++ b/.github/workflows/unit-and-integration-tests.yml @@ -12,7 +12,7 @@ jobs: outputs: python-versions: ${{ steps.read-versions.outputs.versions }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - id: read-versions run: | versions=$(cat .github/supported-python-versions.json) @@ -31,10 +31,10 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} @@ -44,7 +44,7 @@ jobs: enable-cache: false - name: Cache uv and .venv - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: | ~/.cache/uv From 611fc3cf3dac74e33edb09f5894e9f181b016006 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Mon, 30 Mar 2026 18:57:14 +0200 Subject: [PATCH 018/118] UV --- .github/workflows/_run-e2e-single.yaml | 2 +- .github/workflows/e2e-subtensor-tests.yaml | 2 +- .github/workflows/flake8-and-mypy.yml | 2 +- .github/workflows/nightly-e2e-tests-subtensor-main.yml | 2 +- .github/workflows/subtensor-consistency-tests.yaml | 4 ++-- .github/workflows/unit-and-integration-tests.yml | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/_run-e2e-single.yaml b/.github/workflows/_run-e2e-single.yaml index aa6d67563b..26d9210b6d 100644 --- a/.github/workflows/_run-e2e-single.yaml +++ b/.github/workflows/_run-e2e-single.yaml @@ -50,7 +50,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install uv - uses: astral-sh/setup-uv@v8 + uses: astral-sh/setup-uv with: enable-cache: false diff --git a/.github/workflows/e2e-subtensor-tests.yaml b/.github/workflows/e2e-subtensor-tests.yaml index 4a965067fa..8561417b72 100644 --- a/.github/workflows/e2e-subtensor-tests.yaml +++ b/.github/workflows/e2e-subtensor-tests.yaml @@ -50,7 +50,7 @@ jobs: python-version: '3.10' - name: Install uv - uses: astral-sh/setup-uv@v4 + uses: astral-sh/setup-uv with: enable-cache: false cache-dependency-glob: '**/pyproject.toml' diff --git a/.github/workflows/flake8-and-mypy.yml b/.github/workflows/flake8-and-mypy.yml index 37633494e4..4b73634feb 100644 --- a/.github/workflows/flake8-and-mypy.yml +++ b/.github/workflows/flake8-and-mypy.yml @@ -39,7 +39,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install uv - uses: astral-sh/setup-uv@v4 + uses: astral-sh/setup-uv with: enable-cache: false diff --git a/.github/workflows/nightly-e2e-tests-subtensor-main.yml b/.github/workflows/nightly-e2e-tests-subtensor-main.yml index 6249501095..ca3b99ff30 100644 --- a/.github/workflows/nightly-e2e-tests-subtensor-main.yml +++ b/.github/workflows/nightly-e2e-tests-subtensor-main.yml @@ -41,7 +41,7 @@ jobs: python-version: '3.10' - name: Install uv - uses: astral-sh/setup-uv@v4 + uses: astral-sh/setup-uv with: enable-cache: false cache-dependency-glob: '**/pyproject.toml' diff --git a/.github/workflows/subtensor-consistency-tests.yaml b/.github/workflows/subtensor-consistency-tests.yaml index 4f48de954d..e495574167 100644 --- a/.github/workflows/subtensor-consistency-tests.yaml +++ b/.github/workflows/subtensor-consistency-tests.yaml @@ -35,7 +35,7 @@ jobs: python-version: '3.10' - name: Install uv - uses: astral-sh/setup-uv@v4 + uses: astral-sh/setup-uv with: enable-cache: false cache-dependency-glob: '**/pyproject.toml' @@ -169,7 +169,7 @@ jobs: python-version: '3.10' - name: Install uv - uses: astral-sh/setup-uv@v4 + uses: astral-sh/setup-uv - name: Cache uv and venv uses: actions/cache@v5 diff --git a/.github/workflows/unit-and-integration-tests.yml b/.github/workflows/unit-and-integration-tests.yml index 4461200b93..e34e15c73e 100644 --- a/.github/workflows/unit-and-integration-tests.yml +++ b/.github/workflows/unit-and-integration-tests.yml @@ -39,7 +39,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install uv - uses: astral-sh/setup-uv@v4 + uses: astral-sh/setup-uv with: enable-cache: false From ad74a25684ed8d836af9e4adf4547a2c41c98907 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Mon, 30 Mar 2026 19:03:11 +0200 Subject: [PATCH 019/118] UV --- .github/workflows/_run-e2e-single.yaml | 2 +- .github/workflows/e2e-subtensor-tests.yaml | 2 +- .github/workflows/flake8-and-mypy.yml | 2 +- .github/workflows/nightly-e2e-tests-subtensor-main.yml | 2 +- .github/workflows/subtensor-consistency-tests.yaml | 4 ++-- .github/workflows/unit-and-integration-tests.yml | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/_run-e2e-single.yaml b/.github/workflows/_run-e2e-single.yaml index 26d9210b6d..96a14441cc 100644 --- a/.github/workflows/_run-e2e-single.yaml +++ b/.github/workflows/_run-e2e-single.yaml @@ -50,7 +50,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install uv - uses: astral-sh/setup-uv + uses: astral-sh/setup-uv@v8.0.0 with: enable-cache: false diff --git a/.github/workflows/e2e-subtensor-tests.yaml b/.github/workflows/e2e-subtensor-tests.yaml index 8561417b72..c904cd18d5 100644 --- a/.github/workflows/e2e-subtensor-tests.yaml +++ b/.github/workflows/e2e-subtensor-tests.yaml @@ -50,7 +50,7 @@ jobs: python-version: '3.10' - name: Install uv - uses: astral-sh/setup-uv + uses: astral-sh/setup-uv@v8.0.0 with: enable-cache: false cache-dependency-glob: '**/pyproject.toml' diff --git a/.github/workflows/flake8-and-mypy.yml b/.github/workflows/flake8-and-mypy.yml index 4b73634feb..d1470cd6ef 100644 --- a/.github/workflows/flake8-and-mypy.yml +++ b/.github/workflows/flake8-and-mypy.yml @@ -39,7 +39,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install uv - uses: astral-sh/setup-uv + uses: astral-sh/setup-uv@v8.0.0 with: enable-cache: false diff --git a/.github/workflows/nightly-e2e-tests-subtensor-main.yml b/.github/workflows/nightly-e2e-tests-subtensor-main.yml index ca3b99ff30..1cbd66e528 100644 --- a/.github/workflows/nightly-e2e-tests-subtensor-main.yml +++ b/.github/workflows/nightly-e2e-tests-subtensor-main.yml @@ -41,7 +41,7 @@ jobs: python-version: '3.10' - name: Install uv - uses: astral-sh/setup-uv + uses: astral-sh/setup-uv@v8.0.0 with: enable-cache: false cache-dependency-glob: '**/pyproject.toml' diff --git a/.github/workflows/subtensor-consistency-tests.yaml b/.github/workflows/subtensor-consistency-tests.yaml index e495574167..3d762ff660 100644 --- a/.github/workflows/subtensor-consistency-tests.yaml +++ b/.github/workflows/subtensor-consistency-tests.yaml @@ -35,7 +35,7 @@ jobs: python-version: '3.10' - name: Install uv - uses: astral-sh/setup-uv + uses: astral-sh/setup-uv@v8.0.0 with: enable-cache: false cache-dependency-glob: '**/pyproject.toml' @@ -169,7 +169,7 @@ jobs: python-version: '3.10' - name: Install uv - uses: astral-sh/setup-uv + uses: astral-sh/setup-uv@v8.0.0 - name: Cache uv and venv uses: actions/cache@v5 diff --git a/.github/workflows/unit-and-integration-tests.yml b/.github/workflows/unit-and-integration-tests.yml index e34e15c73e..51b9a5eb2b 100644 --- a/.github/workflows/unit-and-integration-tests.yml +++ b/.github/workflows/unit-and-integration-tests.yml @@ -39,7 +39,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install uv - uses: astral-sh/setup-uv + uses: astral-sh/setup-uv@v8.0.0 with: enable-cache: false From 81eb64960e5d43e2b4db10a19ec1e8e26e44e697 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Mon, 30 Mar 2026 19:44:24 +0200 Subject: [PATCH 020/118] Ensures we don't accidentally install ASI 2.0 on this --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index fde38124c3..b43cab375c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,7 @@ dependencies = [ "uvicorn", "bittensor-drand>=1.3.0,<2.0.0", "bittensor-wallet==4.0.1", - "async-substrate-interface>=1.6.2" + "async-substrate-interface>=1.6.2,<2.0.0" ] [project.optional-dependencies] From 0b8c20e87b3e1bcb31b0ce8a01eb91750b687b18 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 30 Mar 2026 12:31:33 -0700 Subject: [PATCH 021/118] updates exception checking for upcoming change --- tests/e2e_tests/test_commitment.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/e2e_tests/test_commitment.py b/tests/e2e_tests/test_commitment.py index be1f14b5e8..484063089f 100644 --- a/tests/e2e_tests/test_commitment.py +++ b/tests/e2e_tests/test_commitment.py @@ -24,7 +24,10 @@ def test_commitment(subtensor, alice_wallet, dave_wallet): ] dave_sn.execute_steps(steps) - with pytest.raises(SubstrateRequestException, match="AccountNotAllowedCommit"): + with pytest.raises( + SubstrateRequestException, + match=r"AccountNotAllowedCommit|Invalid signing address", + ): subtensor.commitments.set_commitment( wallet=alice_wallet, netuid=dave_sn.netuid, @@ -89,7 +92,10 @@ async def test_commitment_async(async_subtensor, alice_wallet, dave_wallet): ] await dave_sn.async_execute_steps(steps) - with pytest.raises(SubstrateRequestException, match="AccountNotAllowedCommit"): + with pytest.raises( + SubstrateRequestException, + match=r"AccountNotAllowedCommit|Invalid signing address", + ): await async_subtensor.commitments.set_commitment( wallet=alice_wallet, netuid=dave_sn.netuid, From 15bead79ee00530ac34604e24a9cd7d0d1ebc8a5 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 30 Mar 2026 13:46:24 -0700 Subject: [PATCH 022/118] testing --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b43cab375c..0a738e9f48 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ requires-python = ">=3.10,<3.15" dependencies = [ "wheel", "setuptools~=70.0", - "aiohttp>=3.9,<4.0", + "aiohttp>=3.9,<3.13.3", "asyncstdlib~=3.13.0", "colorama~=0.4.6", "fastapi>=0.110.1", From 43f2713bee6a2e26476f1f00b7f9706de5c2503c Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 30 Mar 2026 14:15:19 -0700 Subject: [PATCH 023/118] revert --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 0a738e9f48..b43cab375c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ requires-python = ">=3.10,<3.15" dependencies = [ "wheel", "setuptools~=70.0", - "aiohttp>=3.9,<3.13.3", + "aiohttp>=3.9,<4.0", "asyncstdlib~=3.13.0", "colorama~=0.4.6", "fastapi>=0.110.1", From 0bae557b1a626dee2780e2a7d23e0eef4692e057 Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Mon, 30 Mar 2026 18:37:35 -0700 Subject: [PATCH 024/118] fix and extend incentive test (sync and async) --- tests/e2e_tests/test_incentive.py | 111 +++++++++++++++++------------- 1 file changed, 65 insertions(+), 46 deletions(-) diff --git a/tests/e2e_tests/test_incentive.py b/tests/e2e_tests/test_incentive.py index 76f1f6cf41..64d5b97268 100644 --- a/tests/e2e_tests/test_incentive.py +++ b/tests/e2e_tests/test_incentive.py @@ -2,6 +2,7 @@ import time import pytest +from bittensor.utils.balance import Balance from bittensor.utils.btlogging import logging from tests.e2e_tests.utils import ( TestSubnet, @@ -45,6 +46,14 @@ def test_incentive(subtensor, templates, alice_wallet, bob_wallet): "Alice & Bob not registered in the subnet" ) + # Stake so Alice has active_stake for Yuma3 dividend calculation + assert subtensor.staking.add_stake( + wallet=alice_wallet, + netuid=alice_sn.netuid, + hotkey_ss58=alice_wallet.hotkey.ss58_address, + amount=Balance.from_tao(10_000), + ).success + # Wait for the first epoch to pass subtensor.wait_for_block( subtensor.subnets.get_next_epoch_start_block(alice_sn.netuid) + 5 @@ -106,46 +115,46 @@ def test_incentive(subtensor, templates, alice_wallet, bob_wallet): ) assert validators[bob_uid] == 0 - while True: + max_retries = 30 + last_error = None + for _ in range(max_retries): + time.sleep(1) try: neurons = subtensor.neurons.neurons(netuid=alice_sn.netuid) - logging.info(f"neurons: {neurons}") # Get current emissions and validate that Alice has gotten tao alice_neuron = neurons[0] assert alice_neuron.validator_permit is True - assert alice_neuron.dividends == 1.0 - assert alice_neuron.stake.tao > 0 - assert alice_neuron.validator_trust > 0.99 - assert alice_neuron.incentive < 0.5 - assert alice_neuron.consensus < 0.5 + assert alice_neuron.dividends == 1.0, f"dividends={alice_neuron.dividends}" + assert alice_neuron.stake.tao > 0, f"stake={alice_neuron.stake.tao}" + assert alice_neuron.validator_trust > 0.99, ( + f"vtrust={alice_neuron.validator_trust}" + ) + assert alice_neuron.incentive < 0.5, f"incentive={alice_neuron.incentive}" + assert alice_neuron.consensus < 0.5, f"consensus={alice_neuron.consensus}" bob_neuron = neurons[1] - assert bob_neuron.incentive > 0.5 - assert bob_neuron.consensus > 0.5 + assert bob_neuron.incentive > 0.5, f"bob.incentive={bob_neuron.incentive}" + assert bob_neuron.consensus > 0.5, f"bob.consensus={bob_neuron.consensus}" bonds = subtensor.subnets.bonds(alice_sn.netuid) - assert bonds == [ - ( - 0, - [ - (0, 65535), - (1, 65535), - ], - ), - ( - 1, - [], - ), - ] + assert len(bonds) == 2, f"bonds={bonds}" + assert bonds[0][0] == 0, f"bonds={bonds}" + assert len(bonds[0][1]) == 1, f"bonds={bonds}" + assert bonds[0][1][0][0] == 1, f"bonds={bonds}" + assert bonds[0][1][0][1] > 0, f"bonds={bonds}" + assert bonds[1] == (1, []), f"bonds={bonds}" break - except Exception: + except Exception as e: + last_error = e subtensor.wait_for_block(subtensor.block) continue + else: + pytest.fail(f"Neuron metrics did not reach expected values: {last_error}") @pytest.mark.asyncio @@ -178,6 +187,16 @@ async def test_incentive_async(async_subtensor, templates, alice_wallet, bob_wal "Alice & Bob not registered in the subnet" ) + # Stake so Alice has active_stake for Yuma3 dividend calculation + assert ( + await async_subtensor.staking.add_stake( + wallet=alice_wallet, + netuid=alice_sn.netuid, + hotkey_ss58=alice_wallet.hotkey.ss58_address, + amount=Balance.from_tao(10_000), + ) + ).success + # Wait for the first epoch to pass next_epoch_start_block = await async_subtensor.subnets.get_next_epoch_start_block( netuid=alice_sn.netuid @@ -245,43 +264,43 @@ async def test_incentive_async(async_subtensor, templates, alice_wallet, bob_wal ) assert validators[bob_uid] == 0 - while True: + max_retries = 30 + last_error = None + for _ in range(max_retries): + await asyncio.sleep(1) try: neurons = await async_subtensor.neurons.neurons(netuid=alice_sn.netuid) - logging.info(f"neurons: {neurons}") # Get current emissions and validate that Alice has gotten tao alice_neuron = neurons[0] assert alice_neuron.validator_permit is True - assert alice_neuron.dividends == 1.0 - assert alice_neuron.stake.tao > 0 - assert alice_neuron.validator_trust > 0.99 - assert alice_neuron.incentive < 0.5 - assert alice_neuron.consensus < 0.5 + assert alice_neuron.dividends == 1.0, f"dividends={alice_neuron.dividends}" + assert alice_neuron.stake.tao > 0, f"stake={alice_neuron.stake.tao}" + assert alice_neuron.validator_trust > 0.99, ( + f"vtrust={alice_neuron.validator_trust}" + ) + assert alice_neuron.incentive < 0.5, f"incentive={alice_neuron.incentive}" + assert alice_neuron.consensus < 0.5, f"consensus={alice_neuron.consensus}" bob_neuron = neurons[1] - assert bob_neuron.incentive > 0.5 - assert bob_neuron.consensus > 0.5 + assert bob_neuron.incentive > 0.5, f"bob.incentive={bob_neuron.incentive}" + assert bob_neuron.consensus > 0.5, f"bob.consensus={bob_neuron.consensus}" bonds = await async_subtensor.subnets.bonds(alice_sn.netuid) - assert bonds == [ - ( - 0, - [ - (0, 65535), - (1, 65535), - ], - ), - ( - 1, - [], - ), - ] + assert len(bonds) == 2, f"bonds={bonds}" + assert bonds[0][0] == 0, f"bonds={bonds}" + assert len(bonds[0][1]) == 1, f"bonds={bonds}" + assert bonds[0][1][0][0] == 1, f"bonds={bonds}" + assert bonds[0][1][0][1] > 0, f"bonds={bonds}" + assert bonds[1] == (1, []), f"bonds={bonds}" break - except Exception: + except Exception as e: + last_error = e await async_subtensor.wait_for_block(await async_subtensor.block) continue + else: + pytest.fail(f"Neuron metrics did not reach expected values: {last_error}") From e9474414d8c351671a76e8d368aaeeae77f1534e Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 2 Apr 2026 19:36:09 -0700 Subject: [PATCH 025/118] owner is now also a validator --- tests/e2e_tests/test_dendrite.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/e2e_tests/test_dendrite.py b/tests/e2e_tests/test_dendrite.py index fda19d35de..584fe09635 100644 --- a/tests/e2e_tests/test_dendrite.py +++ b/tests/e2e_tests/test_dendrite.py @@ -46,7 +46,7 @@ def test_dendrite(subtensor, templates, alice_wallet, bob_wallet): REGISTER_SUBNET(alice_wallet), SUDO_SET_TEMPO(alice_wallet, AdminUtils, True, NETUID, TEMPO_TO_SET), ACTIVATE_SUBNET(alice_wallet), - SUDO_SET_MAX_ALLOWED_VALIDATORS(alice_wallet, AdminUtils, True, NETUID, 1), + SUDO_SET_MAX_ALLOWED_VALIDATORS(alice_wallet, AdminUtils, True, NETUID, 2), SUDO_SET_WEIGHTS_SET_RATE_LIMIT(alice_wallet, AdminUtils, True, NETUID, 10), REGISTER_NEURON(bob_wallet), ] @@ -160,7 +160,7 @@ async def test_dendrite_async(async_subtensor, templates, alice_wallet, bob_wall REGISTER_SUBNET(alice_wallet), SUDO_SET_TEMPO(alice_wallet, AdminUtils, True, NETUID, TEMPO_TO_SET), ACTIVATE_SUBNET(alice_wallet), - SUDO_SET_MAX_ALLOWED_VALIDATORS(alice_wallet, AdminUtils, True, NETUID, 1), + SUDO_SET_MAX_ALLOWED_VALIDATORS(alice_wallet, AdminUtils, True, NETUID, 2), SUDO_SET_WEIGHTS_SET_RATE_LIMIT(alice_wallet, AdminUtils, True, NETUID, 10), REGISTER_NEURON(bob_wallet), ] From 6ef4543e6389065ef771f9880c2106c9b0dba45c Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 2 Apr 2026 19:36:26 -0700 Subject: [PATCH 026/118] update url --- tests/e2e_tests/utils/e2e_test_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e_tests/utils/e2e_test_utils.py b/tests/e2e_tests/utils/e2e_test_utils.py index 1f24643d1f..b98f8bd036 100644 --- a/tests/e2e_tests/utils/e2e_test_utils.py +++ b/tests/e2e_tests/utils/e2e_test_utils.py @@ -64,7 +64,7 @@ def clone_or_update_templates(specific_commit=None): """ install_dir = template_path repo_mapping = { - templates_repo: "https://github.com/opentensor/subnet-template.git", + templates_repo: "https://github.com/latent-to/subnet-template", } cwd = os.getcwd() From 3c11f9a226a93e0f18185a16a946d0b1656a1629 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 2 Apr 2026 19:49:46 -0700 Subject: [PATCH 027/118] fix metagraph test --- tests/e2e_tests/test_metagraph.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/tests/e2e_tests/test_metagraph.py b/tests/e2e_tests/test_metagraph.py index 5c337eac02..ded015a6ba 100644 --- a/tests/e2e_tests/test_metagraph.py +++ b/tests/e2e_tests/test_metagraph.py @@ -1,6 +1,7 @@ import os.path import shutil import time +from dataclasses import replace import numpy as np import pytest @@ -27,6 +28,12 @@ NULL_KEY = tuple(bytearray(32)) +def _strip_unique_fields_for_metagraph_parity(m: MetagraphInfo) -> MetagraphInfo: + # get_metagraph_info (selective mechagraph) contains validators/commitments; + # get_all_metagraphs_info does not get these fields from the chain. + return replace(m, validators=None, commitments=None) + + torch = LazyLoadedTorch() @@ -833,7 +840,9 @@ def test_metagraph_info(subtensor, alice_wallet, bob_wallet): metagraph_infos = subtensor.metagraphs.get_all_metagraphs_info(block=block) assert len(metagraph_infos) == 4 - assert metagraph_infos[-1] == metagraph_info + assert metagraph_infos[-1] == _strip_unique_fields_for_metagraph_parity( + metagraph_info + ) # non-existed subnet metagraph_info = subtensor.metagraphs.get_metagraph_info(netuid=bob_sn.netuid + 1) @@ -1088,7 +1097,9 @@ async def test_metagraph_info_async(async_subtensor, alice_wallet, bob_wallet): ) assert len(metagraph_infos) == 4 - assert metagraph_infos[-1] == metagraph_info + assert metagraph_infos[-1] == _strip_unique_fields_for_metagraph_parity( + metagraph_info + ) # non-existed subnet metagraph_info = await async_subtensor.metagraphs.get_metagraph_info( From 8cc19d02d41dc4c53a9fcb9612422aba4ba92609 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 2 Apr 2026 19:52:24 -0700 Subject: [PATCH 028/118] update url --- tests/e2e_tests/utils/e2e_test_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e_tests/utils/e2e_test_utils.py b/tests/e2e_tests/utils/e2e_test_utils.py index b98f8bd036..dd31553524 100644 --- a/tests/e2e_tests/utils/e2e_test_utils.py +++ b/tests/e2e_tests/utils/e2e_test_utils.py @@ -64,7 +64,7 @@ def clone_or_update_templates(specific_commit=None): """ install_dir = template_path repo_mapping = { - templates_repo: "https://github.com/latent-to/subnet-template", + templates_repo: "https://github.com/latent-to/subnet-template.git", } cwd = os.getcwd() From fd4464e6133b3ef28032cdf7205a9a3045833ccf Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 2 Apr 2026 20:29:19 -0700 Subject: [PATCH 029/118] parity --- bittensor/core/extrinsics/children.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor/core/extrinsics/children.py b/bittensor/core/extrinsics/children.py index 5172d2c2f2..ed7daf21cd 100644 --- a/bittensor/core/extrinsics/children.py +++ b/bittensor/core/extrinsics/children.py @@ -111,7 +111,7 @@ def root_set_pending_childkey_cooldown_extrinsic( period: Optional[int] = None, raise_error: bool = False, wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, + wait_for_finalization: bool = True, wait_for_revealed_execution: bool = True, ) -> ExtrinsicResponse: """ From d8428e37e7f282ac3dbe3a6d5e971497990dfdeb Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 2 Apr 2026 21:08:34 -0700 Subject: [PATCH 030/118] reg fees change every block now --- tests/e2e_tests/test_subtensor_functions.py | 45 +++++++++------------ 1 file changed, 19 insertions(+), 26 deletions(-) diff --git a/tests/e2e_tests/test_subtensor_functions.py b/tests/e2e_tests/test_subtensor_functions.py index 1783a44e56..cec1d35884 100644 --- a/tests/e2e_tests/test_subtensor_functions.py +++ b/tests/e2e_tests/test_subtensor_functions.py @@ -15,6 +15,7 @@ ACTIVATE_SUBNET, REGISTER_NEURON, ) +from tests.helpers import CloseInValue """ Verifies: @@ -142,12 +143,9 @@ def test_subtensor_extrinsics(subtensor, templates, alice_wallet, bob_wallet): bob_balance = subtensor.wallets.get_balance(bob_wallet.coldkeypub.ss58_address) - alice_sn.execute_steps( - [ - ACTIVATE_SUBNET(alice_wallet), - REGISTER_NEURON(bob_wallet), - ] - ) + alice_sn.execute_one(ACTIVATE_SUBNET(alice_wallet)) + recycle_amount = subtensor.subnets.recycle(netuid) + reg_response = alice_sn.execute_one(REGISTER_NEURON(bob_wallet)) # Verify Bob's UID on netuid 2 is 1 assert ( @@ -157,17 +155,16 @@ def test_subtensor_extrinsics(subtensor, templates, alice_wallet, bob_wallet): == 1 ), "UID for Bob's hotkey on netuid 2 is not 1 as expected" - # Fetch recycle_amount to register to the subnet - recycle_amount = subtensor.subnets.recycle(netuid) - fee = alice_sn.calls[-1].response.extrinsic_fee + fee = reg_response.extrinsic_fee bob_balance_post_reg = subtensor.wallets.get_balance( bob_wallet.coldkeypub.ss58_address ) - # Ensure recycled amount is only deducted from the balance after registration - assert bob_balance - recycle_amount - fee == bob_balance_post_reg, ( - "Balance for Bob is not correct after burned register" - ) + # Burn decays every block and bumps after registration; hence tolerance + expected_post = bob_balance - recycle_amount - fee + assert ( + CloseInValue(bob_balance_post_reg, Balance.from_tao(0.002)) == expected_post + ), "Balance for Bob is not correct after burned register" with templates.validator(alice_wallet, netuid): # wait for 5 seconds for the metagraph and subtensor to refresh with latest data @@ -311,12 +308,9 @@ async def test_subtensor_extrinsics_async( bob_wallet.coldkeypub.ss58_address ) - await alice_sn.async_execute_steps( - [ - ACTIVATE_SUBNET(alice_wallet), - REGISTER_NEURON(bob_wallet), - ] - ) + await alice_sn.async_execute_one(ACTIVATE_SUBNET(alice_wallet)) + recycle_amount = await async_subtensor.subnets.recycle(netuid) + reg_response = await alice_sn.async_execute_one(REGISTER_NEURON(bob_wallet)) # Verify Bob's UID on netuid 2 is 1 assert ( @@ -326,17 +320,16 @@ async def test_subtensor_extrinsics_async( == 1 ), "UID for Bob's hotkey on netuid 2 is not 1 as expected." - # Fetch recycle_amount to register to the subnet - recycle_amount = await async_subtensor.subnets.recycle(netuid) - fee = alice_sn.calls[-1].response.extrinsic_fee + fee = reg_response.extrinsic_fee bob_balance_post_reg = await async_subtensor.wallets.get_balance( bob_wallet.coldkeypub.ss58_address ) - # Ensure recycled amount is only deducted from the balance after registration - assert bob_balance - recycle_amount - fee == bob_balance_post_reg, ( - "Balance for Bob is not correct after burned register" - ) + expected_post = bob_balance - recycle_amount - fee + # Burn decays every block and bumps after registration; hence tolerance + assert ( + CloseInValue(bob_balance_post_reg, Balance.from_tao(0.005)) == expected_post + ), "Balance for Bob is not correct after burned register" # neuron_info_old = subtensor.get_neuron_for_pubkey_and_subnet( # alice_wallet.hotkey.ss58_address, netuid=netuid From 20821aa03703be38632f6921d83777cebc2146b7 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 2 Apr 2026 21:09:02 -0700 Subject: [PATCH 031/118] account for neuron reg increasing costs --- tests/e2e_tests/test_staking.py | 48 +++++++++++++++++++++++++++------ 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/tests/e2e_tests/test_staking.py b/tests/e2e_tests/test_staking.py index f0010f234f..5d571249b5 100644 --- a/tests/e2e_tests/test_staking.py +++ b/tests/e2e_tests/test_staking.py @@ -20,6 +20,32 @@ from tests.helpers.helpers import CloseInValue +def _get_expected_balance_after_neuron_registrations( + initial_balance: Balance, sns: list[TestSubnet] +) -> Balance: + """ + Get the expected balance after neuron registrations + by subtracting the total registration cost from the initial balance. + """ + total_registration_cost = Balance.from_rao(0) + + for sn in sns: + for call in sn.calls: + if call.operation != REGISTER_NEURON.__name__: + continue + + balance_before = call.response.data.get("balance_before") + balance_after = call.response.data.get("balance_after") + + assert balance_before is not None and balance_after is not None, ( + "REGISTER_NEURON response missing balance data." + ) + + total_registration_cost += balance_before - balance_after + + return initial_balance - total_registration_cost + + def test_single_operation(subtensor, alice_wallet, bob_wallet): """ Tests: @@ -334,6 +360,9 @@ def test_batch_operations(subtensor, alice_wallet, bob_wallet): - Checks Accounts Balance """ subnets_tested = 2 + bob_balance_before_setup = subtensor.wallets.get_balance( + bob_wallet.coldkey.ss58_address + ) sns = [TestSubnet(subtensor) for _ in range(subnets_tested)] @@ -357,6 +386,9 @@ def test_batch_operations(subtensor, alice_wallet, bob_wallet): ) netuids = [sn.netuid for sn in sns] + expected_bob_balance = _get_expected_balance_after_neuron_registrations( + bob_balance_before_setup, sns + ) balances = subtensor.wallets.get_balances( alice_wallet.coldkey.ss58_address, @@ -443,10 +475,7 @@ def test_batch_operations(subtensor, alice_wallet, bob_wallet): bob_wallet.coldkey.ss58_address, ) - assert CloseInValue( # Make sure we are within 0.0002 TAO due to tx fees - balances[bob_wallet.coldkey.ss58_address], Balance.from_rao(5_000_000) - ) == Balance.from_tao(999_999.7979) - + assert balances[bob_wallet.coldkey.ss58_address] == expected_bob_balance assert balances[alice_wallet.coldkey.ss58_address] > alice_balance @@ -460,6 +489,9 @@ async def test_batch_operations_async(async_subtensor, alice_wallet, bob_wallet) - Checks Accounts Balance """ subnets_tested = 2 + bob_balance_before_setup = await async_subtensor.wallets.get_balance( + bob_wallet.coldkey.ss58_address + ) sns = [TestSubnet(async_subtensor) for _ in range(subnets_tested)] @@ -483,6 +515,9 @@ async def test_batch_operations_async(async_subtensor, alice_wallet, bob_wallet) ) netuids = [sn.netuid for sn in sns] + expected_bob_balance = _get_expected_balance_after_neuron_registrations( + bob_balance_before_setup, sns + ) balances = await async_subtensor.wallets.get_balances( alice_wallet.coldkey.ss58_address, @@ -569,10 +604,7 @@ async def test_batch_operations_async(async_subtensor, alice_wallet, bob_wallet) bob_wallet.coldkey.ss58_address, ) - assert CloseInValue( # Make sure we are within 0.0002 TAO due to tx fees - balances[bob_wallet.coldkey.ss58_address], Balance.from_rao(5_000_000) - ) == Balance.from_tao(999_999.7979) - + assert balances[bob_wallet.coldkey.ss58_address] == expected_bob_balance assert balances[alice_wallet.coldkey.ss58_address] > alice_balance From a9442f2cc2e36584e90e788b1a9a17eb5af7ea04 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 2 Apr 2026 21:11:57 -0700 Subject: [PATCH 032/118] update --- tests/unit_tests/extrinsics/test_children.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit_tests/extrinsics/test_children.py b/tests/unit_tests/extrinsics/test_children.py index d1db6e49ca..5d0343c139 100644 --- a/tests/unit_tests/extrinsics/test_children.py +++ b/tests/unit_tests/extrinsics/test_children.py @@ -91,7 +91,7 @@ def test_root_set_pending_childkey_cooldown_extrinsic(subtensor, mocker, fake_wa period=None, raise_error=False, wait_for_inclusion=True, - wait_for_finalization=False, + wait_for_finalization=True, ) assert success is True assert "Success" in message From 5fd407e9fca0809e3b0c8d175c4210fd00483919 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 6 Apr 2026 16:28:21 -0700 Subject: [PATCH 033/118] get_staking_hotkeys query --- bittensor/core/async_subtensor.py | 30 ++++++++++++++++++++++++++++++ bittensor/core/subtensor.py | 21 +++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index 4ef14ebd9a..0f22901573 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -4468,6 +4468,36 @@ async def get_stake_weight( ) return [u16_normalized_float(w) for w in cast(list[int], result or [])] + async def get_staking_hotkeys( + self, + coldkey_ss58: str, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> list[str]: + """ + Retrieves the hotkeys that have staked for a given coldkey. + + Parameters: + coldkey_ss58: The SS58 address of the coldkey. + block: The block number at which to query the stake information. + block_hash: The hash of the blockchain block number for the query. + reuse_block: Whether to reuse the last-used block hash. + + Returns: + A list of hotkey SS58 addresses that have staked for the given coldkey. + """ + block_hash = await self.determine_block_hash( + block=block, block_hash=block_hash, reuse_block=reuse_block + ) + result = await self.substrate.query( + module="SubtensorModule", + storage_function="StakingHotkeys", + params=[coldkey_ss58], + block_hash=block_hash, + ) + return [decode_account_id(hotkey[0]) for hotkey in result or []] + async def get_start_call_delay( self, block: Optional[int] = None, diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 3f84614253..35a8818833 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -3666,6 +3666,27 @@ def get_stake_weight(self, netuid: int, block: Optional[int] = None) -> list[flo ) return [u16_normalized_float(w) for w in cast(list[int], result or [])] + def get_staking_hotkeys( + self, coldkey_ss58: str, block: Optional[int] = None + ) -> list[str]: + """ + Retrieves the hotkeys that have staked for a given coldkey. + + Parameters: + coldkey_ss58: The SS58 address of the coldkey. + block: The block number at which to query the stake information. + + Returns: + A list of hotkey SS58 addresses that have staked for the given coldkey. + """ + result = self.substrate.query( + module="SubtensorModule", + storage_function="StakingHotkeys", + params=[coldkey_ss58], + block_hash=self.determine_block_hash(block), + ) + return [decode_account_id(hotkey[0]) for hotkey in result or []] + def get_start_call_delay(self, block: Optional[int] = None) -> int: """ Retrieves the start call delay in blocks. From 0740a83bf8fb513908fa9a952242b6b5a1817d25 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 6 Apr 2026 16:28:38 -0700 Subject: [PATCH 034/118] add condition sync --- bittensor/core/extrinsics/coldkey_swap.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/bittensor/core/extrinsics/coldkey_swap.py b/bittensor/core/extrinsics/coldkey_swap.py index d391261730..ab266abb4a 100644 --- a/bittensor/core/extrinsics/coldkey_swap.py +++ b/bittensor/core/extrinsics/coldkey_swap.py @@ -60,6 +60,7 @@ def announce_coldkey_swap_extrinsic( - A swap cost is charged when making the first announcement (not when reannouncing). - After making an announcement, all transactions from the coldkey are blocked except for `swap_coldkey_announced`. - The swap can only be executed after the delay period has passed (check via `get_coldkey_swap_announcement`). + - The destination coldkey cannot have any staking hotkeys. It must be completely new without any staking activity. - See: """ try: @@ -68,6 +69,18 @@ def announce_coldkey_swap_extrinsic( ).success: return unlocked + + staking_hotkeys = subtensor.get_staking_hotkeys(new_coldkey_ss58) + if staking_hotkeys: + error_msg = "Destination coldkey cannot have any staking hotkeys. Please use a new coldkey for the swap." + if raise_error: + raise ValueError(error_msg) + return ExtrinsicResponse( + success=False, + message=error_msg, + extrinsic_receipt=None, + ) + # Compute hash of new coldkey new_keypair = Keypair( ss58_address=new_coldkey_ss58, From d1ffa2f7ebf06d79143c9092889b76fa004a1109 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 6 Apr 2026 16:28:47 -0700 Subject: [PATCH 035/118] condition async --- bittensor/core/extrinsics/asyncex/coldkey_swap.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/bittensor/core/extrinsics/asyncex/coldkey_swap.py b/bittensor/core/extrinsics/asyncex/coldkey_swap.py index 67ac802051..202c41fa6a 100644 --- a/bittensor/core/extrinsics/asyncex/coldkey_swap.py +++ b/bittensor/core/extrinsics/asyncex/coldkey_swap.py @@ -60,6 +60,7 @@ async def announce_coldkey_swap_extrinsic( - A swap cost is charged when making the first announcement (not when reannouncing). - After making an announcement, all transactions from the coldkey are blocked except for `swap_coldkey_announced`. - The swap can only be executed after the delay period has passed (check via `get_coldkey_swap_announcement`). + - The destination coldkey cannot have any staking hotkeys. It must be completely new without any staking activity. - See: """ try: @@ -68,6 +69,17 @@ async def announce_coldkey_swap_extrinsic( ).success: return unlocked + staking_hotkeys = await subtensor.get_staking_hotkeys(new_coldkey_ss58) + if staking_hotkeys: + error_msg = "Destination coldkey cannot have any staking hotkeys. Please use a new coldkey for the swap." + if raise_error: + raise ValueError(error_msg) + return ExtrinsicResponse( + success=False, + message=error_msg, + extrinsic_receipt=None, + ) + # Compute hash of new coldkey new_coldkey = Keypair(ss58_address=new_coldkey_ss58) new_coldkey_hash = compute_coldkey_hash(new_coldkey) From 6c6f84e687150ae23c3157fa2d374037d078a62e Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 6 Apr 2026 16:29:14 -0700 Subject: [PATCH 036/118] update test --- tests/unit_tests/extrinsics/test_coldkey_swap.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/unit_tests/extrinsics/test_coldkey_swap.py b/tests/unit_tests/extrinsics/test_coldkey_swap.py index 28b280124c..bf490e25bc 100644 --- a/tests/unit_tests/extrinsics/test_coldkey_swap.py +++ b/tests/unit_tests/extrinsics/test_coldkey_swap.py @@ -20,6 +20,9 @@ def test_announce_coldkey_swap_extrinsic(subtensor, mocker): "unlock_wallet", return_value=ExtrinsicResponse(success=True, message="Unlocked"), ) + mocked_get_staking_hotkeys = mocker.patch.object( + subtensor, "get_staking_hotkeys", return_value=[] + ) mocked_keypair = mocker.patch("bittensor.core.extrinsics.coldkey_swap.Keypair") mocked_keypair_instance = mocker.MagicMock() mocked_keypair_instance.public_key = b"\x00" * 32 @@ -49,6 +52,7 @@ def test_announce_coldkey_swap_extrinsic(subtensor, mocker): # Asserts mocked_unlock_wallet.assert_called_once_with(wallet, False) + mocked_get_staking_hotkeys.assert_called_once_with(new_coldkey_ss58) mocked_keypair.assert_called_once_with(ss58_address=new_coldkey_ss58) mocked_compute_hash.assert_called_once_with(mocked_keypair_instance) mocked_subtensor_module.assert_called_once_with(subtensor) @@ -78,6 +82,9 @@ def test_announce_coldkey_swap_extrinsic_with_mev_protection(subtensor, mocker): "unlock_wallet", return_value=ExtrinsicResponse(success=True, message="Unlocked"), ) + mocked_get_staking_hotkeys = mocker.patch.object( + subtensor, "get_staking_hotkeys", return_value=[] + ) mocked_keypair = mocker.patch("bittensor.core.extrinsics.coldkey_swap.Keypair") mocked_keypair_instance = mocker.MagicMock() mocked_keypair_instance.public_key = b"\x00" * 32 @@ -107,6 +114,7 @@ def test_announce_coldkey_swap_extrinsic_with_mev_protection(subtensor, mocker): # Asserts mocked_unlock_wallet.assert_called_once_with(wallet, False) + mocked_get_staking_hotkeys.assert_called_once_with(new_coldkey_ss58) mocked_subtensor_module.assert_called_once_with(subtensor) mocked_pallet_instance.announce_coldkey_swap.assert_called_once_with( new_coldkey_hash="0x" + "00" * 32 From 715893481383895bde7566338e56e6f014871a43 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 6 Apr 2026 16:29:24 -0700 Subject: [PATCH 037/118] async tests --- tests/unit_tests/extrinsics/asyncex/test_coldkey_swap.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/unit_tests/extrinsics/asyncex/test_coldkey_swap.py b/tests/unit_tests/extrinsics/asyncex/test_coldkey_swap.py index 7307f9a4bf..0f969281bd 100644 --- a/tests/unit_tests/extrinsics/asyncex/test_coldkey_swap.py +++ b/tests/unit_tests/extrinsics/asyncex/test_coldkey_swap.py @@ -22,6 +22,9 @@ async def test_announce_coldkey_swap_extrinsic(subtensor, mocker): "unlock_wallet", return_value=ExtrinsicResponse(success=True, message="Unlocked"), ) + mocked_get_staking_hotkeys = mocker.patch.object( + subtensor, "get_staking_hotkeys", new=mocker.AsyncMock(return_value=[]) + ) mocked_keypair = mocker.patch( "bittensor.core.extrinsics.asyncex.coldkey_swap.Keypair" ) @@ -55,6 +58,7 @@ async def test_announce_coldkey_swap_extrinsic(subtensor, mocker): # Asserts mocked_unlock_wallet.assert_called_once_with(wallet, False) + mocked_get_staking_hotkeys.assert_awaited_once_with(new_coldkey_ss58) mocked_keypair.assert_called_once_with(ss58_address=new_coldkey_ss58) mocked_compute_hash.assert_called_once_with(mocked_keypair_instance) mocked_subtensor_module.assert_called_once_with(subtensor) @@ -85,6 +89,9 @@ async def test_announce_coldkey_swap_extrinsic_with_mev_protection(subtensor, mo "unlock_wallet", return_value=ExtrinsicResponse(success=True, message="Unlocked"), ) + mocked_get_staking_hotkeys = mocker.patch.object( + subtensor, "get_staking_hotkeys", new=mocker.AsyncMock(return_value=[]) + ) mocked_keypair = mocker.patch( "bittensor.core.extrinsics.asyncex.coldkey_swap.Keypair" ) @@ -118,6 +125,7 @@ async def test_announce_coldkey_swap_extrinsic_with_mev_protection(subtensor, mo # Asserts mocked_unlock_wallet.assert_called_once_with(wallet, False) + mocked_get_staking_hotkeys.assert_awaited_once_with(new_coldkey_ss58) mocked_subtensor_module.assert_called_once_with(subtensor) mocked_pallet_instance.announce_coldkey_swap.assert_awaited_once_with( new_coldkey_hash="0x" + "00" * 32 From c66c52b8f9fac757d16cd826b6305055c0aab2e6 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 6 Apr 2026 16:41:17 -0700 Subject: [PATCH 038/118] ruff & tests --- bittensor/core/extrinsics/coldkey_swap.py | 1 - bittensor/extras/subtensor_api/staking.py | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor/core/extrinsics/coldkey_swap.py b/bittensor/core/extrinsics/coldkey_swap.py index ab266abb4a..c408cd7324 100644 --- a/bittensor/core/extrinsics/coldkey_swap.py +++ b/bittensor/core/extrinsics/coldkey_swap.py @@ -69,7 +69,6 @@ def announce_coldkey_swap_extrinsic( ).success: return unlocked - staking_hotkeys = subtensor.get_staking_hotkeys(new_coldkey_ss58) if staking_hotkeys: error_msg = "Destination coldkey cannot have any staking hotkeys. Please use a new coldkey for the swap." diff --git a/bittensor/extras/subtensor_api/staking.py b/bittensor/extras/subtensor_api/staking.py index 0ab2e5485b..7c6f31ae9c 100644 --- a/bittensor/extras/subtensor_api/staking.py +++ b/bittensor/extras/subtensor_api/staking.py @@ -31,6 +31,7 @@ def __init__(self, subtensor: Union["_Subtensor", "_AsyncSubtensor"]): self.get_stake_info_for_coldkeys = subtensor.get_stake_info_for_coldkeys self.get_stake_movement_fee = subtensor.get_stake_movement_fee self.get_stake_weight = subtensor.get_stake_weight + self.get_staking_hotkeys = subtensor.get_staking_hotkeys self.get_unstake_fee = subtensor.get_unstake_fee self.move_stake = subtensor.move_stake self.set_auto_stake = subtensor.set_auto_stake From f3f5d2b2eb06705f0859e8b63560457ebf606b86 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 7 Apr 2026 10:43:58 -0700 Subject: [PATCH 039/118] updates workflow perms and echos checksums/versions for reviewers --- .github/workflows/release.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d7e7f8e9af..72b92a53e0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,6 +12,8 @@ jobs: build: name: Build Python distribution runs-on: ubuntu-latest + permissions: + contents: read steps: - uses: actions/checkout@v6 @@ -65,6 +67,14 @@ jobs: name: dist path: dist/ + - name: Verify artifact checksums + run: | + echo "Artifacts to be published:" + ls -la dist/ + echo "" + echo "SHA256 checksums:" + sha256sum dist/* + - name: Publish package distributions to PyPI uses: pypa/gh-action-pypi-publish@release/v1 with: From 590f218d5c4a5b631b85cfc0a1c982b3e0dc26cf Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Fri, 10 Apr 2026 11:19:57 -0700 Subject: [PATCH 040/118] update metagraph info data for comparison --- tests/e2e_tests/test_metagraph.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/e2e_tests/test_metagraph.py b/tests/e2e_tests/test_metagraph.py index ded015a6ba..102899bae5 100644 --- a/tests/e2e_tests/test_metagraph.py +++ b/tests/e2e_tests/test_metagraph.py @@ -686,14 +686,14 @@ def test_metagraph_info(subtensor, alice_wallet, bob_wallet): ), active=(True,), validator_permit=(False,), - pruning_score=[0.0], + pruning_score=[], last_update=(0,), emission=[Balance(0).set_unit(1)], dividends=[0.0], incentives=[0.0], consensus=[0.0], - trust=[0.0], - rank=[0.0], + trust=[], + rank=[], block_at_registration=(0,), alpha_stake=[Balance.from_tao(1.0).set_unit(1)], tao_stake=[Balance(0)], @@ -937,14 +937,14 @@ async def test_metagraph_info_async(async_subtensor, alice_wallet, bob_wallet): ), active=(True,), validator_permit=(False,), - pruning_score=[0.0], + pruning_score=[], last_update=(0,), emission=[Balance(0).set_unit(1)], dividends=[0.0], incentives=[0.0], consensus=[0.0], - trust=[0.0], - rank=[0.0], + trust=[], + rank=[], block_at_registration=(0,), alpha_stake=[Balance.from_tao(1.0).set_unit(1)], tao_stake=[Balance(0)], From e93b7515dbe40b6542f69509ceb5f86b225c3e0c Mon Sep 17 00:00:00 2001 From: BD Himes Date: Tue, 14 Apr 2026 12:35:30 +0200 Subject: [PATCH 041/118] Removes flake8, uses ruff for linter check --- .github/workflows/flake8-and-mypy.yml | 5 +---- .github/workflows/ruff.yml | 30 ++++++++++++--------------- pyproject.toml | 1 - 3 files changed, 14 insertions(+), 22 deletions(-) diff --git a/.github/workflows/flake8-and-mypy.yml b/.github/workflows/flake8-and-mypy.yml index d1470cd6ef..ee436db4ca 100644 --- a/.github/workflows/flake8-and-mypy.yml +++ b/.github/workflows/flake8-and-mypy.yml @@ -1,4 +1,4 @@ -name: Flake8 and Mypy - linters check +name: Mypy check permissions: contents: read @@ -55,8 +55,5 @@ jobs: - name: Sync dev deps run: uv sync --extra dev --dev - - name: Flake8 - run: uv run flake8 bittensor/ --count - - name: Mypy run: uv run mypy --ignore-missing-imports bittensor/ diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml index fdf1141a7b..9f2676aac7 100644 --- a/.github/workflows/ruff.yml +++ b/.github/workflows/ruff.yml @@ -1,4 +1,4 @@ -name: Ruff - formatter check +name: Ruff - formatter/linter check permissions: contents: read @@ -8,26 +8,22 @@ on: jobs: ruff: - if: github.event.pull_request.draft == false runs-on: ubuntu-latest - + timeout-minutes: 10 steps: - name: Checkout repository uses: actions/checkout@v6 - - name: Set up Python - uses: actions/setup-python@v6 + - name: Ruff format check + uses: astral-sh/ruff-action@v4 with: - python-version: "3.11" - - - name: Install Ruff in virtual environment - run: | - python -m venv venv - source venv/bin/activate - python -m pip install --upgrade pip - python -m pip install ruff==0.11.5 + version: "0.11.5" + args: "format --diff" + src: "bittensor tests" - - name: Ruff format check - run: | - source venv/bin/activate - python -m ruff format --diff bittensor + - name: Ruff linter check + uses: astral-sh/ruff-action@v4 + with: + version: "0.11.5" + args: "check" + src: "bittensor" diff --git a/pyproject.toml b/pyproject.toml index b43cab375c..d244bea20e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,6 @@ dev = [ "pytest-cov==4.0.0", "ddt==1.6.0", "hypothesis==6.81.1", - "flake8==7.0.0", "mypy==1.8.0", "types-retry==0.9.9.4", "typing_extensions>= 4.0.0; python_version<'3.11'", From dc62262b26b289dfdc27c74cb75ececf3fc14251 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Tue, 14 Apr 2026 12:37:11 +0200 Subject: [PATCH 042/118] Update version --- .github/workflows/ruff.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml index 9f2676aac7..4990b1e391 100644 --- a/.github/workflows/ruff.yml +++ b/.github/workflows/ruff.yml @@ -15,14 +15,14 @@ jobs: uses: actions/checkout@v6 - name: Ruff format check - uses: astral-sh/ruff-action@v4 + uses: astral-sh/ruff-action@v4.0.0 with: version: "0.11.5" args: "format --diff" src: "bittensor tests" - name: Ruff linter check - uses: astral-sh/ruff-action@v4 + uses: astral-sh/ruff-action@v4.0.0 with: version: "0.11.5" args: "check" From 1713ed8c08aa32355f85e96d9f0fea98a84a3dc9 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Tue, 14 Apr 2026 12:48:23 +0200 Subject: [PATCH 043/118] Ruff linter fixes --- bittensor/__init__.py | 6 +++--- bittensor/core/extrinsics/asyncex/registration.py | 4 ++-- bittensor/core/extrinsics/registration.py | 4 ++-- bittensor/core/extrinsics/utils.py | 10 +++++----- bittensor/core/metagraph.py | 2 +- bittensor/extras/dev_framework/__init__.py | 12 ++++++------ bittensor/extras/dev_framework/calls/__init__.py | 6 +++--- bittensor/extras/dev_framework/subnet.py | 2 +- bittensor/utils/__init__.py | 1 - bittensor/utils/btlogging/defines.py | 2 +- bittensor/utils/mock/__init__.py | 2 +- bittensor/utils/mock/subtensor_mock.py | 2 +- bittensor/utils/version.py | 2 +- 13 files changed, 27 insertions(+), 28 deletions(-) diff --git a/bittensor/__init__.py b/bittensor/__init__.py index e290570af2..2f96a83472 100644 --- a/bittensor/__init__.py +++ b/bittensor/__init__.py @@ -1,3 +1,3 @@ -from .core.settings import __version__, DEFAULTS, DEFAULT_NETWORK -from .utils.btlogging import logging -from .utils.easy_imports import * +from .core.settings import __version__ as __version__, DEFAULTS as DEFAULTS, DEFAULT_NETWORK as DEFAULT_NETWORK +from .utils.btlogging import logging # noqa: F401 +from .utils.easy_imports import * # noqa: F403 diff --git a/bittensor/core/extrinsics/asyncex/registration.py b/bittensor/core/extrinsics/asyncex/registration.py index adcd78c992..2e9531380f 100644 --- a/bittensor/core/extrinsics/asyncex/registration.py +++ b/bittensor/core/extrinsics/asyncex/registration.py @@ -341,7 +341,7 @@ async def register_extrinsic( if not torch.cuda.is_available(): return ExtrinsicResponse(False, "CUDA not available.").with_log() - logging.debug(f"Creating a POW with CUDA.") + logging.debug("Creating a POW with CUDA.") pow_result = await create_pow_async( subtensor=subtensor, wallet=wallet, @@ -355,7 +355,7 @@ async def register_extrinsic( log_verbose=log_verbose, ) else: - logging.debug(f"Creating a POW.") + logging.debug("Creating a POW.") pow_result = await create_pow_async( subtensor=subtensor, wallet=wallet, diff --git a/bittensor/core/extrinsics/registration.py b/bittensor/core/extrinsics/registration.py index 50962fb9c1..fd8952bbf8 100644 --- a/bittensor/core/extrinsics/registration.py +++ b/bittensor/core/extrinsics/registration.py @@ -333,7 +333,7 @@ def register_extrinsic( if not torch.cuda.is_available(): return ExtrinsicResponse(False, "CUDA not available.").with_log() - logging.debug(f"Creating a POW with CUDA.") + logging.debug("Creating a POW with CUDA.") pow_result = create_pow( subtensor=subtensor, wallet=wallet, @@ -347,7 +347,7 @@ def register_extrinsic( log_verbose=log_verbose, ) else: - logging.debug(f"Creating a POW.") + logging.debug("Creating a POW.") pow_result = create_pow( subtensor=subtensor, wallet=wallet, diff --git a/bittensor/core/extrinsics/utils.py b/bittensor/core/extrinsics/utils.py index a491d82da8..7af0985eab 100644 --- a/bittensor/core/extrinsics/utils.py +++ b/bittensor/core/extrinsics/utils.py @@ -217,11 +217,11 @@ def apply_pure_proxy_data( # If triggered events are not available or event PureCreated does not exist in the response, return the response # with warning message ot raise the error if raise_error is True. message = ( - f"The ExtrinsicResponse doesn't contain pure_proxy data (`pure_account`, `spawner`, `proxy_type`, etc.) " - f"because the extrinsic receipt doesn't have triggered events. This typically happens when " - f"`wait_for_inclusion=False` or when `block_hash` is not available. To get this data, either pass " - f"`wait_for_inclusion=True` when calling this function, or retrieve the data manually from the blockchain " - f"using the extrinsic hash." + "The ExtrinsicResponse doesn't contain pure_proxy data (`pure_account`, `spawner`, `proxy_type`, etc.) " + "because the extrinsic receipt doesn't have triggered events. This typically happens when " + "`wait_for_inclusion=False` or when `block_hash` is not available. To get this data, either pass " + "`wait_for_inclusion=True` when calling this function, or retrieve the data manually from the blockchain " + "using the extrinsic hash." ) if response.extrinsic is not None and hasattr(response.extrinsic, "extrinsic_hash"): extrinsic_hash = response.extrinsic.extrinsic_hash diff --git a/bittensor/core/metagraph.py b/bittensor/core/metagraph.py index 44f4a033c1..5496dfca9c 100644 --- a/bittensor/core/metagraph.py +++ b/bittensor/core/metagraph.py @@ -320,7 +320,7 @@ def S(self) -> Tensor: return self.stake @property - def I(self) -> Tensor: + def I(self) -> Tensor: # noqa: E743 """ Incentive values of neurons represent the rewards they receive for their contributions to the network. The Bittensor network employs an incentive mechanism that rewards neurons based on their diff --git a/bittensor/extras/dev_framework/__init__.py b/bittensor/extras/dev_framework/__init__.py index 6aed8910fc..2fbd8693b4 100644 --- a/bittensor/extras/dev_framework/__init__.py +++ b/bittensor/extras/dev_framework/__init__.py @@ -1,8 +1,8 @@ -from .calls import * # noqa: F401 +from .calls import * # noqa: F403 from .subnet import ( - NETUID, - TestSubnet, - ACTIVATE_SUBNET, - REGISTER_SUBNET, - REGISTER_NEURON, + NETUID as NETUID, + TestSubnet as TestSubnet, + ACTIVATE_SUBNET as ACTIVATE_SUBNET, + REGISTER_SUBNET as REGISTER_SUBNET, + REGISTER_NEURON as REGISTER_NEURON, ) diff --git a/bittensor/extras/dev_framework/calls/__init__.py b/bittensor/extras/dev_framework/calls/__init__.py index 0455d723f9..f88c0a6808 100644 --- a/bittensor/extras/dev_framework/calls/__init__.py +++ b/bittensor/extras/dev_framework/calls/__init__.py @@ -11,9 +11,9 @@ import os from bittensor import Subtensor -from bittensor.extras.dev_framework.calls.sudo_calls import * # noqa: F401 -from bittensor.extras.dev_framework.calls.non_sudo_calls import * # noqa: F401 -from bittensor.extras.dev_framework.calls.pallets import * # noqa: F401 +from bittensor.extras.dev_framework.calls.sudo_calls import * # noqa: F403 +from bittensor.extras.dev_framework.calls.non_sudo_calls import * # noqa: F403 +from bittensor.extras.dev_framework.calls.pallets import * # noqa: F403 HEADER = '''""" This file is auto-generated. Do not edit manually. diff --git a/bittensor/extras/dev_framework/subnet.py b/bittensor/extras/dev_framework/subnet.py index b14c530500..13b15366cc 100644 --- a/bittensor/extras/dev_framework/subnet.py +++ b/bittensor/extras/dev_framework/subnet.py @@ -10,7 +10,7 @@ from bittensor.core.types import ExtrinsicResponse from bittensor.extras import SubtensorApi from bittensor.utils.btlogging import logging -from .calls import * # noqa: F401# +from .calls import * # noqa: F403 from .utils import ( is_instance_namedtuple, split_command, diff --git a/bittensor/utils/__init__.py b/bittensor/utils/__init__.py index 44889d4c40..52003f0017 100644 --- a/bittensor/utils/__init__.py +++ b/bittensor/utils/__init__.py @@ -28,7 +28,6 @@ if TYPE_CHECKING: from bittensor_wallet import Wallet from bittensor.core.types import ExtrinsicResponse - from bittensor.utils.balance import Balance # keep save from import analyzer as obvious aliases hex_to_ss58 = ss58_encode diff --git a/bittensor/utils/btlogging/defines.py b/bittensor/utils/btlogging/defines.py index f71a99e702..526e5444b3 100644 --- a/bittensor/utils/btlogging/defines.py +++ b/bittensor/utils/btlogging/defines.py @@ -2,7 +2,7 @@ BASE_LOG_FORMAT = "%(asctime)s | %(levelname)s | %(message)s" TRACE_LOG_FORMAT = ( - f"%(asctime)s | %(levelname)s | %(name)s:%(filename)s:%(lineno)s | %(message)s" + "%(asctime)s | %(levelname)s | %(name)s:%(filename)s:%(lineno)s | %(message)s" ) DATE_FORMAT = "%Y-%m-%d %H:%M:%S" BITTENSOR_LOGGER_NAME = "bittensor" diff --git a/bittensor/utils/mock/__init__.py b/bittensor/utils/mock/__init__.py index 04893c78a3..fc4d22dacf 100644 --- a/bittensor/utils/mock/__init__.py +++ b/bittensor/utils/mock/__init__.py @@ -1 +1 @@ -from .subtensor_mock import MockSubtensor +from .subtensor_mock import MockSubtensor as MockSubtensor diff --git a/bittensor/utils/mock/subtensor_mock.py b/bittensor/utils/mock/subtensor_mock.py index 95c6d4af54..497a231655 100644 --- a/bittensor/utils/mock/subtensor_mock.py +++ b/bittensor/utils/mock/subtensor_mock.py @@ -871,7 +871,7 @@ def _neuron_subnet_exists( dividends = self._get_most_recent_storage( subtensor_state["Dividends"][netuid][uid], block ) - pruning_score = self._get_most_recent_storage( + pruning_score = self._get_most_recent_storage( # noqa subtensor_state["PruningScores"][netuid][uid], block ) last_update = self._get_most_recent_storage( diff --git a/bittensor/utils/version.py b/bittensor/utils/version.py index e78fd109e8..53aa8b8550 100644 --- a/bittensor/utils/version.py +++ b/bittensor/utils/version.py @@ -117,6 +117,6 @@ def check_latest_version_in_pypi(): except InvalidVersion: # stay silent if InvalidVersion pass - except (requests.RequestException, KeyError) as e: + except (requests.RequestException, KeyError): # stay silent if not internet connection or pypi.org issue pass From 23cfdc7eb800469fceae946f789c8379ff108d50 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Tue, 14 Apr 2026 12:50:45 +0200 Subject: [PATCH 044/118] Ruff --- bittensor/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/bittensor/__init__.py b/bittensor/__init__.py index 2f96a83472..a9d99b0c76 100644 --- a/bittensor/__init__.py +++ b/bittensor/__init__.py @@ -1,3 +1,7 @@ -from .core.settings import __version__ as __version__, DEFAULTS as DEFAULTS, DEFAULT_NETWORK as DEFAULT_NETWORK +from .core.settings import ( + __version__ as __version__, + DEFAULTS as DEFAULTS, + DEFAULT_NETWORK as DEFAULT_NETWORK, +) from .utils.btlogging import logging # noqa: F401 from .utils.easy_imports import * # noqa: F403 From 3c1a1de73f3bd79ade3dd62d1e85930b7676f54b Mon Sep 17 00:00:00 2001 From: BD Himes Date: Tue, 14 Apr 2026 13:20:50 +0200 Subject: [PATCH 045/118] Add reqs --- pyproject.toml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d244bea20e..e98ce292fb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,11 +29,12 @@ dependencies = [ "retry==0.9.2", "requests>=2.0.0,<3.0", "pydantic>=2.3,<3", - "scalecodec==1.2.12", + "cyscale==0.1.11", "uvicorn", "bittensor-drand>=1.3.0,<2.0.0", "bittensor-wallet==4.0.1", - "async-substrate-interface>=1.6.2,<2.0.0" + # TODO this will need to be changed to asi 2.0-3.0 when merged + "async-substrate-interface @ git+https://github.com/opentensor/async-substrate-interface.git@release/2.0.0", ] [project.optional-dependencies] From 73fba4a7acd4e4343ad91b2a02be0e40b7d20eae Mon Sep 17 00:00:00 2001 From: BD Himes Date: Tue, 14 Apr 2026 13:48:19 +0200 Subject: [PATCH 046/118] Remove ScaleObj --- bittensor/core/async_subtensor.py | 27 ++++++---------- bittensor/core/chain_data/coldkey_swap.py | 8 ++--- bittensor/core/chain_data/utils.py | 6 ++-- bittensor/core/subtensor.py | 32 ++++++++----------- bittensor/utils/balance.py | 22 +------------ .../chain_data/test_coldkey_swap.py | 8 ++--- tests/unit_tests/test_async_subtensor.py | 7 ++-- tests/unit_tests/test_subtensor.py | 7 ++-- 8 files changed, 45 insertions(+), 72 deletions(-) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index 0f22901573..d142a71473 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -8,11 +8,11 @@ import scalecodec from async_substrate_interface import AsyncSubstrateInterface from async_substrate_interface.substrate_addons import RetryAsyncSubstrate -from async_substrate_interface.types import ScaleObj from async_substrate_interface.utils.storage import StorageKey from bittensor_drand import get_encrypted_commitment from bittensor_wallet.utils import SS58_FORMAT from scalecodec import GenericCall +from scalecodec.base import ScaleType from bittensor.core.chain_data import ( ColdkeySwapAnnouncementInfo, @@ -43,7 +43,6 @@ from bittensor.core.chain_data.chain_identity import ChainIdentity from bittensor.core.chain_data.delegate_info import DelegatedInfo from bittensor.core.chain_data.utils import ( - decode_block, decode_metadata, decode_revealed_commitment, decode_revealed_commitment_with_hotkey, @@ -755,7 +754,7 @@ async def query_constant( block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> Optional["ScaleObj"]: + ) -> Optional[ScaleType[Any]]: """Retrieves a constant from the specified module on the Bittensor blockchain. Use this function for nonstandard queries to constants defined within the Bittensor blockchain, if these cannot @@ -859,7 +858,7 @@ async def query_module( block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> Optional[Union["ScaleObj", Any]]: + ) -> Optional[ScaleType[Any]]: """Queries any module storage on the Bittensor blockchain with the specified parameters and block number. This function is a generic query interface that allows for flexible and diverse data retrieval from various blockchain modules. Use this function for nonstandard queries to storage defined within the Bittensor @@ -928,7 +927,7 @@ async def query_subtensor( block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> Optional[Union["ScaleObj", Any]]: + ) -> Optional[ScaleType[Any]]: """Queries named storage from the Subtensor module on the Bittensor blockchain. Use this function for nonstandard queries to storage defined within the Bittensor blockchain, if these cannot @@ -1986,7 +1985,7 @@ async def get_coldkey_swap_announcement( if query is None: return None return ColdkeySwapAnnouncementInfo.from_query( - coldkey_ss58=coldkey_ss58, query=cast(ScaleObj, query) + coldkey_ss58=coldkey_ss58, query=query ) async def get_coldkey_swap_announcements( @@ -2130,9 +2129,7 @@ async def get_coldkey_swap_dispute( ) if query is None: return None - return ColdkeySwapDisputeInfo.from_query( - coldkey_ss58=coldkey_ss58, query=cast(ScaleObj, query) - ) + return ColdkeySwapDisputeInfo.from_query(coldkey_ss58=coldkey_ss58, query=query) async def get_coldkey_swap_disputes( self, @@ -2851,7 +2848,7 @@ async def get_last_bonds_reset( block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ): + ) -> ScaleType[int]: """Retrieves the block number when bonds were last reset for a specific hotkey on a subnet. Parameters: @@ -2911,10 +2908,7 @@ async def get_last_commitment_bonds_reset_block( block_data = await self.get_last_bonds_reset( netuid, hotkey, block, block_hash, reuse_block ) - try: - return decode_block(block_data) - except TypeError: - return None + return getattr(block_data, "value", None) async def get_liquidity_list( self, @@ -5220,9 +5214,8 @@ async def is_fast_blocks(self) -> bool: - """ - slot_duration_obj = cast( - ScaleObj, await self.query_constant("Aura", "SlotDuration") - ) + slot_duration_obj = await self.query_constant("Aura", "SlotDuration") + assert slot_duration_obj is not None return slot_duration_obj.value == 250 async def is_hotkey_delegate( diff --git a/bittensor/core/chain_data/coldkey_swap.py b/bittensor/core/chain_data/coldkey_swap.py index e638569e53..c323a84a55 100644 --- a/bittensor/core/chain_data/coldkey_swap.py +++ b/bittensor/core/chain_data/coldkey_swap.py @@ -1,8 +1,6 @@ from dataclasses import asdict, dataclass, fields from typing import Optional - -from async_substrate_interface.types import ScaleObj - +from scalecodec.base import ScaleType from bittensor.core.chain_data.utils import decode_account_id @@ -34,7 +32,7 @@ class ColdkeySwapAnnouncementInfo: @classmethod def from_query( - cls, coldkey_ss58: str, query: "ScaleObj" + cls, coldkey_ss58: str, query: ScaleType ) -> Optional["ColdkeySwapAnnouncementInfo"]: """ Creates a ColdkeySwapAnnouncementInfo object from a Substrate query result. @@ -98,7 +96,7 @@ class ColdkeySwapDisputeInfo: @classmethod def from_query( - cls, coldkey_ss58: str, query: "ScaleObj" + cls, coldkey_ss58: str, query: ScaleType ) -> Optional["ColdkeySwapDisputeInfo"]: """ Creates a ColdkeySwapDisputeInfo object from a Substrate query result. diff --git a/bittensor/core/chain_data/utils.py b/bittensor/core/chain_data/utils.py index 5374652d8c..d6946c96ae 100644 --- a/bittensor/core/chain_data/utils.py +++ b/bittensor/core/chain_data/utils.py @@ -3,9 +3,9 @@ from enum import Enum from typing import Optional, Union, TYPE_CHECKING -from async_substrate_interface.types import ScaleObj from bittensor_wallet.utils import SS58_FORMAT -from scalecodec.base import RuntimeConfiguration, ScaleBytes +from scalecodec import ScaleBytes +from scalecodec.base import RuntimeConfiguration, ScaleType from scalecodec.type_registry import load_type_registry_preset from scalecodec.utils.ss58 import ss58_encode @@ -152,7 +152,7 @@ def decode_block(data: bytes) -> int: Returns: int: The decoded block. """ - return int(data.value) if isinstance(data, ScaleObj) else data + return int.from_bytes(data, byteorder="little") # TODO verify this is little endian def decode_revealed_commitment(encoded_data) -> tuple[int, str]: diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 35a8818833..73c2299835 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -7,10 +7,10 @@ from async_substrate_interface.errors import SubstrateRequestException from async_substrate_interface.substrate_addons import RetrySyncSubstrate from async_substrate_interface.sync_substrate import SubstrateInterface -from async_substrate_interface.types import ScaleObj from async_substrate_interface.utils.storage import StorageKey from bittensor_drand import get_encrypted_commitment from bittensor_wallet.utils import SS58_FORMAT +from scalecodec.base import ScaleType from bittensor.core.axon import Axon from bittensor.core.chain_data import ( @@ -42,7 +42,6 @@ ) from bittensor.core.chain_data.chain_identity import ChainIdentity from bittensor.core.chain_data.utils import ( - decode_block, decode_metadata, decode_revealed_commitment, decode_revealed_commitment_with_hotkey, @@ -638,7 +637,7 @@ def sim_swap( def query_constant( self, module_name: str, constant_name: str, block: Optional[int] = None - ) -> Optional["ScaleObj"]: + ) -> Optional[ScaleType[Any]]: """Retrieves a constant from the specified module on the Bittensor blockchain. Use this function for nonstandard queries to constants defined within the Bittensor blockchain, if these cannot @@ -721,7 +720,7 @@ def query_module( name: str, params: Optional[list] = None, block: Optional[int] = None, - ) -> Optional[Union["ScaleObj", Any, FixedPoint]]: + ) -> Optional[ScaleType]: """Queries any module storage on the Bittensor blockchain with the specified parameters and block number. This function is a generic query interface that allows for flexible and diverse data retrieval from various blockchain modules. Use this function for nonstandard queries to storage defined within the Bittensor @@ -775,7 +774,7 @@ def query_subtensor( name: str, params: Optional[list] = None, block: Optional[int] = None, - ) -> Optional[Union["ScaleObj", Any]]: + ) -> Optional[ScaleType]: """Queries named storage from the Subtensor module on the Bittensor blockchain. Use this function for nonstandard queries to constants defined within the Bittensor blockchain, if these cannot @@ -1604,7 +1603,7 @@ def get_coldkey_swap_announcement( if query is None: return None return ColdkeySwapAnnouncementInfo.from_query( - coldkey_ss58=coldkey_ss58, query=cast(ScaleObj, query) + coldkey_ss58=coldkey_ss58, query=cast(ScaleType, query) ) def get_coldkey_swap_announcements( @@ -1771,9 +1770,7 @@ def get_coldkey_swap_dispute( ) if query is None: return None - return ColdkeySwapDisputeInfo.from_query( - coldkey_ss58=coldkey_ss58, query=cast(ScaleObj, query) - ) + return ColdkeySwapDisputeInfo.from_query(coldkey_ss58=coldkey_ss58, query=query) def get_coldkey_swap_disputes( self, @@ -2350,10 +2347,7 @@ def get_last_commitment_bonds_reset_block( ) return None block_data = self.get_last_bonds_reset(netuid, hotkey_ss58, block) - try: - return decode_block(block_data) - except TypeError: - return None + return getattr(block_data, "value", None) def get_liquidity_list( self, @@ -3588,8 +3582,8 @@ def get_stake_for_hotkey( hotkey_alpha_query = self.query_subtensor( name="TotalHotkeyAlpha", params=[hotkey_ss58, netuid], block=block ) - hotkey_alpha = cast(ScaleObj, hotkey_alpha_query) - balance = Balance.from_rao(hotkey_alpha.value) + assert hotkey_alpha_query is not None + balance = Balance.from_rao(hotkey_alpha_query.value) balance.set_unit(netuid=netuid) return balance @@ -3969,8 +3963,9 @@ def get_timestamp(self, block: Optional[int] = None) -> datetime: Returns: datetime object for the timestamp of the block """ - unix = cast(ScaleObj, self.query_module("Timestamp", "Now", block=block)).value - return datetime.fromtimestamp(unix / 1000, tz=timezone.utc) + unix = self.query_module("Timestamp", "Now", block=block) + assert unix is not None + return datetime.fromtimestamp(unix.value / 1000, tz=timezone.utc) def get_total_subnets(self, block: Optional[int] = None) -> Optional[int]: """Retrieves the total number of subnets within the Bittensor network as of a specific blockchain block. @@ -4254,7 +4249,8 @@ def is_fast_blocks(self) -> bool: - """ - slot_duration_obj = cast(ScaleObj, self.query_constant("Aura", "SlotDuration")) + slot_duration_obj = self.query_constant("Aura", "SlotDuration") + assert slot_duration_obj is not None return slot_duration_obj.value == 250 def is_hotkey_delegate(self, hotkey_ss58: str, block: Optional[int] = None) -> bool: diff --git a/bittensor/utils/balance.py b/bittensor/utils/balance.py index 670174f48e..0fc55b4d5e 100644 --- a/bittensor/utils/balance.py +++ b/bittensor/utils/balance.py @@ -1,7 +1,7 @@ from typing import Optional, TypedDict, Union from scalecodec import ScaleType -from async_substrate_interface.types import ScaleObj +from scalecodec.utils.math import fixed_to_float as fixed_to_float from bittensor.core import settings from bittensor.core.errors import BalanceTypeError, BalanceUnitMismatchError @@ -374,26 +374,6 @@ class FixedPoint(TypedDict): bits: int -def fixed_to_float( - fixed: FixedPoint | ScaleType | ScaleObj, frac_bits: int = 64, total_bits: int = 128 -) -> float: - """Converts a fixed-point value (e.g., U64F64) into a floating-point number.""" - # By default, this is a U64F64 - # which is 64 bits of integer and 64 bits of fractional - data: int = ( - fb.value if isinstance((fb := fixed["bits"]), (ScaleType, ScaleObj)) else fb - ) - - # Logical and to get the fractional part; remaining is the integer part - fractional_part = data & (2**frac_bits - 1) - # Shift to get the integer part from the remaining bits - integer_part = data >> (total_bits - frac_bits) - - frac_float = fractional_part / (2**frac_bits) - - return integer_part + frac_float - - # lowercase is added for backwards compatibility to not break API units = UNITS = [ chr( diff --git a/tests/unit_tests/chain_data/test_coldkey_swap.py b/tests/unit_tests/chain_data/test_coldkey_swap.py index e13e245af6..157f23b799 100644 --- a/tests/unit_tests/chain_data/test_coldkey_swap.py +++ b/tests/unit_tests/chain_data/test_coldkey_swap.py @@ -1,4 +1,4 @@ -from async_substrate_interface.types import ScaleObj +from scalecodec.base import ScaleType from bittensor.core.chain_data.coldkey_swap import ( ColdkeySwapAnnouncementInfo, @@ -10,7 +10,7 @@ def test_coldkey_swap_announcement_info_from_query_none(mocker): """Test from_query returns None when query has no value.""" # Prep coldkey_ss58 = mocker.Mock(spec=str) - query = mocker.Mock(spec=ScaleObj) + query = mocker.Mock(spec=ScaleType) # Call from_query = ColdkeySwapAnnouncementInfo.from_query(coldkey_ss58, query) @@ -46,7 +46,7 @@ def test_coldkey_swap_announcement_info_from_query_happy_path(mocker): def test_coldkey_swap_dispute_info_from_query_none(mocker): """Test from_query returns None when query has no value.""" coldkey_ss58 = mocker.Mock(spec=str) - query = mocker.Mock(spec=ScaleObj) + query = mocker.Mock(spec=ScaleType) query.value = None from_query = ColdkeySwapDisputeInfo.from_query(coldkey_ss58, query) @@ -58,7 +58,7 @@ def test_coldkey_swap_dispute_info_from_query_happy_path(mocker): """Test from_query returns ColdkeySwapDisputeInfo when query has valid data.""" coldkey_ss58 = mocker.Mock(spec=str) fake_block = 12345 - query = mocker.Mock(spec=ScaleObj, value=fake_block) + query = mocker.Mock(spec=ScaleType, value=fake_block) from_query = ColdkeySwapDisputeInfo.from_query(coldkey_ss58, query) diff --git a/tests/unit_tests/test_async_subtensor.py b/tests/unit_tests/test_async_subtensor.py index 072be4b650..84127a7da9 100644 --- a/tests/unit_tests/test_async_subtensor.py +++ b/tests/unit_tests/test_async_subtensor.py @@ -2,9 +2,10 @@ import unittest.mock as mock import pytest -from async_substrate_interface.types import Runtime, ScaleObj +from async_substrate_interface.types import Runtime from bittensor_wallet import Wallet from scalecodec import GenericCall +from scalecodec.base import ScaleType from bittensor import u64_normalized_float from bittensor.core import async_subtensor, settings @@ -2829,7 +2830,9 @@ async def test_get_all_neuron_certificates(mocker, subtensor): @pytest.mark.asyncio async def test_get_timestamp(mocker, subtensor): fake_block = 1000 - mocked_query = mocker.AsyncMock(return_value=ScaleObj(1740586018 * 1000)) + mock_return = mocker.MagicMock(spec=ScaleType) + mock_return.value = 1740586018 * 1000 + mocked_query = mocker.AsyncMock(return_value=mock_return) mocker.patch.object(subtensor.substrate, "query", mocked_query) expected_result = datetime.datetime( 2025, 2, 26, 16, 6, 58, tzinfo=datetime.timezone.utc diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index 0a55814fd7..da34c5970e 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -6,9 +6,10 @@ import pytest import websockets from async_substrate_interface import sync_substrate -from async_substrate_interface.types import Runtime, ScaleObj +from async_substrate_interface.types import Runtime from bittensor_wallet import Wallet from scalecodec import GenericCall +from scalecodec.base import ScaleType from bittensor import StakeInfo from bittensor.core import settings @@ -3096,7 +3097,9 @@ def test_get_all_neuron_certificates(mocker, subtensor): def test_get_timestamp(mocker, subtensor): fake_block = 1000 - mocked_query = mocker.MagicMock(return_value=ScaleObj(1740586018 * 1000)) + mock_return = mocker.MagicMock(spec=ScaleType) + mock_return.value = 1740586018 * 1000 + mocked_query = mocker.MagicMock(return_value=mock_return) mocker.patch.object(subtensor.substrate, "query", mocked_query) expected_result = datetime.datetime( 2025, 2, 26, 16, 6, 58, tzinfo=datetime.timezone.utc From d72d6164434af0ef81fde6a2e6379da9162a372f Mon Sep 17 00:00:00 2001 From: BD Himes Date: Tue, 14 Apr 2026 14:18:32 +0200 Subject: [PATCH 047/118] Remove reuse block hash upstream logic --- bittensor/core/async_subtensor.py | 44 +------------ bittensor/core/chain_data/coldkey_swap.py | 2 +- bittensor/core/subtensor.py | 2 +- .../chain_data/test_coldkey_swap.py | 2 +- .../extrinsics/asyncex/test_registration.py | 2 +- .../extrinsics/asyncex/test_root.py | 2 - tests/unit_tests/test_async_subtensor.py | 62 +++---------------- tests/unit_tests/test_subtensor.py | 5 -- 8 files changed, 13 insertions(+), 108 deletions(-) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index d142a71473..541b197872 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -485,6 +485,8 @@ async def determine_block_hash( return block_hash if block is not None: return await self.get_block_hash(block) + if reuse_block: + return self.substrate.last_block_hash return None async def _runtime_method_exists( @@ -640,7 +642,6 @@ async def get_hyperparameter( storage_function=param_name, params=[netuid], block_hash=block_hash, - reuse_block_hash=reuse_block, ) return getattr(result, "value", result) @@ -778,7 +779,6 @@ async def query_constant( module_name=module_name, constant_name=constant_name, block_hash=block_hash, - reuse_block_hash=reuse_block, ) async def query_map( @@ -813,7 +813,6 @@ async def query_map( storage_function=name, params=params, block_hash=block_hash, - reuse_block_hash=reuse_block, ) return result @@ -847,7 +846,6 @@ async def query_map_subtensor( storage_function=name, params=params, block_hash=block_hash, - reuse_block_hash=reuse_block, ) async def query_module( @@ -883,7 +881,6 @@ async def query_module( storage_function=name, params=params, block_hash=block_hash, - reuse_block_hash=reuse_block, ) async def query_runtime_api( @@ -950,7 +947,6 @@ async def query_subtensor( storage_function=name, params=params, block_hash=block_hash, - reuse_block_hash=reuse_block, ) async def state_call( @@ -985,7 +981,6 @@ async def state_call( method="state_call", params=[method, data], block_hash=block_hash, - reuse_block_hash=reuse_block, ) # Common subtensor methods ========================================================================================= @@ -1178,7 +1173,6 @@ async def bonds( storage_function="Bonds", params=[storage_index], block_hash=block_hash, - reuse_block_hash=reuse_block, ) b_map = [] async for uid, b in b_map_encoded: @@ -1294,7 +1288,6 @@ async def does_hotkey_exist( storage_function="Owner", params=[hotkey_ss58], block_hash=block_hash, - reuse_block_hash=reuse_block, ) return_val = ( False @@ -1620,7 +1613,6 @@ async def get_all_subnets_netuid( module="SubtensorModule", storage_function="NetworksAdded", block_hash=block_hash, - reuse_block_hash=reuse_block, ) subnets = [] if result.records: @@ -1697,7 +1689,6 @@ async def get_balance( storage_function="Account", params=[address], block_hash=block_hash, - reuse_block_hash=reuse_block, ) return Balance(balance["data"]["free"]) @@ -1871,7 +1862,6 @@ async def get_children( storage_function="ChildKeys", params=[hotkey_ss58, netuid], block_hash=block_hash, - reuse_block_hash=reuse_block, ) if children: formatted_children = [] @@ -1927,7 +1917,6 @@ async def get_children_pending( block_hash, reuse_block, ), - reuse_block_hash=reuse_block, ) pending_value = getattr(response, "value", response) children, cooldown = cast( @@ -1980,7 +1969,6 @@ async def get_coldkey_swap_announcement( storage_function="ColdkeySwapAnnouncements", params=[coldkey_ss58], block_hash=block_hash, - reuse_block_hash=reuse_block, ) if query is None: return None @@ -2018,7 +2006,6 @@ async def get_coldkey_swap_announcements( module="SubtensorModule", storage_function="ColdkeySwapAnnouncements", block_hash=block_hash, - reuse_block_hash=reuse_block, ) return [ ColdkeySwapAnnouncementInfo.from_record(record) @@ -2054,7 +2041,6 @@ async def get_coldkey_swap_announcement_delay( module="SubtensorModule", storage_function="ColdkeySwapAnnouncementDelay", block_hash=block_hash, - reuse_block_hash=reuse_block, ) value = getattr(query, "value", query) return cast(int, value) if value is not None else 0 @@ -2088,7 +2074,6 @@ async def get_coldkey_swap_reannouncement_delay( module="SubtensorModule", storage_function="ColdkeySwapReannouncementDelay", block_hash=block_hash, - reuse_block_hash=reuse_block, ) value = getattr(query, "value", query) return cast(int, value) if value is not None else 0 @@ -2125,7 +2110,6 @@ async def get_coldkey_swap_dispute( storage_function="ColdkeySwapDisputes", params=[coldkey_ss58], block_hash=block_hash, - reuse_block_hash=reuse_block, ) if query is None: return None @@ -2161,7 +2145,6 @@ async def get_coldkey_swap_disputes( module="SubtensorModule", storage_function="ColdkeySwapDisputes", block_hash=block_hash, - reuse_block_hash=reuse_block, ) return [ ColdkeySwapDisputeInfo.from_record(record) async for record in query_map @@ -2303,7 +2286,6 @@ async def get_commitment_metadata( storage_function="CommitmentOf", params=[netuid, hotkey_ss58], block_hash=block_hash, - reuse_block_hash=reuse_block, ) if commit_data is None: return "" @@ -2597,7 +2579,6 @@ async def get_delegate_identities( module="SubtensorModule", storage_function="IdentitiesV2", block_hash=block_hash, - reuse_block_hash=reuse_block, ) return { @@ -2749,7 +2730,6 @@ async def get_existential_deposit( module_name="Balances", constant_name="ExistentialDeposit", block_hash=block_hash, - reuse_block_hash=reuse_block, ) if result is None: @@ -2833,7 +2813,6 @@ async def get_hotkey_owner( storage_function="Owner", params=[hotkey_ss58], block_hash=block_hash, - reuse_block_hash=reuse_block, ) exists = False if hk_owner_query: @@ -2871,7 +2850,6 @@ async def get_last_bonds_reset( storage_function="LastBondsReset", params=[netuid, hotkey_ss58], block_hash=block_hash, - reuse_block_hash=reuse_block, ) return block @@ -3407,7 +3385,6 @@ async def get_netuids_for_hotkey( storage_function="IsNetworkMember", params=[hotkey_ss58], block_hash=block_hash, - reuse_block_hash=reuse_block, ) netuids = [] if result.records: @@ -3493,7 +3470,6 @@ async def get_neuron_for_pubkey_and_subnet( storage_function="Uids", params=[netuid, hotkey_ss58], block_hash=block_hash, - reuse_block_hash=reuse_block, ) if (uid := getattr(uid_query, "value", None)) is None: return NeuronInfo.get_null_neuron() @@ -3571,7 +3547,6 @@ async def get_owned_hotkeys( storage_function="OwnedHotkeys", params=[coldkey_ss58], block_hash=block_hash, - reuse_block_hash=reuse_block, ) return [decode_account_id(hotkey[0]) for hotkey in owned_hotkeys or []] @@ -3608,7 +3583,6 @@ async def get_parents( storage_function="ParentKeys", params=[hotkey_ss58, netuid], block_hash=block_hash, - reuse_block_hash=reuse_block, ) if parents: formatted_parents = [] @@ -3652,7 +3626,6 @@ async def get_proxies( module="Proxy", storage_function="Proxies", block_hash=block_hash, - reuse_block_hash=reuse_block, ) proxies = {} @@ -3697,7 +3670,6 @@ async def get_proxies_for_real_account( storage_function="Proxies", params=[real_account_ss58], block_hash=block_hash, - reuse_block_hash=reuse_block, ) return ProxyInfo.from_query(query) @@ -3735,7 +3707,6 @@ async def get_proxy_announcement( storage_function="Announcements", params=[delegate_account_ss58], block_hash=block_hash, - reuse_block_hash=reuse_block, ) query_value = getattr(query, "value", query) return ProxyAnnouncementInfo.from_dict(cast(list[Any], query_value)[0]) @@ -3771,7 +3742,6 @@ async def get_proxy_announcements( module="Proxy", storage_function="Announcements", block_hash=block_hash, - reuse_block_hash=reuse_block, ) announcements = {} async for record in query_map: @@ -3952,7 +3922,6 @@ async def get_root_claim_type( storage_function="RootClaimType", params=[coldkey_ss58], block_hash=block_hash, - reuse_block_hash=reuse_block, ) query_value = getattr(query, "value", query) claim_type = cast(dict[str, Any], query_value) @@ -4002,7 +3971,6 @@ async def get_root_alpha_dividends_per_subnet( storage_function="RootAlphaDividendsPerSubnet", params=[netuid, hotkey_ss58], block_hash=block_hash, - reuse_block_hash=reuse_block, ) value = getattr(query, "value", query) return Balance.from_rao(cast(int, value)).set_unit(netuid=netuid) @@ -4071,7 +4039,6 @@ async def get_root_claimable_all_rates( storage_function="RootClaimable", params=[hotkey_ss58], block_hash=block_hash, - reuse_block_hash=reuse_block, ) query_value = getattr(query, "value", query) bits_list = next(iter(cast(list[list[tuple[int, FixedPoint]]], query_value))) @@ -4171,7 +4138,6 @@ async def get_root_claimed( storage_function="RootClaimed", params=[netuid, hotkey_ss58, coldkey_ss58], block_hash=block_hash, - reuse_block_hash=reuse_block, ) value = getattr(query, "value", query) return Balance.from_rao(cast(int, value)).set_unit(netuid=netuid) @@ -4894,7 +4860,6 @@ async def get_total_subnets( storage_function="TotalNetworks", params=[], block_hash=block_hash, - reuse_block_hash=reuse_block, ) return getattr(result, "value", None) @@ -5013,7 +4978,6 @@ async def get_vote_data( storage_function="Voting", params=[proposal_hash], block_hash=block_hash, - reuse_block_hash=reuse_block, ), ) @@ -5052,7 +5016,6 @@ async def get_uid_for_hotkey_on_subnet( storage_function="Uids", params=[netuid, hotkey_ss58], block_hash=block_hash, - reuse_block_hash=reuse_block, ) return cast(Optional[int], getattr(result, "value", result)) @@ -5657,7 +5620,6 @@ async def query_identity( storage_function="IdentitiesV2", params=[coldkey_ss58], block_hash=block_hash, - reuse_block_hash=reuse_block, ) if not identity_info: @@ -5778,7 +5740,6 @@ async def subnet_exists( storage_function="NetworksAdded", params=[netuid], block_hash=block_hash, - reuse_block_hash=reuse_block, ) return getattr(result, "value", False) @@ -5945,7 +5906,6 @@ async def weights( storage_function="Weights", params=[storage_index], block_hash=block_hash, - reuse_block_hash=reuse_block, ) w_map = [] async for uid, w in w_map_encoded: diff --git a/bittensor/core/chain_data/coldkey_swap.py b/bittensor/core/chain_data/coldkey_swap.py index c323a84a55..5a7d5f9e45 100644 --- a/bittensor/core/chain_data/coldkey_swap.py +++ b/bittensor/core/chain_data/coldkey_swap.py @@ -32,7 +32,7 @@ class ColdkeySwapAnnouncementInfo: @classmethod def from_query( - cls, coldkey_ss58: str, query: ScaleType + cls, coldkey_ss58: str, query: Optional[ScaleType] ) -> Optional["ColdkeySwapAnnouncementInfo"]: """ Creates a ColdkeySwapAnnouncementInfo object from a Substrate query result. diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 73c2299835..68265eccaf 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -3244,7 +3244,7 @@ def get_root_claim_type( Parameters: coldkey_ss58: The SS58 address of the coldkey whose root claim preference to query. - block: The block number to query. Do not specify if using `block_hash` or `reuse_block`. + block: The block number to query. Returns: diff --git a/tests/unit_tests/chain_data/test_coldkey_swap.py b/tests/unit_tests/chain_data/test_coldkey_swap.py index 157f23b799..db870030b5 100644 --- a/tests/unit_tests/chain_data/test_coldkey_swap.py +++ b/tests/unit_tests/chain_data/test_coldkey_swap.py @@ -10,7 +10,7 @@ def test_coldkey_swap_announcement_info_from_query_none(mocker): """Test from_query returns None when query has no value.""" # Prep coldkey_ss58 = mocker.Mock(spec=str) - query = mocker.Mock(spec=ScaleType) + query = None # Call from_query = ColdkeySwapAnnouncementInfo.from_query(coldkey_ss58, query) diff --git a/tests/unit_tests/extrinsics/asyncex/test_registration.py b/tests/unit_tests/extrinsics/asyncex/test_registration.py index 278d20c2c8..5e853e24af 100644 --- a/tests/unit_tests/extrinsics/asyncex/test_registration.py +++ b/tests/unit_tests/extrinsics/asyncex/test_registration.py @@ -232,7 +232,7 @@ async def test_register_extrinsic_already_registered(subtensor, fake_wallet, moc block_hash=subtensor.substrate.get_chain_head.return_value, ) assert success is True - assert message == f"Already registered." + assert message == "Already registered." @pytest.mark.asyncio diff --git a/tests/unit_tests/extrinsics/asyncex/test_root.py b/tests/unit_tests/extrinsics/asyncex/test_root.py index 87b95f0776..42b73ce266 100644 --- a/tests/unit_tests/extrinsics/asyncex/test_root.py +++ b/tests/unit_tests/extrinsics/asyncex/test_root.py @@ -95,7 +95,6 @@ async def test_root_register_extrinsic_success(subtensor, fake_wallet, mocker): storage_function="Uids", params=[0, "fake_hotkey_address"], block_hash=None, - reuse_block_hash=False, ) assert result.success is True assert result.message == "Success" @@ -329,7 +328,6 @@ async def test_root_register_extrinsic_uid_not_found(subtensor, fake_wallet, moc storage_function="Uids", params=[0, "fake_hotkey_address"], block_hash=None, - reuse_block_hash=False, ) assert result.success is False diff --git a/tests/unit_tests/test_async_subtensor.py b/tests/unit_tests/test_async_subtensor.py index 84127a7da9..f4ae3b17af 100644 --- a/tests/unit_tests/test_async_subtensor.py +++ b/tests/unit_tests/test_async_subtensor.py @@ -336,7 +336,6 @@ async def test_get_total_subnets(subtensor, mocker): storage_function="TotalNetworks", params=[], block_hash=fake_block_hash, - reuse_block_hash=False, ) @@ -369,7 +368,6 @@ async def test_get_subnets(subtensor, mocker, records, response): module="SubtensorModule", storage_function="NetworksAdded", block_hash=fake_block_hash, - reuse_block_hash=False, ) assert result == response @@ -398,7 +396,9 @@ async def test_is_hotkey_delegate(subtensor, mocker, hotkey_ss58_in_result): # Asserts assert result == hotkey_ss58_in_result - mocked_get_delegates.assert_called_once_with(block_hash=None, reuse_block=True) + mocked_get_delegates.assert_called_once_with( + block_hash=subtensor.substrate.last_block_hash, reuse_block=True + ) @pytest.mark.parametrize( @@ -611,7 +611,6 @@ async def test_get_balance(subtensor, mocker): storage_function="Account", params=[fake_address], block_hash=mocked_determine_block_hash.return_value, - reuse_block_hash=reuse_block, ) mocked_balance.assert_called_once_with( subtensor.substrate.query.return_value.__getitem__.return_value.__getitem__.return_value @@ -691,11 +690,11 @@ async def test_get_netuids_for_hotkey_with_records(subtensor, mocker): subtensor.substrate.query_map = mocked_substrate_query_map fake_hotkey_ss58 = "hotkey_58" - fake_block_hash = None + fake_block_hash = subtensor.substrate.last_block_hash # Call result = await subtensor.get_netuids_for_hotkey( - hotkey_ss58=fake_hotkey_ss58, block_hash=fake_block_hash, reuse_block=True + hotkey_ss58=fake_hotkey_ss58, reuse_block=True ) # Assertions @@ -704,7 +703,6 @@ async def test_get_netuids_for_hotkey_with_records(subtensor, mocker): storage_function="IsNetworkMember", params=[fake_hotkey_ss58], block_hash=fake_block_hash, - reuse_block_hash=True, ) assert result == expected_response @@ -726,11 +724,11 @@ async def test_get_netuids_for_hotkey_without_records(subtensor, mocker): subtensor.substrate.query_map = mocked_substrate_query_map fake_hotkey_ss58 = "hotkey_58" - fake_block_hash = None + fake_block_hash = subtensor.substrate.last_block_hash # Call result = await subtensor.get_netuids_for_hotkey( - hotkey_ss58=fake_hotkey_ss58, block_hash=fake_block_hash, reuse_block=True + hotkey_ss58=fake_hotkey_ss58, block_hash=None, reuse_block=True ) # Assertions @@ -739,7 +737,6 @@ async def test_get_netuids_for_hotkey_without_records(subtensor, mocker): storage_function="IsNetworkMember", params=[fake_hotkey_ss58], block_hash=fake_block_hash, - reuse_block_hash=True, ) assert result == expected_response @@ -770,7 +767,6 @@ async def test_subnet_exists(subtensor, mocker): storage_function="NetworksAdded", params=[fake_netuid], block_hash=fake_block_hash, - reuse_block_hash=fake_reuse_block_hash, ) assert result == mocked_substrate_query.return_value.value @@ -808,7 +804,6 @@ async def test_get_hyperparameter_happy_path(subtensor, mocker): storage_function=fake_param_name, params=[fake_netuid], block_hash=fake_block_hash, - reuse_block_hash=fake_reuse_block_hash, ) assert result == mocked_substrate_query.return_value.value @@ -913,7 +908,6 @@ async def test_get_existential_deposit_happy_path(subtensor, mocker): module_name="Balances", constant_name="ExistentialDeposit", block_hash=fake_block_hash, - reuse_block_hash=fake_reuse_block_hash, ) spy_balance_from_rao.assert_called_once_with( mocked_substrate_get_constant.return_value.value @@ -947,7 +941,6 @@ async def test_get_existential_deposit_raise_exception(subtensor, mocker): module_name="Balances", constant_name="ExistentialDeposit", block_hash=fake_block_hash, - reuse_block_hash=fake_reuse_block_hash, ) spy_balance_from_rao.assert_not_called() @@ -1065,7 +1058,6 @@ async def test_get_neuron_for_pubkey_and_subnet_success(subtensor, mocker): storage_function="Uids", params=[fake_netuid, fake_hotkey], block_hash=None, - reuse_block_hash=False, ) subtensor.substrate.runtime_call.assert_awaited_once() subtensor.substrate.runtime_call.assert_called_once_with( @@ -1105,7 +1097,6 @@ async def test_get_neuron_for_pubkey_and_subnet_uid_not_found(subtensor, mocker) storage_function="Uids", params=[fake_netuid, fake_hotkey], block_hash=None, - reuse_block_hash=False, ) mocked_get_null_neuron.assert_called_once() assert result == "null_neuron" @@ -1144,7 +1135,6 @@ async def test_get_neuron_for_pubkey_and_subnet_rpc_result_empty(subtensor, mock storage_function="Uids", params=[fake_netuid, fake_hotkey], block_hash=None, - reuse_block_hash=False, ) subtensor.substrate.runtime_call.assert_called_once_with( "NeuronInfoRuntimeApi", @@ -1389,7 +1379,6 @@ async def test_query_identity_successful(subtensor, mocker): storage_function="IdentitiesV2", params=[fake_coldkey_ss58], block_hash=fake_block_hash, - reuse_block_hash=False, ) assert result == ChainIdentity( additional="Additional", @@ -1420,7 +1409,6 @@ async def test_query_identity_no_info(subtensor, mocker): storage_function="IdentitiesV2", params=[fake_coldkey_ss58], block_hash=None, - reuse_block_hash=False, ) assert result is None @@ -1450,7 +1438,6 @@ async def test_query_identity_type_error(subtensor, mocker): storage_function="IdentitiesV2", params=[fake_coldkey_ss58], block_hash=None, - reuse_block_hash=False, ) assert result is None @@ -1481,7 +1468,6 @@ async def mock_query_map(**_): storage_function="Weights", params=[fake_netuid], block_hash=fake_block_hash, - reuse_block_hash=False, ) assert result == [(0, [(1, 10), (2, 20)]), (1, [(0, 15), (2, 25)])] @@ -1512,7 +1498,6 @@ async def mock_query_map(**_): storage_function="Bonds", params=[fake_netuid], block_hash=fake_block_hash, - reuse_block_hash=False, ) assert result == [(0, [(1, 100), (2, 200)]), (1, [(0, 150), (2, 250)])] @@ -1539,7 +1524,6 @@ async def test_does_hotkey_exist_true(subtensor, mocker): storage_function="Owner", params=[fake_hotkey_ss58], block_hash=fake_block_hash, - reuse_block_hash=False, ) assert result is True @@ -1564,7 +1548,6 @@ async def test_does_hotkey_exist_false_for_specific_account(subtensor, mocker): storage_function="Owner", params=[fake_hotkey_ss58], block_hash=None, - reuse_block_hash=False, ) assert result is False @@ -1593,7 +1576,6 @@ async def test_get_hotkey_owner_successful(subtensor, mocker): storage_function="Owner", params=[fake_hotkey_ss58], block_hash=fake_block_hash, - reuse_block_hash=False, ) mocked_does_hotkey_exist.assert_awaited_once_with( fake_hotkey_ss58, block_hash=fake_block_hash @@ -1622,7 +1604,6 @@ async def test_get_hotkey_owner_non_existent_hotkey(subtensor, mocker): storage_function="Owner", params=[fake_hotkey_ss58], block_hash=fake_block_hash, - reuse_block_hash=False, ) assert result is None @@ -1899,7 +1880,6 @@ async def test_get_children_success(subtensor, mocker): module="SubtensorModule", storage_function="ChildKeys", params=[fake_hotkey, fake_netuid], - reuse_block_hash=False, ) mocked_decode_account_id.assert_has_calls( [mocker.call("child_key_1"), mocker.call("child_key_2")] @@ -1927,7 +1907,6 @@ async def test_get_children_no_children(subtensor, mocker): module="SubtensorModule", storage_function="ChildKeys", params=[fake_hotkey, fake_netuid], - reuse_block_hash=False, ) assert result == (True, [], "") @@ -1957,7 +1936,6 @@ async def test_get_children_substrate_request_exception(subtensor, mocker): module="SubtensorModule", storage_function="ChildKeys", params=[fake_hotkey, fake_netuid], - reuse_block_hash=False, ) mocked_format_error_message.assert_called_once_with(fake_exception) assert result == (False, [], "Formatted error message") @@ -1998,7 +1976,6 @@ async def test_get_parents_success(subtensor, mocker): module="SubtensorModule", storage_function="ParentKeys", params=[fake_hotkey, fake_netuid], - reuse_block_hash=False, ) mocked_decode_account_id.assert_has_calls( [mocker.call("parent_key_1"), mocker.call("parent_key_2")] @@ -2026,7 +2003,6 @@ async def test_get_parents_no_parents(subtensor, mocker): module="SubtensorModule", storage_function="ParentKeys", params=[fake_hotkey, fake_netuid], - reuse_block_hash=False, ) assert result == [] @@ -2077,7 +2053,6 @@ async def test_get_children_pending(mock_substrate, subtensor): storage_function="PendingChildKeys", params=[1, "hotkey_ss58"], block_hash=None, - reuse_block_hash=False, ) @@ -2198,7 +2173,6 @@ async def test_get_vote_data_success(subtensor, mocker): storage_function="Voting", params=[fake_proposal_hash], block_hash=fake_block_hash, - reuse_block_hash=False, ) assert result == mocked_proposal_vote_data @@ -2224,7 +2198,6 @@ async def test_get_vote_data_no_data(subtensor, mocker): storage_function="Voting", params=[fake_proposal_hash], block_hash=fake_block_hash, - reuse_block_hash=False, ) assert result is None @@ -2286,7 +2259,6 @@ async def test_get_delegate_identities(subtensor, mocker): module="SubtensorModule", storage_function="IdentitiesV2", block_hash=fake_block_hash, - reuse_block_hash=False, ) assert result["delegate1_ss58"].name == "Chain Delegate 1" @@ -2314,7 +2286,6 @@ async def test_is_hotkey_registered_true(subtensor, mocker): storage_function="Uids", params=[fake_netuid, fake_hotkey_ss58], block_hash=None, - reuse_block_hash=False, ) assert result is True @@ -2341,7 +2312,6 @@ async def test_is_hotkey_registered_false(subtensor, mocker): storage_function="Uids", params=[fake_netuid, fake_hotkey_ss58], block_hash=None, - reuse_block_hash=False, ) assert result is False @@ -2369,7 +2339,6 @@ async def test_get_uid_for_hotkey_on_subnet_registered(subtensor, mocker): storage_function="Uids", params=[fake_netuid, fake_hotkey_ss58], block_hash=fake_block_hash, - reuse_block_hash=False, ) assert result == fake_uid @@ -2397,7 +2366,6 @@ async def test_get_uid_for_hotkey_on_subnet_not_registered(subtensor, mocker): storage_function="Uids", params=[fake_netuid, fake_hotkey_ss58], block_hash=fake_block_hash, - reuse_block_hash=False, ) assert result is None @@ -2823,7 +2791,6 @@ async def test_get_all_neuron_certificates(mocker, subtensor): storage_function="NeuronCertificates", params=[fake_netuid], block_hash=None, - reuse_block_hash=False, ) @@ -2869,7 +2836,6 @@ async def test_get_owned_hotkeys_happy_path(subtensor, mocker): storage_function="OwnedHotkeys", params=[fake_coldkey], block_hash=None, - reuse_block_hash=False, ) assert result == [mocked_decode_account_id.return_value] mocked_decode_account_id.assert_called_once_with(fake_hotkey) @@ -2892,7 +2858,6 @@ async def test_get_owned_hotkeys_return_empty(subtensor, mocker): storage_function="OwnedHotkeys", params=[fake_coldkey], block_hash=None, - reuse_block_hash=False, ) assert result == [] @@ -4893,7 +4858,6 @@ async def test_get_root_claim_type(mocker, subtensor, fake_result, expected_resu storage_function="RootClaimType", params=[fake_coldkey_ss58], block_hash=mocked_determine_block_hash.return_value, - reuse_block_hash=False, ) assert result == expected_result @@ -4949,7 +4913,6 @@ async def test_get_root_claimable_all_rates(mocker, subtensor): storage_function="RootClaimable", params=[hotkey_ss58], block_hash=mocked_determine_block_hash.return_value, - reuse_block_hash=False, ) mocked_fixed_to_float.assert_called_once_with({"bits": 6520190}, frac_bits=32) assert result == {14: mocked_fixed_to_float.return_value} @@ -5033,7 +4996,6 @@ async def test_get_root_claimed(mocker, subtensor): storage_function="RootClaimed", params=[netuid, hotkey_ss58, coldkey_ss58], block_hash=mocked_determine_block_hash.return_value, - reuse_block_hash=False, ) assert result == Balance.from_rao(1).set_unit(netuid) @@ -5211,7 +5173,6 @@ async def test_get_proxies(subtensor, mocker): module="Proxy", storage_function="Proxies", block_hash=mocked_determine_block_hash.return_value, - reuse_block_hash=False, ) mocked_from_query_map_record.assert_called_once_with(fake_record) assert result == {fake_real_account: [fake_proxy_list]} @@ -5245,7 +5206,6 @@ async def test_get_proxies_for_real_account(subtensor, mocker): storage_function="Proxies", params=[fake_real_account_ss58], block_hash=mocked_determine_block_hash.return_value, - reuse_block_hash=False, ) mocked_from_query.assert_called_once_with(mocked_query.return_value) assert result == mocked_from_query.return_value @@ -5278,7 +5238,6 @@ async def test_get_proxy_announcement(subtensor, mocker): storage_function="Announcements", params=[fake_delegate_account_ss58], block_hash=mocked_determine_block_hash.return_value, - reuse_block_hash=False, ) mocked_from_dict.assert_called_once_with(mocked_query.return_value.value[0]) assert result == mocked_from_dict.return_value @@ -5319,7 +5278,6 @@ async def test_get_proxy_announcements(subtensor, mocker): module="Proxy", storage_function="Announcements", block_hash=mocked_determine_block_hash.return_value, - reuse_block_hash=False, ) mocked_from_query_map_record.assert_called_once_with(fake_record) assert result == {fake_delegate: fake_proxies_list} @@ -6228,7 +6186,6 @@ async def test_get_coldkey_swap_announcement(subtensor, mocker): storage_function="ColdkeySwapAnnouncements", params=[fake_coldkey_ss58], block_hash=mocked_determine_block_hash.return_value, - reuse_block_hash=False, ) mocked_from_query.assert_called_once_with( coldkey_ss58=fake_coldkey_ss58, query=mocked_query.return_value @@ -6260,7 +6217,6 @@ async def test_get_coldkey_swap_announcements(subtensor, mocker): module="SubtensorModule", storage_function="ColdkeySwapAnnouncements", block_hash=mocked_determine_block_hash.return_value, - reuse_block_hash=False, ) mocked_from_record.assert_called_once_with(fake_record) assert result == [mocked_from_record.return_value] @@ -6282,7 +6238,6 @@ async def test_get_coldkey_swap_announcement_delay(subtensor, mocker): module="SubtensorModule", storage_function="ColdkeySwapAnnouncementDelay", block_hash=mocked_determine_block_hash.return_value, - reuse_block_hash=False, ) assert result == mocked_query.return_value.value @@ -6303,7 +6258,6 @@ async def test_get_coldkey_swap_reannouncement_delay(subtensor, mocker): module="SubtensorModule", storage_function="ColdkeySwapReannouncementDelay", block_hash=mocked_determine_block_hash.return_value, - reuse_block_hash=False, ) assert result == mocked_query.return_value.value @@ -6428,7 +6382,6 @@ async def test_get_coldkey_swap_dispute(subtensor, mocker): storage_function="ColdkeySwapDisputes", params=[fake_coldkey_ss58], block_hash=mocked_determine_block_hash.return_value, - reuse_block_hash=False, ) mocked_from_query.assert_called_once_with( coldkey_ss58=fake_coldkey_ss58, query=mocked_query.return_value @@ -6460,7 +6413,6 @@ async def test_get_coldkey_swap_disputes(subtensor, mocker): module="SubtensorModule", storage_function="ColdkeySwapDisputes", block_hash=mocked_determine_block_hash.return_value, - reuse_block_hash=False, ) mocked_from_record.assert_called_once_with(fake_record) assert result == [mocked_from_record.return_value] diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index da34c5970e..3e63a7be24 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -1664,7 +1664,6 @@ def test_get_last_commitment_bonds_reset_block(subtensor, mocker): fake_hotkey = "hotkey" mocked_get_last_bonds_reset = mocker.patch.object(subtensor, "get_last_bonds_reset") - mocked_decode_block = mocker.patch.object(subtensor_module, "decode_block") mocked_metagraph = mocker.MagicMock() subtensor.metagraph = mocked_metagraph @@ -1678,10 +1677,6 @@ def test_get_last_commitment_bonds_reset_block(subtensor, mocker): # Assertions mocked_metagraph.assert_called_once_with(fake_netuid, block=None) mocked_get_last_bonds_reset.assert_called_once_with(fake_netuid, fake_hotkey, None) - mocked_decode_block.assert_called_once_with( - mocked_get_last_bonds_reset.return_value - ) - assert result == mocked_decode_block.return_value def test_min_allowed_weights(subtensor, mocker): From f9f84ed008ee84c0c4571404600b5748990a4f79 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Tue, 14 Apr 2026 15:50:00 +0200 Subject: [PATCH 048/118] Checkin --- bittensor/core/async_subtensor.py | 64 ++++++------- bittensor/core/chain_data/coldkey_swap.py | 9 +- bittensor/core/chain_data/proxy.py | 31 ++++--- bittensor/core/chain_data/utils.py | 20 +++-- .../core/chain_data/weight_commit_info.py | 16 ++-- bittensor/core/subtensor.py | 22 ++--- bittensor/utils/__init__.py | 44 ++++++--- tests/unit_tests/chain_data/test_utils.py | 89 ++----------------- 8 files changed, 114 insertions(+), 181 deletions(-) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index 541b197872..7c3525011b 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -38,7 +38,6 @@ SubnetIdentity, SubnetInfo, WeightCommitInfo, - decode_account_id, ) from bittensor.core.chain_data.chain_identity import ChainIdentity from bittensor.core.chain_data.delegate_info import DelegatedInfo @@ -891,7 +890,7 @@ async def query_runtime_api( block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> Optional[Any]: + ) -> Any: """Queries the runtime API of the Bittensor blockchain, providing a way to interact with the underlying runtime and retrieve data encoded in Scale Bytes format. Use this function for nonstandard queries to the runtime environment, if these cannot be accessed through other, standard getter methods. @@ -915,7 +914,7 @@ async def query_runtime_api( result = await self.substrate.runtime_call( runtime_api, method, params, block_hash ) - return result.value + return result async def query_subtensor( self, @@ -1174,12 +1173,12 @@ async def bonds( params=[storage_index], block_hash=block_hash, ) - b_map = [] - async for uid, b in b_map_encoded: - if b.value is not None: - b_map.append((uid, b.value)) + bond_map = [] + async for uid, bond in b_map_encoded: + if bond is not None: + bond_map.append((uid, bond)) - return b_map + return bond_map async def commit_reveal_enabled( self, @@ -1411,7 +1410,7 @@ async def get_all_commitments( result = {} async for id_, value in query: try: - result[decode_account_id(id_[0])] = decode_metadata(value) + result[id_] = decode_metadata(value) except Exception as error: logging.error( f"Error decoding [red]{id_}[/red] and [red]{value}[/red]: {error}" @@ -1532,7 +1531,7 @@ async def get_all_neuron_certificates( ) output = {} async for key, item in query_certificates: - output[decode_account_id(key)] = Certificate(item.value) + output[key] = Certificate(item) return output async def get_all_revealed_commitments( @@ -1654,8 +1653,7 @@ async def get_auto_stakes( ) pairs = {} - async for netuid, destination in query: - hotkey_ss58 = decode_account_id(destination.value[0]) + async for netuid, hotkey_ss58 in query: if hotkey_ss58: pairs[int(netuid)] = hotkey_ss58 @@ -1865,9 +1863,8 @@ async def get_children( ) if children: formatted_children = [] - for proportion, child in children.value: + for proportion, formatted_child in children.value: # Convert U64 to int - formatted_child = decode_account_id(child[0]) normalized_proportion = u64_normalized_float(proportion) formatted_children.append((normalized_proportion, formatted_child)) return True, formatted_children, "" @@ -1928,7 +1925,7 @@ async def get_children_pending( [ ( u64_normalized_float(proportion), - decode_account_id(child[0]), + child, ) for proportion, child in children ], @@ -2384,10 +2381,8 @@ async def get_crowdloan_contributions( if query.records: async for record in query: - if record[1].value: - result[decode_account_id(record[0])] = Balance.from_rao( - record[1].value - ) + if record[1]: + result[record[0]] = Balance.from_rao(record[1]) return result @@ -2498,7 +2493,7 @@ async def get_crowdloans( if query.records: async for c_id, value_obj in query: - data = value_obj.value + data = value_obj if not data: continue crowdloans.append( @@ -2582,8 +2577,8 @@ async def get_delegate_identities( ) return { - decode_account_id(ss58_address[0]): ChainIdentity.from_dict( - decode_hex_identity_dict(identity.value), + ss58_address: ChainIdentity.from_dict( + decode_hex_identity_dict(identity), ) async for ss58_address, identity in identities } @@ -2980,9 +2975,7 @@ async def get_liquidity_list( # Fetch positions positions_values: list[tuple[dict, int, int]] = [] positions_storage_keys: list[StorageKey] = [] - async for _, p in positions_response: - position = p.value - + async for _, position in positions_response: tick_low_idx = position.get("tick_low")[0] tick_high_idx = position.get("tick_high")[0] positions_values.append((position, tick_low_idx, tick_high_idx)) @@ -3388,9 +3381,9 @@ async def get_netuids_for_hotkey( ) netuids = [] if result.records: - async for record in result: - if record[1].value: - netuids.append(record[0]) + async for netuid, is_member in result: + if is_member: + netuids.append(netuid) return netuids async def get_neuron_certificate( @@ -3549,7 +3542,7 @@ async def get_owned_hotkeys( block_hash=block_hash, ) - return [decode_account_id(hotkey[0]) for hotkey in owned_hotkeys or []] + return owned_hotkeys or [] async def get_parents( self, @@ -3586,9 +3579,8 @@ async def get_parents( ) if parents: formatted_parents = [] - for proportion, parent in parents.value: + for proportion, formatted_child in parents.value: # Convert U64 to int - formatted_child = decode_account_id(parent[0]) normalized_proportion = u64_normalized_float(proportion) formatted_parents.append((normalized_proportion, formatted_child)) return formatted_parents @@ -4360,10 +4352,7 @@ async def get_stake_info_for_coldkeys( if query is None: return {} - return { - decode_account_id(ck): StakeInfo.list_from_dicts(st_info) - for ck, st_info in query - } + return {ck: StakeInfo.list_from_dicts(st_info) for ck, st_info in query} async def get_stake_for_hotkey( self, @@ -4456,7 +4445,7 @@ async def get_staking_hotkeys( params=[coldkey_ss58], block_hash=block_hash, ) - return [decode_account_id(hotkey[0]) for hotkey in result or []] + return result or [] async def get_start_call_delay( self, @@ -4691,6 +4680,7 @@ async def get_subnet_prices( prices = {} async for id_, current_sqrt_price in current_sqrt_prices: + # TODO investigate if we need to use fixed_to_decimal here instead current_sqrt_price = fixed_to_float(current_sqrt_price) current_price = current_sqrt_price * current_sqrt_price current_price_in_tao = Balance.from_rao(int(current_price * 1e9)) @@ -5909,7 +5899,7 @@ async def weights( ) w_map = [] async for uid, w in w_map_encoded: - w_map.append((uid, w.value)) + w_map.append((uid, w)) return w_map diff --git a/bittensor/core/chain_data/coldkey_swap.py b/bittensor/core/chain_data/coldkey_swap.py index 5a7d5f9e45..fb64df43c5 100644 --- a/bittensor/core/chain_data/coldkey_swap.py +++ b/bittensor/core/chain_data/coldkey_swap.py @@ -113,7 +113,7 @@ def from_query( return cls(coldkey=coldkey_ss58, disputed_block=int(query.value)) @classmethod - def from_record(cls, record: tuple) -> "ColdkeySwapDisputeInfo": + def from_record(cls, record: tuple[str, int]) -> "ColdkeySwapDisputeInfo": """ Creates a ColdkeySwapDisputeInfo object from a query_map record. @@ -124,12 +124,7 @@ def from_record(cls, record: tuple) -> "ColdkeySwapDisputeInfo": Returns: ColdkeySwapDisputeInfo object with dispute details for the coldkey from the record. """ - coldkey_ss58 = decode_account_id(record[0]) - val = record[1] - disputed_block = ( - int(val.value) if getattr(val, "value", None) is not None else int(val) - ) - return cls(coldkey=coldkey_ss58, disputed_block=disputed_block) + return cls(coldkey=record[0], disputed_block=int(record[1])) @dataclass diff --git a/bittensor/core/chain_data/proxy.py b/bittensor/core/chain_data/proxy.py index 8e663d26b3..465e6b9dfd 100644 --- a/bittensor/core/chain_data/proxy.py +++ b/bittensor/core/chain_data/proxy.py @@ -1,8 +1,7 @@ from dataclasses import dataclass from enum import Enum -from typing import Any, Optional, Union +from typing import Any, Optional, Union, Sequence -from bittensor.core.chain_data.utils import decode_account_id from bittensor.utils.balance import Balance @@ -193,7 +192,7 @@ class ProxyInfo: delay: int @classmethod - def from_tuple(cls, data: tuple) -> list["ProxyInfo"]: + def from_tuple(cls, data: Sequence[dict[str, str | int]]) -> list["ProxyInfo"]: """Creates a list of ProxyInfo objects from chain proxy data. This method decodes the raw proxy data returned from the Proxy.Proxies storage function and creates @@ -210,8 +209,8 @@ def from_tuple(cls, data: tuple) -> list["ProxyInfo"]: """ return [ cls( - delegate=decode_account_id(proxy["delegate"]), - proxy_type=next(iter(proxy["proxy_type"].keys())), + delegate=proxy["delegate"], + proxy_type=proxy["proxy_type"], delay=proxy["delay"], ) for proxy in data @@ -245,7 +244,9 @@ def from_query(cls, query: Any) -> tuple[list["ProxyInfo"], Balance]: return cls.from_tuple(proxies), Balance.from_rao(balance) @classmethod - def from_query_map_record(cls, record: list) -> tuple[str, list["ProxyInfo"]]: + def from_query_map_record( + cls, record: tuple[str, tuple[list[dict[str, str | int]], int]] + ) -> tuple[str, list["ProxyInfo"]]: """Creates a dictionary mapping delegate addresses to their ProxyInfo lists from a query_map record. Processes a single record from a query_map call to the Proxy.Proxies storage function. Each record represents @@ -262,9 +263,9 @@ def from_query_map_record(cls, record: list) -> tuple[str, list["ProxyInfo"]]: """ # record[0] is the real account (key from storage) # record[1] is the value containing proxies data - real_account_ss58 = decode_account_id(record[0]) + real_account_ss58 = record[0] # list with proxies data is always in that path - proxy_data = cls.from_tuple(record[1].value[0][0]) + proxy_data = cls.from_tuple(record[1][0]) return real_account_ss58, proxy_data @@ -295,7 +296,9 @@ class ProxyAnnouncementInfo: height: int @classmethod - def from_dict(cls, data: tuple) -> list["ProxyAnnouncementInfo"]: + def from_dict( + cls, data: tuple[list[dict[str, str | int]], int] + ) -> list["ProxyAnnouncementInfo"]: """Creates a list of ProxyAnnouncementInfo objects from chain announcement data. This method decodes the raw announcement data returned from the Proxy.Announcements storage function. @@ -311,8 +314,8 @@ def from_dict(cls, data: tuple) -> list["ProxyAnnouncementInfo"]: """ return [ cls( - real=decode_account_id(next(iter(annt["real"]))), - call_hash="0x" + bytes(next(iter(annt["call_hash"]))).hex(), + real=annt["real"], + call_hash=annt["call_hash"], height=annt["height"], ) for annt in data[0] @@ -320,7 +323,7 @@ def from_dict(cls, data: tuple) -> list["ProxyAnnouncementInfo"]: @classmethod def from_query_map_record( - cls, record: tuple + cls, record: tuple[str, tuple[list[dict[str, str | int]], int]] ) -> tuple[str, list["ProxyAnnouncementInfo"]]: """Returns a list of ProxyAnnouncementInfo objects from a tuple of announcements data. @@ -335,9 +338,9 @@ def from_query_map_record( """ # record[0] is the real account (key from storage) # record[1] is the value containing announcements data - delegate = decode_account_id(record[0]) + delegate = record[0] # list with proxies data is always in that path - announcements_data = cls.from_dict(record[1].value[0]) + announcements_data = cls.from_dict(record[1]) return delegate, announcements_data diff --git a/bittensor/core/chain_data/utils.py b/bittensor/core/chain_data/utils.py index d6946c96ae..6c318b172b 100644 --- a/bittensor/core/chain_data/utils.py +++ b/bittensor/core/chain_data/utils.py @@ -5,7 +5,7 @@ from bittensor_wallet.utils import SS58_FORMAT from scalecodec import ScaleBytes -from scalecodec.base import RuntimeConfiguration, ScaleType +from scalecodec.base import RuntimeConfiguration from scalecodec.type_registry import load_type_registry_preset from scalecodec.utils.ss58 import ss58_encode @@ -136,10 +136,11 @@ def process_stake_data(stake_data: list) -> dict: def decode_metadata(metadata: dict) -> str: - commitment = metadata["info"]["fields"][0][0] - raw_bytes = next(iter(commitment.values())) - byte_tuple = raw_bytes[0] if raw_bytes else raw_bytes - return bytes(byte_tuple).decode("utf-8", errors="ignore") + commitment = metadata["info"]["fields"][0] + if isinstance(commitment, str): + return "" + hex_: str = next(iter(commitment.values())) + return bytes.fromhex(hex_.removeprefix("0x")).decode("utf-8", errors="ignore") def decode_block(data: bytes) -> int: @@ -177,7 +178,10 @@ def scale_decode_offset(data: bytes) -> int: else: return 4 - com_bytes, revealed_block = encoded_data + com_hex: str + revealed_block: int + com_hex, revealed_block = encoded_data + com_bytes = bytes.fromhex(com_hex.removeprefix("0x")) offset = scale_decode_offset(com_bytes) revealed_commitment = bytes(com_bytes[offset:]).decode("utf-8", errors="ignore") @@ -196,6 +200,6 @@ def decode_revealed_commitment_with_hotkey( """ key, data = encoded_data - ss58_address = decode_account_id(next(iter(key))) - block_data = tuple(decode_revealed_commitment(p) for p in data.value) + ss58_address = key + block_data = tuple(decode_revealed_commitment(p) for p in data) return ss58_address, block_data diff --git a/bittensor/core/chain_data/weight_commit_info.py b/bittensor/core/chain_data/weight_commit_info.py index 814ef54aca..3e6f13517a 100644 --- a/bittensor/core/chain_data/weight_commit_info.py +++ b/bittensor/core/chain_data/weight_commit_info.py @@ -46,6 +46,7 @@ def from_vec_u8(cls, data: tuple) -> tuple[str, str, int]: @classmethod def from_vec_u8_v2(cls, data: tuple) -> tuple[str, int, str, int]: """ + # TODO no it does not Creates a WeightCommitInfo instance Parameters: @@ -54,13 +55,10 @@ def from_vec_u8_v2(cls, data: tuple) -> tuple[str, int, str, int]: Returns: WeightCommitInfo: A new instance with the decoded data """ - account_id, commit_block, commit_data, round_number = data - - account_id_ = account_id[0] if isinstance(account_id, tuple) else account_id - commit_block = ( - commit_block[0] if isinstance(commit_block, tuple) else commit_block - ) - commit_data = commit_data[0] if isinstance(commit_data, tuple) else commit_data - commit_hex = "0x" + "".join(format(x, "02x") for x in commit_data) + account_id: str + commit_block: int + commit_hex: str + round_number: int + account_id, commit_block, commit_hex, round_number = data - return decode_account_id(account_id_), commit_block, commit_hex, round_number + return account_id, commit_block, commit_hex, round_number diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 68265eccaf..26efd83eaa 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -767,7 +767,7 @@ def query_runtime_api( block_hash = self.determine_block_hash(block) result = self.substrate.runtime_call(runtime_api, method, params, block_hash) - return result.value + return result def query_subtensor( self, @@ -833,12 +833,11 @@ def all_subnets(self, block: Optional[int] = None) -> Optional[list["DynamicInfo a subnet, or None if the query fails. """ block_hash = self.determine_block_hash(block=block) - query = self.substrate.runtime_call( + decoded = self.substrate.runtime_call( api="SubnetInfoRuntimeApi", method="get_all_dynamic_info", block_hash=block_hash, ) - decoded = query.decode() try: subnet_prices = self.get_subnet_prices(block=block) for sn in decoded: @@ -957,12 +956,12 @@ def bonds( params=[storage_index], block_hash=self.determine_block_hash(block), ) - b_map = [] - for uid, b in b_map_encoded: - if b.value is not None: - b_map.append((uid, b.value)) + bond_map = [] + for uid, bond in b_map_encoded: + if bond.value is not None: + bond_map.append((uid, bond)) - return b_map + return bond_map def commit_reveal_enabled(self, netuid: int, block: Optional[int] = None) -> bool: """Check if commit-reveal mechanism is enabled for a given subnet at a specific block. @@ -1123,7 +1122,7 @@ def get_all_commitments( result = {} for id_, value in query: try: - result[decode_account_id(id_[0])] = decode_metadata(value) + result[id_] = decode_metadata(value) except Exception as error: logging.error( f"Error decoding [red]{id_}[/red] and [red]{value}[/red]: {error}" @@ -1225,7 +1224,7 @@ def get_all_neuron_certificates( ) output = {} for key, item in query_certificates: - output[decode_account_id(key)] = Certificate(item.value) + output[key] = Certificate(item) return output def get_all_revealed_commitments( @@ -3948,6 +3947,7 @@ def get_timelocked_weight_commits( storage_function="TimelockedWeightCommits", params=[storage_index], block_hash=self.determine_block_hash(block=block), + page_size=1, ) commits = result.records[0][1] if result.records else [] @@ -4798,7 +4798,7 @@ def weights( params=[storage_index], block_hash=self.determine_block_hash(block), ) - w_map = [(uid, w.value or []) for uid, w in w_map_encoded] + w_map = [(uid, w or []) for uid, w in w_map_encoded] return w_map diff --git a/bittensor/utils/__init__.py b/bittensor/utils/__init__.py index 52003f0017..bcbb3450a0 100644 --- a/bittensor/utils/__init__.py +++ b/bittensor/utils/__init__.py @@ -104,21 +104,37 @@ def __new__(cls, data: Union[str, dict]): def decode_hex_identity_dict(info_dictionary: dict[str, Any]) -> dict[str, Any]: """Decodes a dictionary of hexadecimal identities.""" - decoded_info = {} - for k, v in info_dictionary.items(): - if isinstance(v, dict): - item = next(iter(v.values())) - else: - item = v - if isinstance(item, tuple): - try: - decoded_info[k] = bytes(item).decode() - except UnicodeDecodeError: - print(f"Could not decode: {k}: {item}") - else: - decoded_info[k] = item - return decoded_info + def get_decoded(data: Optional[str]) -> str: + """Decodes a hex-encoded string.""" + if data is None: + return "" + try: + return hex_to_bytes(data).decode() + except (UnicodeDecodeError, ValueError): + raise ValueError(f"Could not decode hex-encoded string: {data}") + + for key, value in info_dictionary.items(): + if isinstance(value, dict): + item = list(value.values())[0] + if isinstance(item, str) and item.startswith("0x"): + info_dictionary[key] = get_decoded(item) + else: + info_dictionary[key] = item + if key == "additional": + additional = [] + for item in value: + if isinstance(item, dict): + for k, v in item.items(): + additional.append((k, get_decoded(v))) + else: + if isinstance(item, (tuple, list)) and len(item) == 2: + k_, v = item + k = k_ if k_ is not None else "" + additional.append((k, get_decoded(v))) + info_dictionary[key] = additional + + return info_dictionary def ss58_to_vec_u8(ss58_address: str) -> list[int]: diff --git a/tests/unit_tests/chain_data/test_utils.py b/tests/unit_tests/chain_data/test_utils.py index 602e03c192..8339cc71eb 100644 --- a/tests/unit_tests/chain_data/test_utils.py +++ b/tests/unit_tests/chain_data/test_utils.py @@ -8,93 +8,20 @@ [ ( { + "block": 5097676, "deposit": 0, - "block": 5415815, "info": { - "fields": ( - ( - { - "Raw64": ( - ( - 51, - 98, - 99, - 54, - 49, - 48, - 57, - 102, - 49, - 101, - 49, - 51, - 102, - 102, - 56, - 102, - 55, - 101, - 98, - 54, - 97, - 102, - 54, - 49, - 53, - 101, - 49, - 102, - 56, - 101, - 49, - 55, - 99, - 57, - 97, - 100, - 100, - 48, - 97, - 50, - 56, - 98, - 99, - 48, - 50, - 54, - 55, - 57, - 52, - 99, - 56, - 54, - 97, - 101, - 50, - 56, - 57, - 57, - 50, - 99, - 102, - 48, - 52, - 53, - ), - ) - }, - ), - ) + "fields": [ + { + "Raw97": "0x7b27706565725f6964273a2027313244334b6f6f57524e7735344157347a725157655a4c32627568553850373167666f3950585151414855774541653468413334272c20276d6f64656c5f68756767696e67666163655f6964273a204e6f6e657d" + } + ] }, }, - "3bc6109f1e13ff8f7eb6af615e1f8e17c9add0a28bc026794c86ae28992cf045", + "{'peer_id': '12D3KooWRNw54AW4zrQWeZL2buhU8P71gfo9PXQQAHUwEAe4hA34', 'model_huggingface_id': None}", ), ( - { - "deposit": 0, - "block": 5866237, - "info": {"fields": (({"ResetBondsFlag": ()},),)}, - }, + {"block": 6161535, "deposit": 0, "info": {"fields": ["ResetBondsFlag"]}}, "", ), ], From d1cafc099a1f93d3339446cae436237baed743f1 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Tue, 14 Apr 2026 16:43:51 +0200 Subject: [PATCH 049/118] Unit tests fixed --- bittensor/core/subtensor.py | 10 +- bittensor/utils/__init__.py | 34 +-- bittensor/utils/balance.py | 1 - .../chain_data/test_coldkey_swap.py | 6 +- tests/unit_tests/test_async_subtensor.py | 244 +++++++----------- tests/unit_tests/test_subtensor.py | 150 +---------- 6 files changed, 110 insertions(+), 335 deletions(-) diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 26efd83eaa..e47759551f 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -1507,9 +1507,8 @@ def get_children( ) if children: formatted_children = [] - for proportion, child in children.value: + for proportion, formatted_child in children.value: # Convert U64 to int - formatted_child = decode_account_id(child[0]) normalized_proportion = u64_normalized_float(proportion) formatted_children.append((normalized_proportion, formatted_child)) return True, formatted_children, "" @@ -4582,6 +4581,7 @@ def query_identity( See the `Bittensor CLI documentation `_ for supported identity parameters. """ + print(coldkey_ss58) identity_info = self.substrate.query( module="SubtensorModule", storage_function="IdentitiesV2", @@ -4593,11 +4593,7 @@ def query_identity( return None try: - identity_data = ( - identity_info.value - if hasattr(identity_info, "value") - else identity_info - ) + identity_data = identity_info.value return ChainIdentity.from_dict( decode_hex_identity_dict(cast(dict[str, Any], identity_data)), ) diff --git a/bittensor/utils/__init__.py b/bittensor/utils/__init__.py index bcbb3450a0..54a11316a3 100644 --- a/bittensor/utils/__init__.py +++ b/bittensor/utils/__init__.py @@ -102,38 +102,18 @@ def __new__(cls, data: Union[str, dict]): return str.__new__(cls, string) -def decode_hex_identity_dict(info_dictionary: dict[str, Any]) -> dict[str, Any]: +def decode_hex_identity_dict(info_dictionary: dict[str, dict | str]) -> dict[str, Any]: """Decodes a dictionary of hexadecimal identities.""" - def get_decoded(data: Optional[str]) -> str: - """Decodes a hex-encoded string.""" - if data is None: - return "" - try: - return hex_to_bytes(data).decode() - except (UnicodeDecodeError, ValueError): - raise ValueError(f"Could not decode hex-encoded string: {data}") - for key, value in info_dictionary.items(): if isinstance(value, dict): item = list(value.values())[0] - if isinstance(item, str) and item.startswith("0x"): - info_dictionary[key] = get_decoded(item) - else: - info_dictionary[key] = item - if key == "additional": - additional = [] - for item in value: - if isinstance(item, dict): - for k, v in item.items(): - additional.append((k, get_decoded(v))) - else: - if isinstance(item, (tuple, list)) and len(item) == 2: - k_, v = item - k = k_ if k_ is not None else "" - additional.append((k, get_decoded(v))) - info_dictionary[key] = additional - + else: + item = value + if isinstance(item, str) and item.startswith("0x"): + info_dictionary[key] = hex_to_bytes(item.removeprefix("0x")).decode() + else: + info_dictionary[key] = item return info_dictionary diff --git a/bittensor/utils/balance.py b/bittensor/utils/balance.py index 0fc55b4d5e..6690f17fb3 100644 --- a/bittensor/utils/balance.py +++ b/bittensor/utils/balance.py @@ -1,6 +1,5 @@ from typing import Optional, TypedDict, Union -from scalecodec import ScaleType from scalecodec.utils.math import fixed_to_float as fixed_to_float from bittensor.core import settings diff --git a/tests/unit_tests/chain_data/test_coldkey_swap.py b/tests/unit_tests/chain_data/test_coldkey_swap.py index db870030b5..db93a8ee6e 100644 --- a/tests/unit_tests/chain_data/test_coldkey_swap.py +++ b/tests/unit_tests/chain_data/test_coldkey_swap.py @@ -71,11 +71,7 @@ def test_coldkey_swap_dispute_info_from_record(mocker): """Test from_record returns ColdkeySwapDisputeInfo from query_map record.""" decoded_coldkey = "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY" disputed_block = 999 - record = (mocker.Mock(), mocker.Mock(value=disputed_block)) - mocker.patch( - "bittensor.core.chain_data.coldkey_swap.decode_account_id", - return_value=decoded_coldkey, - ) + record = (decoded_coldkey, disputed_block) from_record = ColdkeySwapDisputeInfo.from_record(record) diff --git a/tests/unit_tests/test_async_subtensor.py b/tests/unit_tests/test_async_subtensor.py index f4ae3b17af..21ba4bb12b 100644 --- a/tests/unit_tests/test_async_subtensor.py +++ b/tests/unit_tests/test_async_subtensor.py @@ -577,7 +577,7 @@ async def test_query_runtime_api(subtensor, mocker): fake_block_hash, ) - assert result == mocked_runtime_call.return_value.value + assert result == mocked_runtime_call.return_value @pytest.mark.asyncio @@ -1040,7 +1040,7 @@ async def test_get_neuron_for_pubkey_and_subnet_success(subtensor, mocker): mocker.patch.object( subtensor.substrate, "runtime_call", - return_value=mocker.Mock(value=fake_result), + return_value=fake_result, ) mocked_neuron_info = mocker.patch.object( async_subtensor.NeuronInfo, "from_dict", return_value="fake_neuron_info" @@ -1118,7 +1118,7 @@ async def test_get_neuron_for_pubkey_and_subnet_rpc_result_empty(subtensor, mock mocker.patch.object( subtensor.substrate, "runtime_call", - return_value=mocker.Mock(value=None), + return_value=None, ) mocked_get_null_neuron = mocker.patch.object( async_subtensor.NeuronInfo, "get_null_neuron", return_value="null_neuron" @@ -1171,7 +1171,7 @@ async def test_neuron_for_uid_happy_path(subtensor, mocker): # Asserts mocked_null_neuron.assert_not_called() mocked_neuron_info_from_dict.assert_called_once_with( - subtensor.substrate.runtime_call.return_value.value + subtensor.substrate.runtime_call.return_value ) assert result == mocked_neuron_info_from_dict.return_value @@ -1213,11 +1213,7 @@ async def test_neuron_for_uid(subtensor, mocker): ) # no result in response - mocked_substrate_runtime_call = mocker.AsyncMock( - return_value=mocker.Mock( - value=None, - ), - ) + mocked_substrate_runtime_call = mocker.AsyncMock(return_value=None) subtensor.substrate.runtime_call = mocked_substrate_runtime_call mocked_neuron_info_from_dict = mocker.patch.object( @@ -1258,7 +1254,7 @@ async def test_get_delegated_no_block_hash_no_reuse(subtensor, mocker): None, ) mocked_delegated_list_from_dicts.assert_called_once_with( - subtensor.substrate.runtime_call.return_value.value + subtensor.substrate.runtime_call.return_value ) assert result == mocked_delegated_list_from_dicts.return_value @@ -1288,7 +1284,7 @@ async def test_get_delegated_with_block_hash(subtensor, mocker): fake_block_hash, ) mocked_delegated_list_from_dicts.assert_called_once_with( - subtensor.substrate.runtime_call.return_value.value + subtensor.substrate.runtime_call.return_value ) assert result == mocked_delegated_list_from_dicts.return_value @@ -1318,7 +1314,7 @@ async def test_get_delegated_with_reuse_block(subtensor, mocker): subtensor.substrate.last_block_hash, ) mocked_delegated_list_from_dicts.assert_called_once_with( - subtensor.substrate.runtime_call.return_value.value + subtensor.substrate.runtime_call.return_value ) assert result == mocked_delegated_list_from_dicts.return_value @@ -1329,11 +1325,7 @@ async def test_get_delegated_with_empty_result(subtensor, mocker): # Preps fake_coldkey_ss58 = "fake_ss58_address" - mocked_runtime_call = mocker.AsyncMock( - return_value=mocker.Mock( - value=None, - ), - ) + mocked_runtime_call = mocker.AsyncMock(return_value=None) subtensor.substrate.runtime_call = mocked_runtime_call # Call @@ -1355,15 +1347,17 @@ async def test_query_identity_successful(subtensor, mocker): # Preps fake_coldkey_ss58 = "test_key" fake_block_hash = "block_hash" - fake_identity_info = { - "additional": "Additional", - "description": "Description", - "discord": "", - "github_repo": "https://github.com/opentensor/bittensor", - "image": "", - "name": "Name", - "url": "https://www.example.com", - } + fake_identity_info = mocker.MagicMock( + value={ + "additional": "Additional", + "description": "Description", + "discord": "", + "github_repo": "https://github.com/opentensor/bittensor", + "image": "", + "name": "Name", + "url": "https://www.example.com", + } + ) mocked_query = mocker.AsyncMock(return_value=fake_identity_info) subtensor.substrate.query = mocked_query @@ -1449,8 +1443,8 @@ async def test_weights_successful(subtensor, mocker): fake_netuid = 1 fake_block_hash = "block_hash" fake_weights = [ - (0, mocker.AsyncMock(value=[(1, 10), (2, 20)])), - (1, mocker.AsyncMock(value=[(0, 15), (2, 25)])), + (0, [(1, 10), (2, 20)]), + (1, [(0, 15), (2, 25)]), ] async def mock_query_map(**_): @@ -1479,8 +1473,8 @@ async def test_bonds(subtensor, mocker): fake_netuid = 1 fake_block_hash = "block_hash" fake_bonds = [ - (0, mocker.Mock(value=[(1, 100), (2, 200)])), - (1, mocker.Mock(value=[(0, 150), (2, 250)])), + (0, [(1, 100), (2, 200)]), + (1, [(0, 150), (2, 250)]), ] async def mock_query_map(**_): @@ -1853,19 +1847,14 @@ async def test_get_children_success(subtensor, mocker): fake_netuid = 1 fake_children = mocker.Mock( value=[ - (1000, ["child_key_1"]), - (2000, ["child_key_2"]), + (1000, "decoded_child_key_1"), + (2000, "decoded_child_key_2"), ] ) mocked_query = mocker.AsyncMock(return_value=fake_children) subtensor.substrate.query = mocked_query - mocked_decode_account_id = mocker.Mock( - side_effect=["decoded_child_key_1", "decoded_child_key_2"] - ) - mocker.patch.object(async_subtensor, "decode_account_id", mocked_decode_account_id) - expected_formatted_children = [ (u64_normalized_float(1000), "decoded_child_key_1"), (u64_normalized_float(2000), "decoded_child_key_2"), @@ -1881,9 +1870,6 @@ async def test_get_children_success(subtensor, mocker): storage_function="ChildKeys", params=[fake_hotkey, fake_netuid], ) - mocked_decode_account_id.assert_has_calls( - [mocker.call("child_key_1"), mocker.call("child_key_2")] - ) assert result == (True, expected_formatted_children, "") @@ -1949,19 +1935,14 @@ async def test_get_parents_success(subtensor, mocker): fake_netuid = 1 fake_parents = mocker.Mock( value=[ - (1000, ["parent_key_1"]), - (2000, ["parent_key_2"]), + (1000, "decoded_parent_key_1"), + (2000, "decoded_parent_key_2"), ] ) mocked_query = mocker.AsyncMock(return_value=fake_parents) subtensor.substrate.query = mocked_query - mocked_decode_account_id = mocker.Mock( - side_effect=["decoded_parent_key_1", "decoded_parent_key_2"] - ) - mocker.patch.object(async_subtensor, "decode_account_id", mocked_decode_account_id) - expected_formatted_parents = [ (u64_normalized_float(1000), "decoded_parent_key_1"), (u64_normalized_float(2000), "decoded_parent_key_2"), @@ -1977,9 +1958,6 @@ async def test_get_parents_success(subtensor, mocker): storage_function="ParentKeys", params=[fake_hotkey, fake_netuid], ) - mocked_decode_account_id.assert_has_calls( - [mocker.call("parent_key_1"), mocker.call("parent_key_2")] - ) assert result == expected_formatted_parents @@ -2029,7 +2007,7 @@ async def test_get_children_pending(mock_substrate, subtensor): [ ( U64_MAX, - (tuple(bytearray(32)),), + "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", ), ], 123, @@ -2209,32 +2187,28 @@ async def test_get_delegate_identities(subtensor, mocker): fake_block_hash = "block_hash" fake_chain_data = [ ( - ["delegate1_ss58"], - mocker.Mock( - value={ - "additional": "", - "description": "", - "discord": "", - "github_repo": "", - "image": "", - "name": "Chain Delegate 1", - "url": "", - }, - ), + "delegate1_ss58", + { + "additional": "", + "description": "", + "discord": "", + "github_repo": "", + "image": "", + "name": "Chain Delegate 1", + "url": "", + }, ), ( - ["delegate2_ss58"], - mocker.Mock( - value={ - "additional": "", - "description": "", - "discord": "", - "github_repo": "", - "image": "", - "name": "Chain Delegate 2", - "url": "", - }, - ), + "delegate2_ss58", + { + "additional": "", + "description": "", + "discord": "", + "github_repo": "", + "image": "", + "name": "Chain Delegate 2", + "url": "", + }, ), ] @@ -2243,9 +2217,6 @@ async def test_get_delegate_identities(subtensor, mocker): ) subtensor.substrate.query_map = mocked_query_map - mocked_decode_account_id = mocker.Mock(side_effect=lambda ss58: ss58) - mocker.patch.object(async_subtensor, "decode_account_id", mocked_decode_account_id) - mocked_decode_hex_identity_dict = mocker.Mock(side_effect=lambda data: data) mocker.patch.object( async_subtensor, "decode_hex_identity_dict", mocked_decode_hex_identity_dict @@ -2814,19 +2785,10 @@ async def test_get_owned_hotkeys_happy_path(subtensor, mocker): # Prep fake_coldkey = "fake_hotkey" fake_hotkey = "fake_hotkey" - fake_hotkeys = [ - [ - fake_hotkey, - ] - ] + fake_hotkeys = [fake_hotkey] mocked_subtensor = mocker.AsyncMock(return_value=fake_hotkeys) mocker.patch.object(subtensor.substrate, "query", new=mocked_subtensor) - mocked_decode_account_id = mocker.Mock() - mocker.patch.object( - async_subtensor, "decode_account_id", new=mocked_decode_account_id - ) - # Call result = await subtensor.get_owned_hotkeys(fake_coldkey) @@ -2837,8 +2799,7 @@ async def test_get_owned_hotkeys_happy_path(subtensor, mocker): params=[fake_coldkey], block_hash=None, ) - assert result == [mocked_decode_account_id.return_value] - mocked_decode_account_id.assert_called_once_with(fake_hotkey) + assert result == [fake_hotkey] @pytest.mark.asyncio @@ -3572,45 +3533,39 @@ async def test_get_liquidity_list_happy_path(subtensor, fake_wallet, mocker): fake_positions = [ [ (2,), - mocker.Mock( - value={ - "id": (2,), - "netuid": 2, - "tick_low": (206189,), - "tick_high": (208196,), - "liquidity": 1000000000000, - "fees_tao": {"bits": 0}, - "fees_alpha": {"bits": 0}, - } - ), + { + "id": (2,), + "netuid": 2, + "tick_low": (206189,), + "tick_high": (208196,), + "liquidity": 1000000000000, + "fees_tao": {"bits": 0}, + "fees_alpha": {"bits": 0}, + }, ], [ - (2,), - mocker.Mock( - value={ - "id": (2,), - "netuid": 2, - "tick_low": (216189,), - "tick_high": (198196,), - "liquidity": 2000000000000, - "fees_tao": {"bits": 0}, - "fees_alpha": {"bits": 0}, - } - ), + 2, + { + "id": (2,), + "netuid": 2, + "tick_low": (216189,), + "tick_high": (198196,), + "liquidity": 2000000000000, + "fees_tao": {"bits": 0}, + "fees_alpha": {"bits": 0}, + }, ], [ - (2,), - mocker.Mock( - value={ - "id": (2,), - "netuid": 2, - "tick_low": (226189,), - "tick_high": (188196,), - "liquidity": 3000000000000, - "fees_tao": {"bits": 0}, - "fees_alpha": {"bits": 0}, - } - ), + 2, + { + "id": (2,), + "netuid": 2, + "tick_low": (226189,), + "tick_high": (188196,), + "liquidity": 3000000000000, + "fees_tao": {"bits": 0}, + "fees_alpha": {"bits": 0}, + }, ], ] @@ -4213,8 +4168,8 @@ async def test_get_auto_stakes(subtensor, mocker): fake_hk_1 = mocker.Mock() fake_hk_2 = mocker.Mock() - dest_value_1 = mocker.Mock(value=[fake_hk_1]) - dest_value_2 = mocker.Mock(value=[fake_hk_2]) + dest_value_1 = fake_hk_1 + dest_value_2 = fake_hk_2 mock_result = mocker.MagicMock() mock_result.__aiter__.return_value = iter([(0, dest_value_1), (1, dest_value_2)]) @@ -4222,12 +4177,6 @@ async def test_get_auto_stakes(subtensor, mocker): subtensor.substrate, "query_map", return_value=mock_result ) - mocked_decode_account_id = mocker.patch.object( - async_subtensor, - "decode_account_id", - side_effect=[fake_hk_1, fake_hk_2], - ) - # Call result = await subtensor.get_auto_stakes(coldkey_ss58=fake_coldkey) @@ -4239,9 +4188,6 @@ async def test_get_auto_stakes(subtensor, mocker): params=[fake_coldkey], block_hash=mock_determine_block_hash.return_value, ) - mocked_decode_account_id.assert_has_calls( - [mocker.call(dest_value_1.value[0]), mocker.call(dest_value_2.value[0])] - ) assert result == {0: fake_hk_1, 1: fake_hk_2} @@ -4639,7 +4585,6 @@ async def test_get_crowdloan_contributions(mocker, subtensor): subtensor.substrate, "query_map", return_value=fake_result ) - mocked_decode_account_id = mocker.patch.object(async_subtensor, "decode_account_id") mocked_from_rao = mocker.patch.object(async_subtensor.Balance, "from_rao") # Call @@ -4653,9 +4598,7 @@ async def test_get_crowdloan_contributions(mocker, subtensor): params=[fake_crowdloan_id], block_hash=mocked_determine_block_hash.return_value, ) - assert result == { - mocked_decode_account_id.return_value: mocked_from_rao.return_value - } + assert result == {fake_hk_array: mocked_from_rao.return_value} @pytest.mark.parametrize( @@ -4724,7 +4667,7 @@ async def test_get_crowdloans(mocker, subtensor): """Tests subtensor `get_crowdloans` method.""" # Preps fake_id = mocker.Mock(spec=int) - fake_crowdloan = mocker.Mock(value=mocker.Mock(spec=dict)) + fake_crowdloan = mocker.Mock(spec=dict) mocked_determine_block_hash = mocker.patch.object(subtensor, "determine_block_hash") records = [(fake_id, fake_crowdloan)] @@ -4753,7 +4696,7 @@ async def test_get_crowdloans(mocker, subtensor): ) mocked_decode_crowdloan_entry.assert_awaited_once_with( crowdloan_id=fake_id, - data=fake_crowdloan.value, + data=fake_crowdloan, block_hash=mocked_determine_block_hash.return_value, ) assert result == [mocked_decode_crowdloan_entry.return_value] @@ -5807,10 +5750,10 @@ async def test_get_stake_info_for_coldkeys_success(subtensor, mocker): fake_block_hash = None fake_reuse_block = False - fake_ck1 = b"\x16:\xech\r\xde,g\x03R1\xb9\x88q\xe79\xb8\x88\x93\xae\xd2)?*\rp\xb2\xe62\xads\x1c" - fake_ck2 = b"\x17:\xech\r\xde,g\x03R1\xb9\x88q\xe79\xb8\x88\x93\xae\xd2)?*\rp\xb2\xe62\xads\x1d" - fake_decoded_ck1 = "decoded_coldkey1" - fake_decoded_ck2 = "decoded_coldkey2" + fake_ck1 = fake_coldkey_ss58s[0] + fake_ck2 = fake_coldkey_ss58s[1] + fake_decoded_ck1 = fake_coldkey_ss58s[0] + fake_decoded_ck2 = fake_coldkey_ss58s[1] stake_info_dict_1 = { "netuid": 1, @@ -5843,12 +5786,6 @@ async def test_get_stake_info_for_coldkeys_success(subtensor, mocker): ) subtensor.query_runtime_api = mocked_query_runtime_api - mocked_decode_account_id = mocker.patch.object( - async_subtensor, - "decode_account_id", - side_effect=[fake_decoded_ck1, fake_decoded_ck2], - ) - mock_stake_info_1 = mocker.Mock(spec=StakeInfo) mock_stake_info_2 = mocker.Mock(spec=StakeInfo) mocked_stake_info_list_from_dicts = mocker.patch.object( @@ -5878,9 +5815,6 @@ async def test_get_stake_info_for_coldkeys_success(subtensor, mocker): block_hash=fake_block_hash, reuse_block=fake_reuse_block, ) - mocked_decode_account_id.assert_has_calls( - [mocker.call(fake_ck1), mocker.call(fake_ck2)] - ) mocked_stake_info_list_from_dicts.assert_has_calls( [mocker.call([stake_info_dict_1]), mocker.call([stake_info_dict_2])] ) diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index 3e63a7be24..436b39b289 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -912,10 +912,6 @@ def test_query_runtime_api(subtensor, mocker): subtensor, "determine_block_hash", ) - # mock_runtime_call = mocker.patch.object( - # subtensor.substrate, - # "runtime_call", - # ) # Call result = subtensor.query_runtime_api(fake_runtime_api, fake_method, None) @@ -929,7 +925,7 @@ def test_query_runtime_api(subtensor, mocker): ) mock_determine_block_hash.assert_called_once_with(None) - assert result == subtensor.substrate.runtime_call.return_value.value + assert result == subtensor.substrate.runtime_call.return_value def test_query_map_subtensor(subtensor, mocker): @@ -1372,7 +1368,7 @@ def test_neuron_for_uid_response_none(subtensor, mocker): subtensor_module.NeuronInfo, "get_null_neuron" ) - subtensor.substrate.runtime_call.return_value.value = None + subtensor.substrate.runtime_call.return_value = None # Call result = subtensor.neuron_for_uid( @@ -1502,10 +1498,7 @@ def test_get_commitment(subtensor, mocker): fake_uid = 2 fake_block = 3 fake_hotkey = "hotkey" - expected_result = ( - "{'peer_id': '12D3KooWFWnHBmUFxvfL6PfZ5eGHdhgsEqNnsxuN1HE9EtfW8THi', " - "'model_huggingface_id': 'kmfoda/gpt2-1b-miner-3'}" - ) + expected_result = "{'peer_id': '12D3KooWRNw54AW4zrQWeZL2buhU8P71gfo9PXQQAHUwEAe4hA34', 'model_huggingface_id': None}" mocked_metagraph = mocker.MagicMock() subtensor.metagraph = mocked_metagraph @@ -1516,133 +1509,11 @@ def test_get_commitment(subtensor, mocker): "deposit": 0, "block": 3843930, "info": { - "fields": ( - ( - { - "Raw117": ( - ( - 123, - 39, - 112, - 101, - 101, - 114, - 95, - 105, - 100, - 39, - 58, - 32, - 39, - 49, - 50, - 68, - 51, - 75, - 111, - 111, - 87, - 70, - 87, - 110, - 72, - 66, - 109, - 85, - 70, - 120, - 118, - 102, - 76, - 54, - 80, - 102, - 90, - 53, - 101, - 71, - 72, - 100, - 104, - 103, - 115, - 69, - 113, - 78, - 110, - 115, - 120, - 117, - 78, - 49, - 72, - 69, - 57, - 69, - 116, - 102, - 87, - 56, - 84, - 72, - 105, - 39, - 44, - 32, - 39, - 109, - 111, - 100, - 101, - 108, - 95, - 104, - 117, - 103, - 103, - 105, - 110, - 103, - 102, - 97, - 99, - 101, - 95, - 105, - 100, - 39, - 58, - 32, - 39, - 107, - 109, - 102, - 111, - 100, - 97, - 47, - 103, - 112, - 116, - 50, - 45, - 49, - 98, - 45, - 109, - 105, - 110, - 101, - 114, - 45, - 51, - 39, - 125, - ), - ) - }, - ), - ) + "fields": [ + { + "Raw97": "0x7b27706565725f6964273a2027313244334b6f6f57524e7735344157347a725157655a4c32627568553850373167666f3950585151414855774541653468413334272c20276d6f64656c5f68756767696e67666163655f6964273a204e6f6e657d" + } + ], }, } @@ -4161,10 +4032,8 @@ def test_all_subnets(subtensor, mocker): "get_subnet_prices", return_value={0: Balance.from_tao(1), 1: Balance.from_tao(0.029258617)}, ) - mocked_decode = mocker.Mock(return_value=[{"netuid": 0}, {"netuid": 1}]) - mocked_runtime_call = mocker.Mock(decode=mocked_decode) mocker.patch.object( - subtensor.substrate, "runtime_call", return_value=mocked_runtime_call + subtensor.substrate, "runtime_call", return_value=[{"netuid": 0}, {"netuid": 1}] ) # Call @@ -4357,6 +4226,7 @@ def test_get_timelocked_weight_commits(subtensor, mocker): storage_function="TimelockedWeightCommits", params=[netuid], block_hash=mock_determine_block_hash.return_value, + page_size=1, ) assert result == [] From ba9ae10a88edb556e4f8968c4f848e77225e77a8 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Tue, 14 Apr 2026 16:58:12 +0200 Subject: [PATCH 050/118] Bump cyscale --- pyproject.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e98ce292fb..7ddaf7e5ee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ version = "10.2.0" description = "Bittensor SDK" readme = "README.md" authors = [ - {name = "bittensor.com"} + { name = "bittensor.com" } ] license = { file = "LICENSE" } requires-python = ">=3.10,<3.15" @@ -29,7 +29,7 @@ dependencies = [ "retry==0.9.2", "requests>=2.0.0,<3.0", "pydantic>=2.3,<3", - "cyscale==0.1.11", + "cyscale==0.2.0", "uvicorn", "bittensor-drand>=1.3.0,<2.0.0", "bittensor-wallet==4.0.1", @@ -94,5 +94,5 @@ classifiers = [ ] [tool.setuptools] -package-dir = {"bittensor" = "bittensor"} +package-dir = { "bittensor" = "bittensor" } script-files = ["bittensor/utils/certifi.sh"] From 75ef5c609522ebf01edc61cfacf49e38562e61ee Mon Sep 17 00:00:00 2001 From: BD Himes Date: Tue, 14 Apr 2026 17:40:48 +0200 Subject: [PATCH 051/118] Checkin --- bittensor/core/chain_data/__init__.py | 3 +- bittensor/core/chain_data/coldkey_swap.py | 3 +- bittensor/core/chain_data/crowdloan_info.py | 11 +--- bittensor/core/chain_data/delegate_info.py | 11 ++-- .../core/chain_data/delegate_info_lite.py | 5 +- bittensor/core/chain_data/dynamic_info.py | 5 +- bittensor/core/chain_data/metagraph_info.py | 42 +++---------- bittensor/core/chain_data/neuron_info.py | 6 +- bittensor/core/chain_data/neuron_info_lite.py | 6 +- .../core/chain_data/proposal_vote_data.py | 5 +- bittensor/core/chain_data/stake_info.py | 5 +- bittensor/core/chain_data/subnet_info.py | 3 +- bittensor/core/chain_data/subnet_state.py | 5 +- bittensor/core/chain_data/utils.py | 22 +------ .../core/chain_data/weight_commit_info.py | 12 ++-- bittensor/core/subtensor.py | 28 ++++----- tests/unit_tests/test_async_subtensor.py | 30 ---------- tests/unit_tests/test_subtensor.py | 59 ++----------------- 18 files changed, 58 insertions(+), 203 deletions(-) diff --git a/bittensor/core/chain_data/__init__.py b/bittensor/core/chain_data/__init__.py index 0982c5cde2..f66c3bd0a6 100644 --- a/bittensor/core/chain_data/__init__.py +++ b/bittensor/core/chain_data/__init__.py @@ -37,7 +37,7 @@ from .subnet_identity import SubnetIdentity from .subnet_info import SubnetInfo from .subnet_state import SubnetState -from .utils import decode_account_id, process_stake_data +from .utils import process_stake_data from .weight_commit_info import WeightCommitInfo ProposalCallData = GenericCall @@ -78,6 +78,5 @@ "SubnetInfo", "SubnetState", "WeightCommitInfo", - "decode_account_id", "process_stake_data", ] diff --git a/bittensor/core/chain_data/coldkey_swap.py b/bittensor/core/chain_data/coldkey_swap.py index fb64df43c5..6388ce452b 100644 --- a/bittensor/core/chain_data/coldkey_swap.py +++ b/bittensor/core/chain_data/coldkey_swap.py @@ -1,7 +1,6 @@ from dataclasses import asdict, dataclass, fields from typing import Optional from scalecodec.base import ScaleType -from bittensor.core.chain_data.utils import decode_account_id @dataclass @@ -67,7 +66,7 @@ def from_record(cls, record: tuple) -> "ColdkeySwapAnnouncementInfo": Returns: ColdkeySwapAnnouncementInfo object with announcement details for the coldkey from the record. """ - coldkey_ss58 = decode_account_id(record[0]) + coldkey_ss58 = record[0] announcement_data = record[1] return cls.from_query(coldkey_ss58, announcement_data) diff --git a/bittensor/core/chain_data/crowdloan_info.py b/bittensor/core/chain_data/crowdloan_info.py index 83e119ffbd..db7185b78d 100644 --- a/bittensor/core/chain_data/crowdloan_info.py +++ b/bittensor/core/chain_data/crowdloan_info.py @@ -1,7 +1,6 @@ from dataclasses import dataclass from typing import Optional -from bittensor.core.chain_data.utils import decode_account_id from bittensor.utils.balance import Balance @@ -47,18 +46,14 @@ def from_dict(cls, idx: int, data: dict) -> "CrowdloanInfo": """Returns a CrowdloanInfo object from decoded chain data.""" return cls( id=idx, - creator=decode_account_id(data["creator"]), + creator=data["creator"], deposit=Balance.from_rao(data["deposit"]), min_contribution=Balance.from_rao(data["min_contribution"]), end=data["end"], cap=Balance.from_rao(data["cap"]), - funds_account=decode_account_id(data["funds_account"]) - if data.get("funds_account") - else None, + funds_account=data["funds_account"], raised=Balance.from_rao(data["raised"]), - target_address=decode_account_id(data.get("target_address")) - if data.get("target_address") - else None, + target_address=data.get("target_address"), call=data.get("call") if data.get("call") else None, finalized=data["finalized"], contributors_count=data["contributors_count"], diff --git a/bittensor/core/chain_data/delegate_info.py b/bittensor/core/chain_data/delegate_info.py index 91301b6034..d12696deb8 100644 --- a/bittensor/core/chain_data/delegate_info.py +++ b/bittensor/core/chain_data/delegate_info.py @@ -2,7 +2,6 @@ from typing import Optional from bittensor.core.chain_data.info_base import InfoBase -from bittensor.core.chain_data.utils import decode_account_id from bittensor.utils import u16_normalized_float from bittensor.utils.balance import Balance @@ -47,14 +46,14 @@ class DelegateInfo(DelegateInfoBase): @classmethod def _from_dict(cls, decoded: dict) -> Optional["DelegateInfo"]: - hotkey = decode_account_id(decoded.get("delegate_ss58")) - owner = decode_account_id(decoded.get("owner_ss58")) + hotkey = decoded.get("delegate_ss58") + owner = decoded.get("owner_ss58") nominators = {} total_stake_by_netuid = {} for raw_nominator, raw_stakes in decoded.get("nominators", []): - nominator_ss58 = decode_account_id(raw_nominator) + nominator_ss58 = raw_nominator stakes = { int(netuid): Balance.from_rao(stake_amt).set_unit(int(netuid)) for (netuid, stake_amt) in raw_stakes @@ -96,8 +95,8 @@ def _from_dict( cls, decoded: tuple[dict, tuple[int, int]] ) -> Optional["DelegatedInfo"]: delegate_info, (netuid, stake) = decoded - hotkey = decode_account_id(delegate_info.get("delegate_ss58")) - owner = decode_account_id(delegate_info.get("owner_ss58")) + hotkey = delegate_info.get("delegate_ss58") + owner = delegate_info.get("owner_ss58") return cls( hotkey_ss58=hotkey, owner_ss58=owner, diff --git a/bittensor/core/chain_data/delegate_info_lite.py b/bittensor/core/chain_data/delegate_info_lite.py index 06666769dc..262c708619 100644 --- a/bittensor/core/chain_data/delegate_info_lite.py +++ b/bittensor/core/chain_data/delegate_info_lite.py @@ -1,7 +1,6 @@ from dataclasses import dataclass from bittensor.core.chain_data.info_base import InfoBase -from bittensor.core.chain_data.utils import decode_account_id from bittensor.utils import u16_normalized_float from bittensor.utils.balance import Balance @@ -34,10 +33,10 @@ class DelegateInfoLite(InfoBase): @classmethod def _from_dict(cls, decoded: dict) -> "DelegateInfoLite": return DelegateInfoLite( - delegate_ss58=decode_account_id(decoded["delegate_ss58"]), + delegate_ss58=decoded["delegate_ss58"], take=u16_normalized_float(decoded["take"]), nominators=decoded["nominators"], - owner_ss58=decode_account_id(decoded["owner_ss58"]), + owner_ss58=decoded["owner_ss58"], registrations=decoded["registrations"], validator_permits=decoded["validator_permits"], return_per_1000=Balance.from_rao(decoded["return_per_1000"]), diff --git a/bittensor/core/chain_data/dynamic_info.py b/bittensor/core/chain_data/dynamic_info.py index 4d80d79dce..b92f9fa816 100644 --- a/bittensor/core/chain_data/dynamic_info.py +++ b/bittensor/core/chain_data/dynamic_info.py @@ -7,7 +7,6 @@ from typing import Optional, Union from bittensor.core.chain_data.info_base import InfoBase -from bittensor.core.chain_data.utils import decode_account_id from bittensor.core.chain_data.subnet_identity import SubnetIdentity from bittensor.utils.balance import Balance, fixed_to_float @@ -52,8 +51,8 @@ def _from_dict(cls, decoded: dict) -> "DynamicInfo": True if int(decoded["netuid"]) > 0 else False ) # Root is not dynamic - owner_hotkey = decode_account_id(decoded["owner_hotkey"]) - owner_coldkey = decode_account_id(decoded["owner_coldkey"]) + owner_hotkey = decoded["owner_hotkey"] + owner_coldkey = decoded["owner_coldkey"] emission = Balance.from_rao(decoded["emission"]).set_unit(0) alpha_in = Balance.from_rao(decoded["alpha_in"]).set_unit(netuid) diff --git a/bittensor/core/chain_data/metagraph_info.py b/bittensor/core/chain_data/metagraph_info.py index d82292fb1e..989bfd6861 100644 --- a/bittensor/core/chain_data/metagraph_info.py +++ b/bittensor/core/chain_data/metagraph_info.py @@ -6,7 +6,6 @@ from bittensor.core.chain_data.chain_identity import ChainIdentity from bittensor.core.chain_data.info_base import InfoBase from bittensor.core.chain_data.subnet_identity import SubnetIdentity -from bittensor.core.chain_data.utils import decode_account_id from bittensor.utils import ( get_netuid_and_mechid_by_storage_index, u64_normalized_float as u64tf, @@ -25,8 +24,7 @@ def get_selective_metagraph_commitments( if commitments := decoded.get("commitments"): result = [] for commitment in commitments: - account_id_bytes, commitment_bytes = commitment - hotkey = decode_account_id(account_id_bytes) + hotkey, commitment_bytes = commitment commitment = bytes( commitment_bytes[SELECTIVE_METAGRAPH_COMMITMENTS_OFFSET:] ).decode("utf-8", errors="ignore") @@ -209,16 +207,8 @@ def _from_dict(cls, decoded: dict) -> "MetagraphInfo": identity=decoded["identity"], network_registered_at=decoded["network_registered_at"], # Keys for owner. - owner_hotkey=( - decode_account_id(decoded["owner_hotkey"][0]) - if decoded.get("owner_hotkey") is not None - else None - ), - owner_coldkey=( - decode_account_id(decoded["owner_coldkey"][0]) - if decoded.get("owner_coldkey") is not None - else None - ), + owner_hotkey=decoded.get("owner_hotkey"), + owner_coldkey=decoded.get("owner_coldkey"), # Tempo terms. block=decoded["block"], tempo=decoded["tempo"], @@ -312,16 +302,8 @@ def _from_dict(cls, decoded: dict) -> "MetagraphInfo": else None ), # Metagraph info. - hotkeys=( - [decode_account_id(ck) for ck in decoded.get("hotkeys", [])] - if decoded.get("hotkeys") is not None - else None - ), - coldkeys=( - [decode_account_id(hk) for hk in decoded.get("coldkeys", [])] - if decoded.get("coldkeys") is not None - else None - ), + hotkeys=decoded.get("hotkeys"), + coldkeys=decoded.get("coldkeys"), identities=decoded["identities"], axons=decoded.get("axons", []), active=decoded["active"], @@ -383,19 +365,13 @@ def _from_dict(cls, decoded: dict) -> "MetagraphInfo": ), # Dividend break down tao_dividends_per_hotkey=( - [ - (decode_account_id(alpha[0]), _tbwu(alpha[1])) - for alpha in decoded["tao_dividends_per_hotkey"] - ] - if decoded.get("tao_dividends_per_hotkey") is not None + [(ss58, _tbwu(alpha)) for (ss58, alpha) in tdph] + if (tdph := decoded.get("tao_dividends_per_hotkey")) is not None else None ), alpha_dividends_per_hotkey=( - [ - (decode_account_id(adphk[0]), _tbwu(adphk[1], _netuid)) - for adphk in decoded["alpha_dividends_per_hotkey"] - ] - if decoded.get("alpha_dividends_per_hotkey") is not None + [(ss58, _tbwu(adphk, _netuid)) for (ss58, adphk) in adph] + if (adph := decoded.get("alpha_dividends_per_hotkey")) is not None else None ), validators=[v for v in decoded["validators"]] diff --git a/bittensor/core/chain_data/neuron_info.py b/bittensor/core/chain_data/neuron_info.py index 6c3b89293d..b9bb43fd94 100644 --- a/bittensor/core/chain_data/neuron_info.py +++ b/bittensor/core/chain_data/neuron_info.py @@ -4,7 +4,7 @@ from bittensor.core.chain_data.axon_info import AxonInfo from bittensor.core.chain_data.info_base import InfoBase from bittensor.core.chain_data.prometheus_info import PrometheusInfo -from bittensor.core.chain_data.utils import decode_account_id, process_stake_data +from bittensor.core.chain_data.utils import process_stake_data from bittensor.utils import u16_normalized_float from bittensor.utils.balance import Balance @@ -120,8 +120,8 @@ def _from_dict(cls, decoded: Any) -> "NeuronInfo": """Returns a NeuronInfo object from decoded chain data.""" stake_dict = process_stake_data(decoded["stake"]) total_stake = sum(stake_dict.values()) if stake_dict else Balance(0) - coldkey = decode_account_id(decoded["coldkey"]) - hotkey = decode_account_id(decoded["hotkey"]) + coldkey = decoded["coldkey"] + hotkey = decoded["hotkey"] return NeuronInfo( active=decoded["active"], axon_info=AxonInfo.from_dict( diff --git a/bittensor/core/chain_data/neuron_info_lite.py b/bittensor/core/chain_data/neuron_info_lite.py index e1d8a33048..7ab5e29f44 100644 --- a/bittensor/core/chain_data/neuron_info_lite.py +++ b/bittensor/core/chain_data/neuron_info_lite.py @@ -4,7 +4,7 @@ from bittensor.core.chain_data.axon_info import AxonInfo from bittensor.core.chain_data.info_base import InfoBase from bittensor.core.chain_data.prometheus_info import PrometheusInfo -from bittensor.core.chain_data.utils import decode_account_id, process_stake_data +from bittensor.core.chain_data.utils import process_stake_data from bittensor.utils import u16_normalized_float from bittensor.utils.balance import Balance @@ -87,8 +87,8 @@ def get_null_neuron() -> "NeuronInfoLite": @classmethod def _from_dict(cls, decoded: Any) -> "NeuronInfoLite": """Returns a NeuronInfoLite object from decoded chain data.""" - coldkey = decode_account_id(decoded["coldkey"]) - hotkey = decode_account_id(decoded["hotkey"]) + coldkey = decoded["coldkey"] + hotkey = decoded["hotkey"] stake_dict = process_stake_data(decoded["stake"]) stake = sum(stake_dict.values()) if stake_dict else Balance(0) diff --git a/bittensor/core/chain_data/proposal_vote_data.py b/bittensor/core/chain_data/proposal_vote_data.py index 3cf5439955..98e221baa5 100644 --- a/bittensor/core/chain_data/proposal_vote_data.py +++ b/bittensor/core/chain_data/proposal_vote_data.py @@ -1,7 +1,6 @@ from dataclasses import dataclass from bittensor.core.chain_data.info_base import InfoBase -from bittensor.core.chain_data.utils import decode_account_id @dataclass @@ -19,9 +18,9 @@ class ProposalVoteData(InfoBase): @classmethod def from_dict(cls, proposal_dict: dict) -> "ProposalVoteData": return cls( - ayes=[decode_account_id(key) for key in proposal_dict["ayes"]], + ayes=proposal_dict["ayes"], end=proposal_dict["end"], index=proposal_dict["index"], - nays=[decode_account_id(key) for key in proposal_dict["nays"]], + nays=proposal_dict["nays"], threshold=proposal_dict["threshold"], ) diff --git a/bittensor/core/chain_data/stake_info.py b/bittensor/core/chain_data/stake_info.py index 4f52ddfe2f..42d397c53f 100644 --- a/bittensor/core/chain_data/stake_info.py +++ b/bittensor/core/chain_data/stake_info.py @@ -1,7 +1,6 @@ from dataclasses import dataclass from bittensor.core.chain_data.info_base import InfoBase -from bittensor.core.chain_data.utils import decode_account_id from bittensor.utils.balance import Balance @@ -30,8 +29,8 @@ def from_dict(cls, decoded: dict) -> "StakeInfo": """Returns a StakeInfo object from decoded chain data.""" netuid = decoded["netuid"] return cls( - hotkey_ss58=decode_account_id(decoded["hotkey"]), - coldkey_ss58=decode_account_id(decoded["coldkey"]), + hotkey_ss58=decoded["hotkey"], + coldkey_ss58=decoded["coldkey"], netuid=int(netuid), stake=Balance.from_rao(decoded["stake"]).set_unit(netuid), locked=Balance.from_rao(decoded["locked"]).set_unit(netuid), diff --git a/bittensor/core/chain_data/subnet_info.py b/bittensor/core/chain_data/subnet_info.py index 978dab29f7..765dae6bb6 100644 --- a/bittensor/core/chain_data/subnet_info.py +++ b/bittensor/core/chain_data/subnet_info.py @@ -2,7 +2,6 @@ from typing import Any from bittensor.core.chain_data.info_base import InfoBase -from bittensor.core.chain_data.utils import decode_account_id from bittensor.utils import u16_normalized_float from bittensor.utils.balance import Balance @@ -50,7 +49,7 @@ def _from_dict(cls, decoded: Any) -> "SubnetInfo": min_allowed_weights=decoded["min_allowed_weights"], modality=decoded["network_modality"], netuid=decoded["netuid"], - owner_ss58=decode_account_id(decoded["owner"]), + owner_ss58=decoded["owner"], rho=decoded["rho"], scaling_law_power=decoded["scaling_law_power"], subnetwork_n=decoded["subnetwork_n"], diff --git a/bittensor/core/chain_data/subnet_state.py b/bittensor/core/chain_data/subnet_state.py index 95a38536e3..b554dae6f4 100644 --- a/bittensor/core/chain_data/subnet_state.py +++ b/bittensor/core/chain_data/subnet_state.py @@ -6,7 +6,6 @@ from dataclasses import dataclass from bittensor.core.chain_data.info_base import InfoBase -from bittensor.core.chain_data.utils import decode_account_id from bittensor.utils import u16_normalized_float from bittensor.utils.balance import Balance @@ -38,8 +37,8 @@ def _from_dict(cls, decoded: dict) -> "SubnetState": netuid = decoded["netuid"] return SubnetState( netuid=netuid, - hotkeys=[decode_account_id(hk) for hk in decoded.get("hotkeys", [])], - coldkeys=[decode_account_id(ck) for ck in decoded.get("coldkeys", [])], + hotkeys=decoded.get("hotkeys", []), + coldkeys=decoded.get("coldkeys", []), active=decoded["active"], validator_permit=decoded["validator_permit"], pruning_score=[ diff --git a/bittensor/core/chain_data/utils.py b/bittensor/core/chain_data/utils.py index 6c318b172b..2d991319b3 100644 --- a/bittensor/core/chain_data/utils.py +++ b/bittensor/core/chain_data/utils.py @@ -3,11 +3,9 @@ from enum import Enum from typing import Optional, Union, TYPE_CHECKING -from bittensor_wallet.utils import SS58_FORMAT from scalecodec import ScaleBytes from scalecodec.base import RuntimeConfiguration from scalecodec.type_registry import load_type_registry_preset -from scalecodec.utils.ss58 import ss58_encode from bittensor.utils.balance import Balance @@ -101,23 +99,6 @@ def from_scale_encoding_using_type_string( return obj.decode() -def decode_account_id(account_id_bytes: Union[bytes, str]) -> str: - """ - Decodes an AccountId from bytes to a Base64 string using SS58 encoding. - - Parameters: - account_id_bytes: The AccountId in bytes that needs to be decoded. - - Returns: - str: The decoded AccountId as a Base64 string. - """ - if isinstance(account_id_bytes, tuple) and isinstance(account_id_bytes[0], tuple): - account_id_bytes = account_id_bytes[0] - - # Convert the AccountId bytes to a Base64 string - return ss58_encode(bytes(account_id_bytes).hex(), SS58_FORMAT) - - def process_stake_data(stake_data: list) -> dict: """ Processes stake data to decode account IDs and convert stakes from rao to Balance objects. @@ -129,8 +110,7 @@ def process_stake_data(stake_data: list) -> dict: dict: A dictionary with account IDs as keys and their corresponding Balance objects as values. """ decoded_stake_data = {} - for account_id_bytes, stake_ in stake_data: - account_id = decode_account_id(account_id_bytes) + for account_id, stake_ in stake_data: decoded_stake_data.update({account_id: Balance.from_rao(stake_)}) return decoded_stake_data diff --git a/bittensor/core/chain_data/weight_commit_info.py b/bittensor/core/chain_data/weight_commit_info.py index 3e6f13517a..0c917c740e 100644 --- a/bittensor/core/chain_data/weight_commit_info.py +++ b/bittensor/core/chain_data/weight_commit_info.py @@ -1,5 +1,4 @@ from dataclasses import dataclass -from bittensor.core.chain_data.utils import decode_account_id from typing import Optional @@ -35,13 +34,12 @@ def from_vec_u8(cls, data: tuple) -> tuple[str, str, int]: This method is used when querying a block or block hash where storage functions `CRV3WeightCommitsV2` does not exist in Subtensor module. """ - account_id, commit_block, commit_data, round_number = data - - account_id_ = account_id[0] if isinstance(account_id, tuple) else account_id - commit_data = commit_data[0] if isinstance(commit_data, tuple) else commit_data - commit_hex = "0x" + "".join(format(x, "02x") for x in commit_data) + account_id: str + commit_hex: str + round_number: int + account_id, commit_hex, round_number = data - return decode_account_id(account_id_), commit_hex, round_number + return account_id, commit_hex, round_number @classmethod def from_vec_u8_v2(cls, data: tuple) -> tuple[str, int, str, int]: diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index e47759551f..82fef7b20b 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -38,7 +38,6 @@ SubnetIdentity, SubnetInfo, WeightCommitInfo, - decode_account_id, ) from bittensor.core.chain_data.chain_identity import ChainIdentity from bittensor.core.chain_data.utils import ( @@ -1325,8 +1324,7 @@ def get_auto_stakes( ) pairs = {} - for netuid, destination in query: - hotkey_ss58 = decode_account_id(destination.value[0]) + for netuid, hotkey_ss58 in query: if hotkey_ss58: pairs[int(netuid)] = hotkey_ss58 @@ -1560,7 +1558,7 @@ def get_children_pending( [ ( u64_normalized_float(proportion), - decode_account_id(child[0]), + child, ) for proportion, child in children ], @@ -1941,9 +1939,9 @@ def get_crowdloan_contributions( block_hash=block_hash, ) result = {} - for record in query.records: - if record[1].value: - result[decode_account_id(record[0])] = Balance.from_rao(record[1].value) + for contributor, amount in query.records: + if amount: + result[contributor] = Balance.from_rao(amount) return result def get_crowdloan_by_id( @@ -2106,8 +2104,8 @@ def get_delegate_identities( ) return { - decode_account_id(ss58_address[0]): ChainIdentity.from_dict( - decode_hex_identity_dict(identity.value), + ss58_address: ChainIdentity.from_dict( + decode_hex_identity_dict(identity), ) for ss58_address, identity in identities } @@ -2934,7 +2932,7 @@ def get_owned_hotkeys( params=[coldkey_ss58], block_hash=block_hash, ) - return [decode_account_id(hotkey[0]) for hotkey in owned_hotkeys or []] + return owned_hotkeys def get_parents( self, hotkey_ss58: str, netuid: int, block: Optional[int] = None @@ -2963,9 +2961,8 @@ def get_parents( ) if parents: formatted_parents = [] - for proportion, parent in parents.value: + for proportion, formatted_child in parents.value: # Convert U64 to int - formatted_child = decode_account_id(parent[0]) normalized_proportion = u64_normalized_float(proportion) formatted_parents.append((normalized_proportion, formatted_child)) return formatted_parents @@ -3561,10 +3558,7 @@ def get_stake_info_for_coldkeys( if query is None: return {} - return { - decode_account_id(ck): StakeInfo.list_from_dicts(st_info) - for ck, st_info in query - } + return {ck: StakeInfo.list_from_dicts(st_info) for ck, st_info in query} def get_stake_for_hotkey( self, hotkey_ss58: str, netuid: int, block: Optional[int] = None @@ -3677,7 +3671,7 @@ def get_staking_hotkeys( params=[coldkey_ss58], block_hash=self.determine_block_hash(block), ) - return [decode_account_id(hotkey[0]) for hotkey in result or []] + return result or [] def get_start_call_delay(self, block: Optional[int] = None) -> int: """ diff --git a/tests/unit_tests/test_async_subtensor.py b/tests/unit_tests/test_async_subtensor.py index 21ba4bb12b..8a24fd0d45 100644 --- a/tests/unit_tests/test_async_subtensor.py +++ b/tests/unit_tests/test_async_subtensor.py @@ -15,7 +15,6 @@ NeuronInfo, SelectiveMetagraphIndex, StakeInfo, - proposal_vote_data, ) from bittensor.core.errors import BalanceTypeError from bittensor.core.settings import DEFAULT_MEV_PROTECTION, DEFAULT_PERIOD @@ -40,35 +39,6 @@ def subtensor(mock_substrate): return async_subtensor.AsyncSubtensor() -def test_decode_ss58_tuples_in_proposal_vote_data(mocker): - """Tests that ProposalVoteData instance instantiation works properly,""" - # Preps - mocked_decode_account_id = mocker.patch.object( - proposal_vote_data, "decode_account_id" - ) - fake_proposal_dict = { - "index": "0", - "threshold": 1, - "ayes": ("0 line", "1 line"), - "nays": ("2 line", "3 line"), - "end": 123, - } - - # Call - async_subtensor.ProposalVoteData.from_dict(fake_proposal_dict) - - # Asserts - assert mocked_decode_account_id.call_count == len(fake_proposal_dict["ayes"]) + len( - fake_proposal_dict["nays"] - ) - assert mocked_decode_account_id.mock_calls == [ - mocker.call("0 line"), - mocker.call("1 line"), - mocker.call("2 line"), - mocker.call("3 line"), - ] - - def test_decode_hex_identity_dict_with_non_tuple_value(): """Tests _decode_hex_identity_dict when value is not a tuple.""" info_dict = {"info": "regular_string"} diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index 436b39b289..8a8d5a8c59 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -2009,11 +2009,6 @@ def test_does_hotkey_exist_true(mocker, subtensor): "query", return_value=mocker.Mock(value=[fake_owner]), ) - mocker.patch.object( - subtensor_module, - "decode_account_id", - return_value=fake_owner, - ) # Call result = subtensor.does_hotkey_exist(fake_hotkey_ss58, block=fake_block) @@ -2067,12 +2062,6 @@ def test_does_hotkey_exist_special_id(mocker, subtensor): "query", return_value=fake_owner, ) - mocker.patch.object( - subtensor_module, - "decode_account_id", - return_value=fake_owner, - ) - # Call result = subtensor.does_hotkey_exist(fake_hotkey_ss58, block=fake_block) @@ -2099,11 +2088,6 @@ def test_does_hotkey_exist_latest_block(mocker, subtensor): "query", return_value=mocker.Mock(value=[fake_owner]), ) - mocker.patch.object( - subtensor_module, - "decode_account_id", - return_value=fake_owner, - ) # Call result = subtensor.does_hotkey_exist(fake_hotkey_ss58) @@ -2194,11 +2178,6 @@ def test_get_hotkey_owner_does_not_exist(mocker, subtensor): mock_does_hotkey_exist = mocker.patch.object( subtensor, "does_hotkey_exist", return_value=False ) - mocker.patch.object( - subtensor_module, - "decode_account_id", - return_value=fake_hotkey_ss58, - ) # Call result = subtensor.get_hotkey_owner(fake_hotkey_ss58, block=fake_block) @@ -2980,18 +2959,11 @@ def test_get_owned_hotkeys_happy_path(subtensor, mocker): fake_coldkey = "fake_hotkey" fake_hotkey = "fake_hotkey" fake_hotkeys = [ - [ - fake_hotkey, - ] + fake_hotkey, ] mocked_subtensor = mocker.Mock(return_value=fake_hotkeys) mocker.patch.object(subtensor.substrate, "query", new=mocked_subtensor) - mocked_decode_account_id = mocker.Mock() - mocker.patch.object( - subtensor_module, "decode_account_id", new=mocked_decode_account_id - ) - # Call result = subtensor.get_owned_hotkeys(fake_coldkey) @@ -3002,8 +2974,7 @@ def test_get_owned_hotkeys_happy_path(subtensor, mocker): params=[fake_coldkey], block_hash=None, ) - assert result == [mocked_decode_account_id.return_value] - mocked_decode_account_id.assert_called_once_with(fake_hotkey) + assert result == fake_hotkeys def test_get_owned_hotkeys_return_empty(subtensor, mocker): @@ -3586,19 +3557,14 @@ def test_get_parents_success(subtensor, mocker): fake_netuid = 1 fake_parents = mocker.Mock( value=[ - (1000, ["parent_key_1"]), - (2000, ["parent_key_2"]), + (1000, "decoded_parent_key_1"), + (2000, "decoded_parent_key_2"), ] ) mocked_query = mocker.MagicMock(return_value=fake_parents) subtensor.substrate.query = mocked_query - mocked_decode_account_id = mocker.Mock( - side_effect=["decoded_parent_key_1", "decoded_parent_key_2"] - ) - mocker.patch.object(subtensor_module, "decode_account_id", mocked_decode_account_id) - expected_formatted_parents = [ (u64_normalized_float(1000), "decoded_parent_key_1"), (u64_normalized_float(2000), "decoded_parent_key_2"), @@ -3614,9 +3580,6 @@ def test_get_parents_success(subtensor, mocker): storage_function="ParentKeys", params=[fake_hotkey, fake_netuid], ) - mocked_decode_account_id.assert_has_calls( - [mocker.call("parent_key_1"), mocker.call("parent_key_2")] - ) assert result == expected_formatted_parents @@ -4358,21 +4321,12 @@ def test_get_auto_stakes(subtensor, mocker): fake_hk_1 = mocker.Mock() fake_hk_2 = mocker.Mock() - dest_value_1 = mocker.Mock(value=[fake_hk_1]) - dest_value_2 = mocker.Mock(value=[fake_hk_2]) - mock_result = mocker.MagicMock() - mock_result.__iter__.return_value = iter([(0, dest_value_1), (1, dest_value_2)]) + mock_result.__iter__.return_value = iter([(0, fake_hk_1), (1, fake_hk_2)]) mocked_query_map = mocker.patch.object( subtensor.substrate, "query_map", return_value=mock_result ) - mocked_decode_account_id = mocker.patch.object( - subtensor_module, - "decode_account_id", - side_effect=[fake_hk_1, fake_hk_2], - ) - # Call result = subtensor.get_auto_stakes(coldkey_ss58=fake_coldkey) @@ -4384,9 +4338,6 @@ def test_get_auto_stakes(subtensor, mocker): params=[fake_coldkey], block_hash=mock_determine_block_hash.return_value, ) - mocked_decode_account_id.assert_has_calls( - [mocker.call(dest_value_1.value[0]), mocker.call(dest_value_2.value[0])] - ) assert result == {0: fake_hk_1, 1: fake_hk_2} From 7ea1d1ba040722f451517ab06be647befce994fe Mon Sep 17 00:00:00 2001 From: BD Himes Date: Tue, 14 Apr 2026 17:44:32 +0200 Subject: [PATCH 052/118] Removed decode_account_id --- tests/unit_tests/test_subtensor.py | 34 +++++++++--------------------- 1 file changed, 10 insertions(+), 24 deletions(-) diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index 8a8d5a8c59..3d34d2980f 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -4676,16 +4676,13 @@ def test_get_crowdloan_constants(mocker, subtensor): def test_get_crowdloan_contributions(mocker, subtensor): """Tests subtensor `get_crowdloan_contributions` method.""" # Preps - fake_hk_array = mocker.Mock(spec=list) - fake_contribution = mocker.Mock(value=mocker.Mock(spec=Balance)) + fake_hk = mocker.Mock(spec=str) + fake_contribution = mocker.Mock(spec=int) fake_crowdloan_id = mocker.Mock(spec=int) mocked_determine_block_hash = mocker.patch.object(subtensor, "determine_block_hash") mocked_query_map = mocker.patch.object(subtensor.substrate, "query_map") - mocked_query_map.return_value.records = [(fake_hk_array, fake_contribution)] - mocked_decode_account_id = mocker.patch.object( - subtensor_module, "decode_account_id" - ) + mocked_query_map.return_value.records = [(fake_hk, fake_contribution)] mocked_from_rao = mocker.patch.object(subtensor_module.Balance, "from_rao") # Call @@ -4693,9 +4690,7 @@ def test_get_crowdloan_contributions(mocker, subtensor): # Asserts mocked_determine_block_hash.assert_called_once() - assert result == { - mocked_decode_account_id.return_value: mocked_from_rao.return_value - } + assert result == {fake_hk: mocked_from_rao.return_value} @pytest.mark.parametrize( @@ -5797,14 +5792,14 @@ def test_get_stake_info_for_coldkeys_success(subtensor, mocker): fake_coldkey_ss58s = ["coldkey1", "coldkey2"] fake_block = 123 - fake_ck1 = b"\x16:\xech\r\xde,g\x03R1\xb9\x88q\xe79\xb8\x88\x93\xae\xd2)?*\rp\xb2\xe62\xads\x1c" - fake_ck2 = b"\x17:\xech\r\xde,g\x03R1\xb9\x88q\xe79\xb8\x88\x93\xae\xd2)?*\rp\xb2\xe62\xads\x1d" - fake_decoded_ck1 = "decoded_coldkey1" - fake_decoded_ck2 = "decoded_coldkey2" + fake_ck1 = "decoded_coldkey1" + fake_ck2 = "decoded_coldkey2" + fake_decoded_ck1 = fake_ck1 + fake_decoded_ck2 = fake_ck2 stake_info_dict_1 = { "netuid": 5, - "hotkey": b"\x16:\xech\r\xde,g\x03R1\xb9\x88q\xe79\xb8\x88\x93\xae\xd2)?*\rp\xb2\xe62\xads\x1c", + "hotkey": "fake_hk", "coldkey": fake_ck1, "stake": 1000, "locked": 0, @@ -5814,7 +5809,7 @@ def test_get_stake_info_for_coldkeys_success(subtensor, mocker): } stake_info_dict_2 = { "netuid": 14, - "hotkey": b"\x17:\xech\r\xde,g\x03R1\xb9\x88q\xe79\xb8\x88\x93\xae\xd2)?*\rp\xb2\xe62\xads\x1d", + "hotkey": "fake_hk", "coldkey": fake_ck2, "stake": 2000, "locked": 0, @@ -5832,12 +5827,6 @@ def test_get_stake_info_for_coldkeys_success(subtensor, mocker): subtensor, "query_runtime_api", return_value=fake_query_result ) - mocked_decode_account_id = mocker.patch.object( - subtensor_module, - "decode_account_id", - side_effect=[fake_decoded_ck1, fake_decoded_ck2], - ) - mock_stake_info_1 = mocker.Mock(spec=StakeInfo) mock_stake_info_2 = mocker.Mock(spec=StakeInfo) mocked_stake_info_list_from_dicts = mocker.patch.object( @@ -5862,9 +5851,6 @@ def test_get_stake_info_for_coldkeys_success(subtensor, mocker): params=[fake_coldkey_ss58s], block=fake_block, ) - mocked_decode_account_id.assert_has_calls( - [mocker.call(fake_ck1), mocker.call(fake_ck2)] - ) mocked_stake_info_list_from_dicts.assert_has_calls( [mocker.call([stake_info_dict_1]), mocker.call([stake_info_dict_2])] ) From 9457319b2716ee58f1b9751e89f62f20c9606308 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Tue, 14 Apr 2026 18:46:30 +0200 Subject: [PATCH 053/118] Checkin --- bittensor/core/async_subtensor.py | 6 +----- bittensor/core/subtensor.py | 18 +++++++----------- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index 7c3525011b..0cbda79b45 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -503,12 +503,8 @@ async def _runtime_method_exists( """ runtime = await self.substrate.init_runtime(block_hash=block_hash) if runtime.metadata_v15 is not None: - metadata_v15_value = runtime.metadata_v15.value() - apis = {entry["name"]: entry for entry in metadata_v15_value["apis"]} try: - api_entry = apis[api] - methods = {entry["name"]: entry for entry in api_entry["methods"]} - _ = methods[method] + _ = runtime.runtime_api_map[method] return True except KeyError: return False diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 82fef7b20b..e5064a01af 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -394,12 +394,8 @@ def _runtime_method_exists(self, api: str, method: str, block_hash: str) -> bool """ runtime = self.substrate.init_runtime(block_hash=block_hash) if runtime.metadata_v15 is not None: - metadata_v15_value = runtime.metadata_v15.value() - apis = {entry["name"]: entry for entry in metadata_v15_value["apis"]} try: - api_entry = apis[api] - methods = {entry["name"]: entry for entry in api_entry["methods"]} - _ = methods[method] + _ = runtime.runtime_api_map[method] return True except KeyError: return False @@ -2547,10 +2543,10 @@ def get_mechanism_emission_split( params=[netuid], block_hash=block_hash, ) - if result is None or not hasattr(result, "value"): + if result is None: return None - - return [round(i / sum(result.value) * 100) for i in result.value] + total = sum(result.value) + return [round(i / total * 100) for i in result.value] def get_mechanism_count( self, @@ -2804,9 +2800,9 @@ def get_netuids_for_hotkey( ) netuids = [] if result.records: - for record in result: - if record[1].value: - netuids.append(record[0]) + for netuid, is_member in result: + if is_member: + netuids.append(netuid) return netuids def get_neuron_certificate( From 812baa22d3cdc5331b77656a1b0b7b859f4a976d Mon Sep 17 00:00:00 2001 From: BD Himes Date: Tue, 14 Apr 2026 18:50:28 +0200 Subject: [PATCH 054/118] Removed .flake8 file --- .flake8 | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 .flake8 diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 6b2eaa0333..0000000000 --- a/.flake8 +++ /dev/null @@ -1,4 +0,0 @@ -[flake8] -max-line-length = 120 -exclude = .git,__pycache__, __init__.py, docs/source/conf.py,old,build,dist,venv,.venv,.tox -select = E9,F63,F7,F82,F401 From c6308172240ac42aa1d7e7f98a3caa8b690006b9 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Tue, 14 Apr 2026 18:50:36 +0200 Subject: [PATCH 055/118] Update pyproject.toml --- pyproject.toml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index d244bea20e..706d2e0e8a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -95,3 +95,10 @@ classifiers = [ [tool.setuptools] package-dir = {"bittensor" = "bittensor"} script-files = ["bittensor/utils/certifi.sh"] + +[tool.ruff] +line-length = 120 +exclude = [".git", "__pycache__", "__init__.py", "docs/source/conf.py", "old", "build", "dist", "venv", ".venv", ".tox"] + +[tool.ruff.lint] +select = ["E9", "F63", "F7", "F82", "F401"] From 929caeb8a100c8a0ffc5c9928781f2bbfe2b43e6 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Tue, 14 Apr 2026 18:50:42 +0200 Subject: [PATCH 056/118] Update Makefile --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index f71c2c2a2b..b776718f25 100644 --- a/Makefile +++ b/Makefile @@ -35,4 +35,4 @@ check: ruff @mypy --ignore-missing-imports bittensor/ --python-version=3.12 @mypy --ignore-missing-imports bittensor/ --python-version=3.13 @mypy --ignore-missing-imports bittensor/ --python-version=3.14 - @flake8 bittensor/ --count + @python -m ruff check bittensor/ From dd8c2abbb26ce6fc93126e12e12ba5aa282087d8 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Tue, 14 Apr 2026 18:51:08 +0200 Subject: [PATCH 057/118] Renames mypy workflow --- .github/workflows/{flake8-and-mypy.yml => mypy.yml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/workflows/{flake8-and-mypy.yml => mypy.yml} (100%) diff --git a/.github/workflows/flake8-and-mypy.yml b/.github/workflows/mypy.yml similarity index 100% rename from .github/workflows/flake8-and-mypy.yml rename to .github/workflows/mypy.yml From fa9084f90db5359ba777553ad7d1a932cd4e5bb6 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Tue, 14 Apr 2026 18:52:58 +0200 Subject: [PATCH 058/118] Removes pruning_score --- bittensor/utils/mock/subtensor_mock.py | 201 +++++++------------------ 1 file changed, 52 insertions(+), 149 deletions(-) diff --git a/bittensor/utils/mock/subtensor_mock.py b/bittensor/utils/mock/subtensor_mock.py index 497a231655..cea8675a7f 100644 --- a/bittensor/utils/mock/subtensor_mock.py +++ b/bittensor/utils/mock/subtensor_mock.py @@ -95,9 +95,7 @@ class MockMapResult: def __init__( self, - records: Optional[ - list[tuple[Union[Any, MockSubtensorValue], Union[Any, MockSubtensorValue]]] - ] = None, + records: Optional[list[tuple[Union[Any, MockSubtensorValue], Union[Any, MockSubtensorValue]]]] = None, ): _records = [ ( @@ -108,10 +106,7 @@ def __init__( # Make sure record is a tuple of MockSubtensorValue (dict with value attr) if not ( isinstance(record, tuple) - and all( - isinstance(item, dict) and hasattr(item, "value") - for item in record - ) + and all(isinstance(item, dict) and hasattr(item, "value") for item in record) ) else record ) @@ -132,12 +127,8 @@ class MockSubtensorState(TypedDict): Rho: dict[int, dict[BlockNumber, int]] # netuid -> block -> rho Kappa: dict[int, dict[BlockNumber, int]] # netuid -> block -> kappa Difficulty: dict[int, dict[BlockNumber, int]] # netuid -> block -> difficulty - ImmunityPeriod: dict[ - int, dict[BlockNumber, int] - ] # netuid -> block -> immunity_period - ValidatorBatchSize: dict[ - int, dict[BlockNumber, int] - ] # netuid -> block -> validator_batch_size + ImmunityPeriod: dict[int, dict[BlockNumber, int]] # netuid -> block -> immunity_period + ValidatorBatchSize: dict[int, dict[BlockNumber, int]] # netuid -> block -> validator_batch_size Active: dict[int, dict[BlockNumber, bool]] # (netuid, uid), block -> active Stake: dict[str, dict[str, dict[int, int]]] # (hotkey, coldkey) -> block -> stake @@ -364,31 +355,24 @@ def _register_neuron(self, netuid: int, hotkey_ss58: str, coldkey: str) -> int: if netuid not in subtensor_state["NetworksAdded"]: raise Exception("Subnet does not exist") - subnetwork_n = self._get_most_recent_storage( - subtensor_state["SubnetworkN"][netuid] - ) + subnetwork_n = self._get_most_recent_storage(subtensor_state["SubnetworkN"][netuid]) if subnetwork_n > 0 and any( - self._get_most_recent_storage(subtensor_state["Keys"][netuid][uid]) - == hotkey_ss58 + self._get_most_recent_storage(subtensor_state["Keys"][netuid][uid]) == hotkey_ss58 for uid in range(subnetwork_n) ): # already_registered raise Exception("Hotkey already registered") else: # Not found - if subnetwork_n >= self._get_most_recent_storage( - subtensor_state["MaxAllowedUids"][netuid] - ): + if subnetwork_n >= self._get_most_recent_storage(subtensor_state["MaxAllowedUids"][netuid]): # Subnet full, replace neuron randomly uid = randint(0, subnetwork_n - 1) else: # Subnet not full, add new neuron # Append as next uid and increment subnetwork_n uid = subnetwork_n - subtensor_state["SubnetworkN"][netuid][self.block_number] = ( - subnetwork_n + 1 - ) + subtensor_state["SubnetworkN"][netuid][self.block_number] = subnetwork_n + 1 subtensor_state["Stake"][hotkey_ss58] = {} subtensor_state["Stake"][hotkey_ss58][coldkey] = {} @@ -407,9 +391,7 @@ def _register_neuron(self, netuid: int, hotkey_ss58: str, coldkey: str) -> int: subtensor_state["Active"][netuid][uid][self.block_number] = True subtensor_state["LastUpdate"][netuid][uid] = {} - subtensor_state["LastUpdate"][netuid][uid][self.block_number] = ( - self.block_number - ) + subtensor_state["LastUpdate"][netuid][uid][self.block_number] = self.block_number subtensor_state["Rank"][netuid][uid] = {} subtensor_state["Rank"][netuid][uid][self.block_number] = 0.0 @@ -453,9 +435,7 @@ def _register_neuron(self, netuid: int, hotkey_ss58: str, coldkey: str) -> int: if hotkey_ss58 not in subtensor_state["IsNetworkMember"]: subtensor_state["IsNetworkMember"][hotkey_ss58] = {} subtensor_state["IsNetworkMember"][hotkey_ss58][netuid] = {} - subtensor_state["IsNetworkMember"][hotkey_ss58][netuid][ - self.block_number - ] = True + subtensor_state["IsNetworkMember"][hotkey_ss58][netuid][self.block_number] = True return uid @@ -487,16 +467,12 @@ def force_register_neuron( if netuid not in subtensor_state["NetworksAdded"]: raise Exception("Subnet does not exist") - uid = self._register_neuron( - netuid=netuid, hotkey_ss58=hotkey_ss58, coldkey=coldkey_ss58 - ) + uid = self._register_neuron(netuid=netuid, hotkey_ss58=hotkey_ss58, coldkey=coldkey_ss58) subtensor_state["TotalStake"][self.block_number] = ( self._get_most_recent_storage(subtensor_state["TotalStake"]) + stake.rao ) - subtensor_state["Stake"][hotkey_ss58][coldkey_ss58][self.block_number] = ( - stake.rao - ) + subtensor_state["Stake"][hotkey_ss58][coldkey_ss58][self.block_number] = stake.rao if balance.rao > 0: self.force_set_balance(coldkey_ss58, balance) @@ -514,24 +490,17 @@ def force_set_balance( balance = self._convert_to_balance(balance) if ss58_address not in self.chain_state["System"]["Account"]: - self.chain_state["System"]["Account"][ss58_address] = { - "data": {"free": {0: 0}} - } + self.chain_state["System"]["Account"][ss58_address] = {"data": {"free": {0: 0}}} old_balance = self.get_balance(ss58_address, self.block_number) diff = balance.rao - old_balance.rao # Update total issuance self.chain_state["SubtensorModule"]["TotalIssuance"][self.block_number] = ( - self._get_most_recent_storage( - self.chain_state["SubtensorModule"]["TotalIssuance"] - ) - + diff + self._get_most_recent_storage(self.chain_state["SubtensorModule"]["TotalIssuance"]) + diff ) - self.chain_state["System"]["Account"][ss58_address] = { - "data": {"free": {self.block_number: balance.rao}} - } + self.chain_state["System"]["Account"][ss58_address] = {"data": {"free": {self.block_number: balance.rao}}} return True, None @@ -545,10 +514,7 @@ def do_block_step(self) -> None: subtensor_state = self.chain_state["SubtensorModule"] for subnet in subtensor_state["NetworksAdded"]: subtensor_state["BlocksSinceLastStep"][subnet][self.block_number] = ( - self._get_most_recent_storage( - subtensor_state["BlocksSinceLastStep"][subnet] - ) - + 1 + self._get_most_recent_storage(subtensor_state["BlocksSinceLastStep"][subnet]) + 1 ) def _handle_type_default(self, name: str, params: list[object]) -> object: @@ -601,9 +567,7 @@ def query_subtensor( while state is not None and len(params) > 0: state = state.get(params.pop(0), None) if state is None: - return SimpleNamespace( - value=self._handle_type_default(name, params) - ) + return SimpleNamespace(value=self._handle_type_default(name, params)) # Use block state_at_block = state.get(block, None) @@ -671,9 +635,7 @@ def query_map_subtensor( else: return MockMapResult([]) - def query_constant( - self, module_name: str, constant_name: str, block: Optional[int] = None - ) -> Optional[object]: + def query_constant(self, module_name: str, constant_name: str, block: Optional[int] = None) -> Optional[object]: if block is not None: if self.block_number < block: raise Exception("Cannot query block in the future") @@ -719,9 +681,7 @@ def get_balance(self, address: str, block: int = None) -> "Balance": # Use block balance_state = state["data"]["free"] - state_at_block = self._get_most_recent_storage( - balance_state, block - ) # Can be None + state_at_block = self._get_most_recent_storage(balance_state, block) # Can be None if state_at_block is not None: bal_as_int = state_at_block return Balance.from_rao(bal_as_int) @@ -732,9 +692,7 @@ def get_balance(self, address: str, block: int = None) -> "Balance": # ==== Neuron RPC methods ==== - def neuron_for_uid( - self, uid: int, netuid: int, block: Optional[int] = None - ) -> Optional[NeuronInfo]: + def neuron_for_uid(self, uid: int, netuid: int, block: Optional[int] = None) -> Optional[NeuronInfo]: if uid is None: return NeuronInfo.get_null_neuron() @@ -760,9 +718,7 @@ def neurons(self, netuid: int, block: Optional[int] = None) -> list[NeuronInfo]: raise Exception("Subnet does not exist") neurons = [] - subnet_n = self._get_most_recent_storage( - self.chain_state["SubtensorModule"]["SubnetworkN"][netuid], block - ) + subnet_n = self._get_most_recent_storage(self.chain_state["SubtensorModule"]["SubnetworkN"][netuid], block) for uid in range(subnet_n): neuron_info = self.neuron_for_uid(uid, netuid, block) if neuron_info is not None: @@ -771,9 +727,7 @@ def neurons(self, netuid: int, block: Optional[int] = None) -> list[NeuronInfo]: return neurons @staticmethod - def _get_most_recent_storage( - storage: dict[BlockNumber, Any], block_number: Optional[int] = None - ) -> Any: + def _get_most_recent_storage(storage: dict[BlockNumber, Any], block_number: Optional[int] = None) -> Any: if block_number is None: items = list(storage.items()) items.sort(key=lambda x: x[0], reverse=True) @@ -791,9 +745,7 @@ def _get_most_recent_storage( return None - def _get_axon_info( - self, netuid: int, hotkey_ss58: str, block: Optional[int] = None - ) -> AxonInfoDict: + def _get_axon_info(self, netuid: int, hotkey_ss58: str, block: Optional[int] = None) -> AxonInfoDict: # Axons [netuid][hotkey][block_number] subtensor_state = self.chain_state["SubtensorModule"] if netuid not in subtensor_state["Axons"]: @@ -802,17 +754,13 @@ def _get_axon_info( if hotkey_ss58 not in subtensor_state["Axons"][netuid]: return AxonInfoDict.default() - result = self._get_most_recent_storage( - subtensor_state["Axons"][netuid][hotkey_ss58], block - ) + result = self._get_most_recent_storage(subtensor_state["Axons"][netuid][hotkey_ss58], block) if not result: return AxonInfoDict.default() return result - def _get_prometheus_info( - self, netuid: int, hotkey_ss58: str, block: Optional[int] = None - ) -> PrometheusInfoDict: + def _get_prometheus_info(self, netuid: int, hotkey_ss58: str, block: Optional[int] = None) -> PrometheusInfoDict: subtensor_state = self.chain_state["SubtensorModule"] if netuid not in subtensor_state["Prometheus"]: return PrometheusInfoDict.default() @@ -820,17 +768,13 @@ def _get_prometheus_info( if hotkey_ss58 not in subtensor_state["Prometheus"][netuid]: return PrometheusInfoDict.default() - result = self._get_most_recent_storage( - subtensor_state["Prometheus"][netuid][hotkey_ss58], block - ) + result = self._get_most_recent_storage(subtensor_state["Prometheus"][netuid][hotkey_ss58], block) if not result: return PrometheusInfoDict.default() return result - def _neuron_subnet_exists( - self, uid: int, netuid: int, block: Optional[int] = None - ) -> Optional[NeuronInfo]: + def _neuron_subnet_exists(self, uid: int, netuid: int, block: Optional[int] = None) -> Optional[NeuronInfo]: subtensor_state = self.chain_state["SubtensorModule"] if netuid not in subtensor_state["NetworksAdded"]: return None @@ -847,53 +791,22 @@ def _neuron_subnet_exists( prometheus_info = self._get_prometheus_info(netuid, hotkey, block) coldkey = self._get_most_recent_storage(subtensor_state["Owner"][hotkey], block) - active = self._get_most_recent_storage( - subtensor_state["Active"][netuid][uid], block - ) - rank = self._get_most_recent_storage( - subtensor_state["Rank"][netuid][uid], block - ) - emission = self._get_most_recent_storage( - subtensor_state["Emission"][netuid][uid], block - ) - incentive = self._get_most_recent_storage( - subtensor_state["Incentive"][netuid][uid], block - ) - consensus = self._get_most_recent_storage( - subtensor_state["Consensus"][netuid][uid], block - ) - trust = self._get_most_recent_storage( - subtensor_state["Trust"][netuid][uid], block - ) - validator_trust = self._get_most_recent_storage( - subtensor_state["ValidatorTrust"][netuid][uid], block - ) - dividends = self._get_most_recent_storage( - subtensor_state["Dividends"][netuid][uid], block - ) - pruning_score = self._get_most_recent_storage( # noqa - subtensor_state["PruningScores"][netuid][uid], block - ) - last_update = self._get_most_recent_storage( - subtensor_state["LastUpdate"][netuid][uid], block - ) - validator_permit = self._get_most_recent_storage( - subtensor_state["ValidatorPermit"][netuid][uid], block - ) - - weights = self._get_most_recent_storage( - subtensor_state["Weights"][netuid][uid], block - ) - bonds = self._get_most_recent_storage( - subtensor_state["Bonds"][netuid][uid], block - ) + active = self._get_most_recent_storage(subtensor_state["Active"][netuid][uid], block) + rank = self._get_most_recent_storage(subtensor_state["Rank"][netuid][uid], block) + emission = self._get_most_recent_storage(subtensor_state["Emission"][netuid][uid], block) + incentive = self._get_most_recent_storage(subtensor_state["Incentive"][netuid][uid], block) + consensus = self._get_most_recent_storage(subtensor_state["Consensus"][netuid][uid], block) + trust = self._get_most_recent_storage(subtensor_state["Trust"][netuid][uid], block) + validator_trust = self._get_most_recent_storage(subtensor_state["ValidatorTrust"][netuid][uid], block) + dividends = self._get_most_recent_storage(subtensor_state["Dividends"][netuid][uid], block) + last_update = self._get_most_recent_storage(subtensor_state["LastUpdate"][netuid][uid], block) + validator_permit = self._get_most_recent_storage(subtensor_state["ValidatorPermit"][netuid][uid], block) + + weights = self._get_most_recent_storage(subtensor_state["Weights"][netuid][uid], block) + bonds = self._get_most_recent_storage(subtensor_state["Bonds"][netuid][uid], block) stake_dict = { - coldkey: Balance.from_rao( - self._get_most_recent_storage( - subtensor_state["Stake"][hotkey][coldkey], block - ) - ) + coldkey: Balance.from_rao(self._get_most_recent_storage(subtensor_state["Stake"][hotkey][coldkey], block)) for coldkey in subtensor_state["Stake"][hotkey] } @@ -909,9 +822,7 @@ def _neuron_subnet_exists( validator_trust = u16_normalized_float(validator_trust) dividends = u16_normalized_float(dividends) prometheus_info = PrometheusInfo.from_dict(prometheus_info) - axon_info_ = AxonInfo.from_neuron_info( - {"hotkey": hotkey, "coldkey": coldkey, "axon_info": axon_info_} - ) + axon_info_ = AxonInfo.from_neuron_info({"hotkey": hotkey, "coldkey": coldkey, "axon_info": axon_info_}) neuron_info = NeuronInfo( hotkey=hotkey, @@ -938,16 +849,12 @@ def _neuron_subnet_exists( return neuron_info - def neurons_lite( - self, netuid: int, block: Optional[int] = None - ) -> list[NeuronInfoLite]: + def neurons_lite(self, netuid: int, block: Optional[int] = None) -> list[NeuronInfoLite]: if netuid not in self.chain_state["SubtensorModule"]["NetworksAdded"]: raise Exception("Subnet does not exist") neurons = [] - subnet_n = self._get_most_recent_storage( - self.chain_state["SubtensorModule"]["SubnetworkN"][netuid] - ) + subnet_n = self._get_most_recent_storage(self.chain_state["SubtensorModule"]["SubnetworkN"][netuid]) for uid in range(subnet_n): neuron_info = self.neuron_for_uid_lite(uid, netuid, block) if neuron_info is not None: @@ -955,9 +862,7 @@ def neurons_lite( return neurons - def neuron_for_uid_lite( - self, uid: int, netuid: int, block: Optional[int] = None - ) -> Optional[NeuronInfoLite]: + def neuron_for_uid_lite(self, uid: int, netuid: int, block: Optional[int] = None) -> Optional[NeuronInfoLite]: if uid is None: return NeuronInfoLite.get_null_neuron() @@ -1000,9 +905,7 @@ def neuron_for_uid_lite( is_null=neuron_info.is_null, ) - def get_transfer_fee( - self, wallet: "Wallet", dest: str, value: Union["Balance", float, int] - ) -> "Balance": + def get_transfer_fee(self, wallet: "Wallet", dest: str, value: Union["Balance", float, int]) -> "Balance": return Balance(700) def do_transfer( @@ -1023,17 +926,17 @@ def do_transfer( raise Exception("Insufficient balance") # Remove from the free balance - self.chain_state["System"]["Account"][wallet.coldkeypub.ss58_address]["data"][ - "free" - ][self.block_number] = (bal - transfer_balance - transfer_fee).rao + self.chain_state["System"]["Account"][wallet.coldkeypub.ss58_address]["data"]["free"][self.block_number] = ( + bal - transfer_balance - transfer_fee + ).rao # Add to the free balance if dest not in self.chain_state["System"]["Account"]: self.chain_state["System"]["Account"][dest] = {"data": {"free": {}}} - self.chain_state["System"]["Account"][dest]["data"]["free"][ - self.block_number - ] = (dest_bal + transfer_balance).rao + self.chain_state["System"]["Account"][dest]["data"]["free"][self.block_number] = ( + dest_bal + transfer_balance + ).rao return True, None, None From 51885634c170ed6f7026435b1518aa279310ef4d Mon Sep 17 00:00:00 2001 From: BD Himes Date: Tue, 14 Apr 2026 18:55:47 +0200 Subject: [PATCH 059/118] Update pyproject.toml --- pyproject.toml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 706d2e0e8a..21579e3984 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ version = "10.2.0" description = "Bittensor SDK" readme = "README.md" authors = [ - {name = "bittensor.com"} + { name = "bittensor.com" } ] license = { file = "LICENSE" } requires-python = ">=3.10,<3.15" @@ -93,11 +93,10 @@ classifiers = [ ] [tool.setuptools] -package-dir = {"bittensor" = "bittensor"} +package-dir = { "bittensor" = "bittensor" } script-files = ["bittensor/utils/certifi.sh"] [tool.ruff] -line-length = 120 exclude = [".git", "__pycache__", "__init__.py", "docs/source/conf.py", "old", "build", "dist", "venv", ".venv", ".tox"] [tool.ruff.lint] From 84d9616907a4db3b58fc04eb4366a6256e5c74a3 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Tue, 14 Apr 2026 18:56:15 +0200 Subject: [PATCH 060/118] Remove unused imports --- bittensor/utils/mock/subtensor_mock.py | 198 +++++++++++++----- tests/consistency/test_proxy_types.py | 1 - tests/e2e_tests/test_crowdloan.py | 1 - tests/e2e_tests/test_delegate.py | 6 - tests/e2e_tests/test_incentive.py | 1 - tests/e2e_tests/test_metagraph.py | 1 - tests/e2e_tests/test_subnets.py | 1 - tests/e2e_tests/test_transfer.py | 3 +- tests/e2e_tests/utils/e2e_test_utils.py | 1 - .../extrinsics/asyncex/test_coldkey_swap.py | 3 - .../extrinsics/asyncex/test_weights.py | 1 - .../unit_tests/extrinsics/test_mev_shield.py | 1 - tests/unit_tests/extrinsics/test_transfer.py | 1 - tests/unit_tests/test_stream.py | 4 +- 14 files changed, 148 insertions(+), 75 deletions(-) diff --git a/bittensor/utils/mock/subtensor_mock.py b/bittensor/utils/mock/subtensor_mock.py index cea8675a7f..832d24a2c0 100644 --- a/bittensor/utils/mock/subtensor_mock.py +++ b/bittensor/utils/mock/subtensor_mock.py @@ -95,7 +95,9 @@ class MockMapResult: def __init__( self, - records: Optional[list[tuple[Union[Any, MockSubtensorValue], Union[Any, MockSubtensorValue]]]] = None, + records: Optional[ + list[tuple[Union[Any, MockSubtensorValue], Union[Any, MockSubtensorValue]]] + ] = None, ): _records = [ ( @@ -106,7 +108,10 @@ def __init__( # Make sure record is a tuple of MockSubtensorValue (dict with value attr) if not ( isinstance(record, tuple) - and all(isinstance(item, dict) and hasattr(item, "value") for item in record) + and all( + isinstance(item, dict) and hasattr(item, "value") + for item in record + ) ) else record ) @@ -127,8 +132,12 @@ class MockSubtensorState(TypedDict): Rho: dict[int, dict[BlockNumber, int]] # netuid -> block -> rho Kappa: dict[int, dict[BlockNumber, int]] # netuid -> block -> kappa Difficulty: dict[int, dict[BlockNumber, int]] # netuid -> block -> difficulty - ImmunityPeriod: dict[int, dict[BlockNumber, int]] # netuid -> block -> immunity_period - ValidatorBatchSize: dict[int, dict[BlockNumber, int]] # netuid -> block -> validator_batch_size + ImmunityPeriod: dict[ + int, dict[BlockNumber, int] + ] # netuid -> block -> immunity_period + ValidatorBatchSize: dict[ + int, dict[BlockNumber, int] + ] # netuid -> block -> validator_batch_size Active: dict[int, dict[BlockNumber, bool]] # (netuid, uid), block -> active Stake: dict[str, dict[str, dict[int, int]]] # (hotkey, coldkey) -> block -> stake @@ -355,24 +364,31 @@ def _register_neuron(self, netuid: int, hotkey_ss58: str, coldkey: str) -> int: if netuid not in subtensor_state["NetworksAdded"]: raise Exception("Subnet does not exist") - subnetwork_n = self._get_most_recent_storage(subtensor_state["SubnetworkN"][netuid]) + subnetwork_n = self._get_most_recent_storage( + subtensor_state["SubnetworkN"][netuid] + ) if subnetwork_n > 0 and any( - self._get_most_recent_storage(subtensor_state["Keys"][netuid][uid]) == hotkey_ss58 + self._get_most_recent_storage(subtensor_state["Keys"][netuid][uid]) + == hotkey_ss58 for uid in range(subnetwork_n) ): # already_registered raise Exception("Hotkey already registered") else: # Not found - if subnetwork_n >= self._get_most_recent_storage(subtensor_state["MaxAllowedUids"][netuid]): + if subnetwork_n >= self._get_most_recent_storage( + subtensor_state["MaxAllowedUids"][netuid] + ): # Subnet full, replace neuron randomly uid = randint(0, subnetwork_n - 1) else: # Subnet not full, add new neuron # Append as next uid and increment subnetwork_n uid = subnetwork_n - subtensor_state["SubnetworkN"][netuid][self.block_number] = subnetwork_n + 1 + subtensor_state["SubnetworkN"][netuid][self.block_number] = ( + subnetwork_n + 1 + ) subtensor_state["Stake"][hotkey_ss58] = {} subtensor_state["Stake"][hotkey_ss58][coldkey] = {} @@ -391,7 +407,9 @@ def _register_neuron(self, netuid: int, hotkey_ss58: str, coldkey: str) -> int: subtensor_state["Active"][netuid][uid][self.block_number] = True subtensor_state["LastUpdate"][netuid][uid] = {} - subtensor_state["LastUpdate"][netuid][uid][self.block_number] = self.block_number + subtensor_state["LastUpdate"][netuid][uid][self.block_number] = ( + self.block_number + ) subtensor_state["Rank"][netuid][uid] = {} subtensor_state["Rank"][netuid][uid][self.block_number] = 0.0 @@ -435,7 +453,9 @@ def _register_neuron(self, netuid: int, hotkey_ss58: str, coldkey: str) -> int: if hotkey_ss58 not in subtensor_state["IsNetworkMember"]: subtensor_state["IsNetworkMember"][hotkey_ss58] = {} subtensor_state["IsNetworkMember"][hotkey_ss58][netuid] = {} - subtensor_state["IsNetworkMember"][hotkey_ss58][netuid][self.block_number] = True + subtensor_state["IsNetworkMember"][hotkey_ss58][netuid][ + self.block_number + ] = True return uid @@ -467,12 +487,16 @@ def force_register_neuron( if netuid not in subtensor_state["NetworksAdded"]: raise Exception("Subnet does not exist") - uid = self._register_neuron(netuid=netuid, hotkey_ss58=hotkey_ss58, coldkey=coldkey_ss58) + uid = self._register_neuron( + netuid=netuid, hotkey_ss58=hotkey_ss58, coldkey=coldkey_ss58 + ) subtensor_state["TotalStake"][self.block_number] = ( self._get_most_recent_storage(subtensor_state["TotalStake"]) + stake.rao ) - subtensor_state["Stake"][hotkey_ss58][coldkey_ss58][self.block_number] = stake.rao + subtensor_state["Stake"][hotkey_ss58][coldkey_ss58][self.block_number] = ( + stake.rao + ) if balance.rao > 0: self.force_set_balance(coldkey_ss58, balance) @@ -490,17 +514,24 @@ def force_set_balance( balance = self._convert_to_balance(balance) if ss58_address not in self.chain_state["System"]["Account"]: - self.chain_state["System"]["Account"][ss58_address] = {"data": {"free": {0: 0}}} + self.chain_state["System"]["Account"][ss58_address] = { + "data": {"free": {0: 0}} + } old_balance = self.get_balance(ss58_address, self.block_number) diff = balance.rao - old_balance.rao # Update total issuance self.chain_state["SubtensorModule"]["TotalIssuance"][self.block_number] = ( - self._get_most_recent_storage(self.chain_state["SubtensorModule"]["TotalIssuance"]) + diff + self._get_most_recent_storage( + self.chain_state["SubtensorModule"]["TotalIssuance"] + ) + + diff ) - self.chain_state["System"]["Account"][ss58_address] = {"data": {"free": {self.block_number: balance.rao}}} + self.chain_state["System"]["Account"][ss58_address] = { + "data": {"free": {self.block_number: balance.rao}} + } return True, None @@ -514,7 +545,10 @@ def do_block_step(self) -> None: subtensor_state = self.chain_state["SubtensorModule"] for subnet in subtensor_state["NetworksAdded"]: subtensor_state["BlocksSinceLastStep"][subnet][self.block_number] = ( - self._get_most_recent_storage(subtensor_state["BlocksSinceLastStep"][subnet]) + 1 + self._get_most_recent_storage( + subtensor_state["BlocksSinceLastStep"][subnet] + ) + + 1 ) def _handle_type_default(self, name: str, params: list[object]) -> object: @@ -567,7 +601,9 @@ def query_subtensor( while state is not None and len(params) > 0: state = state.get(params.pop(0), None) if state is None: - return SimpleNamespace(value=self._handle_type_default(name, params)) + return SimpleNamespace( + value=self._handle_type_default(name, params) + ) # Use block state_at_block = state.get(block, None) @@ -635,7 +671,9 @@ def query_map_subtensor( else: return MockMapResult([]) - def query_constant(self, module_name: str, constant_name: str, block: Optional[int] = None) -> Optional[object]: + def query_constant( + self, module_name: str, constant_name: str, block: Optional[int] = None + ) -> Optional[object]: if block is not None: if self.block_number < block: raise Exception("Cannot query block in the future") @@ -681,7 +719,9 @@ def get_balance(self, address: str, block: int = None) -> "Balance": # Use block balance_state = state["data"]["free"] - state_at_block = self._get_most_recent_storage(balance_state, block) # Can be None + state_at_block = self._get_most_recent_storage( + balance_state, block + ) # Can be None if state_at_block is not None: bal_as_int = state_at_block return Balance.from_rao(bal_as_int) @@ -692,7 +732,9 @@ def get_balance(self, address: str, block: int = None) -> "Balance": # ==== Neuron RPC methods ==== - def neuron_for_uid(self, uid: int, netuid: int, block: Optional[int] = None) -> Optional[NeuronInfo]: + def neuron_for_uid( + self, uid: int, netuid: int, block: Optional[int] = None + ) -> Optional[NeuronInfo]: if uid is None: return NeuronInfo.get_null_neuron() @@ -718,7 +760,9 @@ def neurons(self, netuid: int, block: Optional[int] = None) -> list[NeuronInfo]: raise Exception("Subnet does not exist") neurons = [] - subnet_n = self._get_most_recent_storage(self.chain_state["SubtensorModule"]["SubnetworkN"][netuid], block) + subnet_n = self._get_most_recent_storage( + self.chain_state["SubtensorModule"]["SubnetworkN"][netuid], block + ) for uid in range(subnet_n): neuron_info = self.neuron_for_uid(uid, netuid, block) if neuron_info is not None: @@ -727,7 +771,9 @@ def neurons(self, netuid: int, block: Optional[int] = None) -> list[NeuronInfo]: return neurons @staticmethod - def _get_most_recent_storage(storage: dict[BlockNumber, Any], block_number: Optional[int] = None) -> Any: + def _get_most_recent_storage( + storage: dict[BlockNumber, Any], block_number: Optional[int] = None + ) -> Any: if block_number is None: items = list(storage.items()) items.sort(key=lambda x: x[0], reverse=True) @@ -745,7 +791,9 @@ def _get_most_recent_storage(storage: dict[BlockNumber, Any], block_number: Opti return None - def _get_axon_info(self, netuid: int, hotkey_ss58: str, block: Optional[int] = None) -> AxonInfoDict: + def _get_axon_info( + self, netuid: int, hotkey_ss58: str, block: Optional[int] = None + ) -> AxonInfoDict: # Axons [netuid][hotkey][block_number] subtensor_state = self.chain_state["SubtensorModule"] if netuid not in subtensor_state["Axons"]: @@ -754,13 +802,17 @@ def _get_axon_info(self, netuid: int, hotkey_ss58: str, block: Optional[int] = N if hotkey_ss58 not in subtensor_state["Axons"][netuid]: return AxonInfoDict.default() - result = self._get_most_recent_storage(subtensor_state["Axons"][netuid][hotkey_ss58], block) + result = self._get_most_recent_storage( + subtensor_state["Axons"][netuid][hotkey_ss58], block + ) if not result: return AxonInfoDict.default() return result - def _get_prometheus_info(self, netuid: int, hotkey_ss58: str, block: Optional[int] = None) -> PrometheusInfoDict: + def _get_prometheus_info( + self, netuid: int, hotkey_ss58: str, block: Optional[int] = None + ) -> PrometheusInfoDict: subtensor_state = self.chain_state["SubtensorModule"] if netuid not in subtensor_state["Prometheus"]: return PrometheusInfoDict.default() @@ -768,13 +820,17 @@ def _get_prometheus_info(self, netuid: int, hotkey_ss58: str, block: Optional[in if hotkey_ss58 not in subtensor_state["Prometheus"][netuid]: return PrometheusInfoDict.default() - result = self._get_most_recent_storage(subtensor_state["Prometheus"][netuid][hotkey_ss58], block) + result = self._get_most_recent_storage( + subtensor_state["Prometheus"][netuid][hotkey_ss58], block + ) if not result: return PrometheusInfoDict.default() return result - def _neuron_subnet_exists(self, uid: int, netuid: int, block: Optional[int] = None) -> Optional[NeuronInfo]: + def _neuron_subnet_exists( + self, uid: int, netuid: int, block: Optional[int] = None + ) -> Optional[NeuronInfo]: subtensor_state = self.chain_state["SubtensorModule"] if netuid not in subtensor_state["NetworksAdded"]: return None @@ -791,22 +847,50 @@ def _neuron_subnet_exists(self, uid: int, netuid: int, block: Optional[int] = No prometheus_info = self._get_prometheus_info(netuid, hotkey, block) coldkey = self._get_most_recent_storage(subtensor_state["Owner"][hotkey], block) - active = self._get_most_recent_storage(subtensor_state["Active"][netuid][uid], block) - rank = self._get_most_recent_storage(subtensor_state["Rank"][netuid][uid], block) - emission = self._get_most_recent_storage(subtensor_state["Emission"][netuid][uid], block) - incentive = self._get_most_recent_storage(subtensor_state["Incentive"][netuid][uid], block) - consensus = self._get_most_recent_storage(subtensor_state["Consensus"][netuid][uid], block) - trust = self._get_most_recent_storage(subtensor_state["Trust"][netuid][uid], block) - validator_trust = self._get_most_recent_storage(subtensor_state["ValidatorTrust"][netuid][uid], block) - dividends = self._get_most_recent_storage(subtensor_state["Dividends"][netuid][uid], block) - last_update = self._get_most_recent_storage(subtensor_state["LastUpdate"][netuid][uid], block) - validator_permit = self._get_most_recent_storage(subtensor_state["ValidatorPermit"][netuid][uid], block) - - weights = self._get_most_recent_storage(subtensor_state["Weights"][netuid][uid], block) - bonds = self._get_most_recent_storage(subtensor_state["Bonds"][netuid][uid], block) + active = self._get_most_recent_storage( + subtensor_state["Active"][netuid][uid], block + ) + rank = self._get_most_recent_storage( + subtensor_state["Rank"][netuid][uid], block + ) + emission = self._get_most_recent_storage( + subtensor_state["Emission"][netuid][uid], block + ) + incentive = self._get_most_recent_storage( + subtensor_state["Incentive"][netuid][uid], block + ) + consensus = self._get_most_recent_storage( + subtensor_state["Consensus"][netuid][uid], block + ) + trust = self._get_most_recent_storage( + subtensor_state["Trust"][netuid][uid], block + ) + validator_trust = self._get_most_recent_storage( + subtensor_state["ValidatorTrust"][netuid][uid], block + ) + dividends = self._get_most_recent_storage( + subtensor_state["Dividends"][netuid][uid], block + ) + last_update = self._get_most_recent_storage( + subtensor_state["LastUpdate"][netuid][uid], block + ) + validator_permit = self._get_most_recent_storage( + subtensor_state["ValidatorPermit"][netuid][uid], block + ) + + weights = self._get_most_recent_storage( + subtensor_state["Weights"][netuid][uid], block + ) + bonds = self._get_most_recent_storage( + subtensor_state["Bonds"][netuid][uid], block + ) stake_dict = { - coldkey: Balance.from_rao(self._get_most_recent_storage(subtensor_state["Stake"][hotkey][coldkey], block)) + coldkey: Balance.from_rao( + self._get_most_recent_storage( + subtensor_state["Stake"][hotkey][coldkey], block + ) + ) for coldkey in subtensor_state["Stake"][hotkey] } @@ -822,7 +906,9 @@ def _neuron_subnet_exists(self, uid: int, netuid: int, block: Optional[int] = No validator_trust = u16_normalized_float(validator_trust) dividends = u16_normalized_float(dividends) prometheus_info = PrometheusInfo.from_dict(prometheus_info) - axon_info_ = AxonInfo.from_neuron_info({"hotkey": hotkey, "coldkey": coldkey, "axon_info": axon_info_}) + axon_info_ = AxonInfo.from_neuron_info( + {"hotkey": hotkey, "coldkey": coldkey, "axon_info": axon_info_} + ) neuron_info = NeuronInfo( hotkey=hotkey, @@ -849,12 +935,16 @@ def _neuron_subnet_exists(self, uid: int, netuid: int, block: Optional[int] = No return neuron_info - def neurons_lite(self, netuid: int, block: Optional[int] = None) -> list[NeuronInfoLite]: + def neurons_lite( + self, netuid: int, block: Optional[int] = None + ) -> list[NeuronInfoLite]: if netuid not in self.chain_state["SubtensorModule"]["NetworksAdded"]: raise Exception("Subnet does not exist") neurons = [] - subnet_n = self._get_most_recent_storage(self.chain_state["SubtensorModule"]["SubnetworkN"][netuid]) + subnet_n = self._get_most_recent_storage( + self.chain_state["SubtensorModule"]["SubnetworkN"][netuid] + ) for uid in range(subnet_n): neuron_info = self.neuron_for_uid_lite(uid, netuid, block) if neuron_info is not None: @@ -862,7 +952,9 @@ def neurons_lite(self, netuid: int, block: Optional[int] = None) -> list[NeuronI return neurons - def neuron_for_uid_lite(self, uid: int, netuid: int, block: Optional[int] = None) -> Optional[NeuronInfoLite]: + def neuron_for_uid_lite( + self, uid: int, netuid: int, block: Optional[int] = None + ) -> Optional[NeuronInfoLite]: if uid is None: return NeuronInfoLite.get_null_neuron() @@ -905,7 +997,9 @@ def neuron_for_uid_lite(self, uid: int, netuid: int, block: Optional[int] = None is_null=neuron_info.is_null, ) - def get_transfer_fee(self, wallet: "Wallet", dest: str, value: Union["Balance", float, int]) -> "Balance": + def get_transfer_fee( + self, wallet: "Wallet", dest: str, value: Union["Balance", float, int] + ) -> "Balance": return Balance(700) def do_transfer( @@ -926,17 +1020,17 @@ def do_transfer( raise Exception("Insufficient balance") # Remove from the free balance - self.chain_state["System"]["Account"][wallet.coldkeypub.ss58_address]["data"]["free"][self.block_number] = ( - bal - transfer_balance - transfer_fee - ).rao + self.chain_state["System"]["Account"][wallet.coldkeypub.ss58_address]["data"][ + "free" + ][self.block_number] = (bal - transfer_balance - transfer_fee).rao # Add to the free balance if dest not in self.chain_state["System"]["Account"]: self.chain_state["System"]["Account"][dest] = {"data": {"free": {}}} - self.chain_state["System"]["Account"][dest]["data"]["free"][self.block_number] = ( - dest_bal + transfer_balance - ).rao + self.chain_state["System"]["Account"][dest]["data"]["free"][ + self.block_number + ] = (dest_bal + transfer_balance).rao return True, None, None diff --git a/tests/consistency/test_proxy_types.py b/tests/consistency/test_proxy_types.py index ab9462f8bc..5bb3e8be6b 100644 --- a/tests/consistency/test_proxy_types.py +++ b/tests/consistency/test_proxy_types.py @@ -1,5 +1,4 @@ from bittensor.core.chain_data.proxy import ProxyType -from bittensor.core.extrinsics.pallets import SubtensorModule, Proxy, Balances def get_proxy_type_fields(meta): diff --git a/tests/e2e_tests/test_crowdloan.py b/tests/e2e_tests/test_crowdloan.py index 52b99bfc9c..b771d36609 100644 --- a/tests/e2e_tests/test_crowdloan.py +++ b/tests/e2e_tests/test_crowdloan.py @@ -1,6 +1,5 @@ from bittensor import Balance from bittensor.core.extrinsics.pallets import SubtensorModule -from bittensor_wallet import Wallet import pytest import asyncio diff --git a/tests/e2e_tests/test_delegate.py b/tests/e2e_tests/test_delegate.py index d0a4a6fb95..07a4d67402 100644 --- a/tests/e2e_tests/test_delegate.py +++ b/tests/e2e_tests/test_delegate.py @@ -2,7 +2,6 @@ from bittensor.core.chain_data.chain_identity import ChainIdentity from bittensor.core.chain_data.delegate_info import DelegatedInfo, DelegateInfo -from bittensor.core.chain_data.proposal_vote_data import ProposalVoteData from bittensor.core.errors import ( DelegateTakeTooHigh, DelegateTxRateLimitExceeded, @@ -11,13 +10,9 @@ ) from bittensor.utils.balance import Balance from tests.e2e_tests.utils import ( - async_propose, async_set_identity, - async_vote, get_dynamic_balance, - propose, set_identity, - vote, TestSubnet, AdminUtils, ACTIVATE_SUBNET, @@ -26,7 +21,6 @@ SUDO_SET_NOMINATOR_MIN_REQUIRED_STAKE, SUDO_SET_TX_DELEGATE_TAKE_RATE_LIMIT, ) -from tests.helpers.helpers import CloseInValue DEFAULT_DELEGATE_TAKE = 0.179995422293431 diff --git a/tests/e2e_tests/test_incentive.py b/tests/e2e_tests/test_incentive.py index 64d5b97268..e02455e166 100644 --- a/tests/e2e_tests/test_incentive.py +++ b/tests/e2e_tests/test_incentive.py @@ -3,7 +3,6 @@ import pytest from bittensor.utils.balance import Balance -from bittensor.utils.btlogging import logging from tests.e2e_tests.utils import ( TestSubnet, AdminUtils, diff --git a/tests/e2e_tests/test_metagraph.py b/tests/e2e_tests/test_metagraph.py index 102899bae5..6606ec62c5 100644 --- a/tests/e2e_tests/test_metagraph.py +++ b/tests/e2e_tests/test_metagraph.py @@ -14,7 +14,6 @@ from bittensor.utils.balance import Balance from bittensor.utils.btlogging import logging from bittensor.utils.registration.pow import LazyLoadedTorch -from bittensor.utils.weight_utils import convert_and_normalize_weights_and_uids from tests.e2e_tests.utils import ( AdminUtils, NETUID, diff --git a/tests/e2e_tests/test_subnets.py b/tests/e2e_tests/test_subnets.py index 7079073d82..5368988946 100644 --- a/tests/e2e_tests/test_subnets.py +++ b/tests/e2e_tests/test_subnets.py @@ -1,5 +1,4 @@ import pytest -from bittensor.utils.btlogging import logging def test_subnets(subtensor, alice_wallet): diff --git a/tests/e2e_tests/test_transfer.py b/tests/e2e_tests/test_transfer.py index b6859238e2..136fb98bba 100644 --- a/tests/e2e_tests/test_transfer.py +++ b/tests/e2e_tests/test_transfer.py @@ -4,10 +4,9 @@ import pytest from bittensor.utils.balance import Balance -from bittensor import logging if typing.TYPE_CHECKING: - from bittensor.extras import SubtensorApi + pass def test_transfer(subtensor, alice_wallet): diff --git a/tests/e2e_tests/utils/e2e_test_utils.py b/tests/e2e_tests/utils/e2e_test_utils.py index dd31553524..2e7239bffe 100644 --- a/tests/e2e_tests/utils/e2e_test_utils.py +++ b/tests/e2e_tests/utils/e2e_test_utils.py @@ -6,7 +6,6 @@ from typing import Optional from bittensor_wallet import Keypair, Wallet -from bittensor.extras import SubtensorApi from bittensor.utils.btlogging import logging template_path = os.getcwd() + "/neurons/" diff --git a/tests/unit_tests/extrinsics/asyncex/test_coldkey_swap.py b/tests/unit_tests/extrinsics/asyncex/test_coldkey_swap.py index 0f969281bd..6611134d52 100644 --- a/tests/unit_tests/extrinsics/asyncex/test_coldkey_swap.py +++ b/tests/unit_tests/extrinsics/asyncex/test_coldkey_swap.py @@ -1,10 +1,7 @@ import pytest from bittensor_wallet import Wallet -from scalecodec.types import GenericCall from bittensor.core.extrinsics.asyncex import coldkey_swap -from bittensor.core.extrinsics.pallets import SubtensorModule -from bittensor.core.settings import DEFAULT_MEV_PROTECTION from bittensor.core.types import ExtrinsicResponse from bittensor.core.chain_data.coldkey_swap import ColdkeySwapAnnouncementInfo diff --git a/tests/unit_tests/extrinsics/asyncex/test_weights.py b/tests/unit_tests/extrinsics/asyncex/test_weights.py index 649dc79b15..c365bf4ed5 100644 --- a/tests/unit_tests/extrinsics/asyncex/test_weights.py +++ b/tests/unit_tests/extrinsics/asyncex/test_weights.py @@ -1,7 +1,6 @@ import pytest from bittensor.core.extrinsics.asyncex import weights as weights_module -from bittensor.core.settings import version_as_int from bittensor.core.types import ExtrinsicResponse diff --git a/tests/unit_tests/extrinsics/test_mev_shield.py b/tests/unit_tests/extrinsics/test_mev_shield.py index 2c0fba851b..549c3d55b9 100644 --- a/tests/unit_tests/extrinsics/test_mev_shield.py +++ b/tests/unit_tests/extrinsics/test_mev_shield.py @@ -1,4 +1,3 @@ -from bittensor_wallet import Wallet from scalecodec.types import GenericCall from async_substrate_interface import ExtrinsicReceipt from async_substrate_interface.errors import SubstrateRequestException diff --git a/tests/unit_tests/extrinsics/test_transfer.py b/tests/unit_tests/extrinsics/test_transfer.py index e6f560c388..4ddd3031bd 100644 --- a/tests/unit_tests/extrinsics/test_transfer.py +++ b/tests/unit_tests/extrinsics/test_transfer.py @@ -1,4 +1,3 @@ -import pytest from bittensor.core.extrinsics import transfer from bittensor.core.settings import DEFAULT_MEV_PROTECTION from bittensor.utils.balance import Balance diff --git a/tests/unit_tests/test_stream.py b/tests/unit_tests/test_stream.py index be333d59b3..b40a51b373 100644 --- a/tests/unit_tests/test_stream.py +++ b/tests/unit_tests/test_stream.py @@ -5,9 +5,7 @@ """ import pytest -from abc import ABC -from typing import Optional -from unittest.mock import AsyncMock, Mock, MagicMock, patch +from unittest.mock import AsyncMock, Mock from aiohttp import ClientResponse from starlette.types import Send, Receive, Scope From b846bb61949726eeeabaa72bda4409b62fc1bda6 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Tue, 14 Apr 2026 21:16:01 +0200 Subject: [PATCH 061/118] Types --- bittensor/core/async_subtensor.py | 2 +- bittensor/core/axon.py | 3 +- bittensor/core/subtensor.py | 69 +++++++++++++++---------------- bittensor/core/types.py | 12 ++++++ bittensor/utils/balance.py | 18 +++----- bittensor/utils/liquidity.py | 4 +- pyproject.toml | 2 +- 7 files changed, 56 insertions(+), 54 deletions(-) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index 0cbda79b45..d9e4ef51e2 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -13,6 +13,7 @@ from bittensor_wallet.utils import SS58_FORMAT from scalecodec import GenericCall from scalecodec.base import ScaleType +from scalecodec.utils.math import FixedPoint from bittensor.core.chain_data import ( ColdkeySwapAnnouncementInfo, @@ -160,7 +161,6 @@ ) from bittensor.utils.balance import ( Balance, - FixedPoint, check_balance_amount, fixed_to_float, ) diff --git a/bittensor/core/axon.py b/bittensor/core/axon.py index f7227d6233..0895cf708a 100644 --- a/bittensor/core/axon.py +++ b/bittensor/core/axon.py @@ -21,6 +21,7 @@ from fastapi.responses import JSONResponse from fastapi.routing import serialize_response from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint +from starlette.types import ASGIApp from starlette.requests import Request from starlette.responses import Response @@ -1097,7 +1098,7 @@ class AxonMiddleware(BaseHTTPMiddleware): such as response header updating and logging. """ - def __init__(self, app: "AxonMiddleware", axon: "Axon"): + def __init__(self, app: ASGIApp, axon: "Axon"): """ Initialize the AxonMiddleware class. diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index e5064a01af..28c97d3c31 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -11,6 +11,7 @@ from bittensor_drand import get_encrypted_commitment from bittensor_wallet.utils import SS58_FORMAT from scalecodec.base import ScaleType +from scalecodec.utils.math import FixedPoint from bittensor.core.axon import Axon from bittensor.core.chain_data import ( @@ -145,6 +146,7 @@ SubtensorMixin, UIDs, Weights, + PositionResponse, ) from bittensor.utils import ( Certificate, @@ -159,7 +161,6 @@ ) from bittensor.utils.balance import ( Balance, - FixedPoint, check_balance_amount, fixed_to_float, ) @@ -715,7 +716,7 @@ def query_module( name: str, params: Optional[list] = None, block: Optional[int] = None, - ) -> Optional[ScaleType]: + ) -> ScaleType: """Queries any module storage on the Bittensor blockchain with the specified parameters and block number. This function is a generic query interface that allows for flexible and diverse data retrieval from various blockchain modules. Use this function for nonstandard queries to storage defined within the Bittensor @@ -769,7 +770,7 @@ def query_subtensor( name: str, params: Optional[list] = None, block: Optional[int] = None, - ) -> Optional[ScaleType]: + ) -> ScaleType[Any]: """Queries named storage from the Subtensor module on the Bittensor blockchain. Use this function for nonstandard queries to constants defined within the Bittensor blockchain, if these cannot @@ -1377,9 +1378,13 @@ def get_balances( ] batch_call = self.substrate.query_multi(calls, block_hash=block_hash) results = {} + key: StorageKey + val: dict for item in batch_call: - value = item[1] or {"data": {"free": 0}} - results.update({item[0].params[0]: Balance(value["data"]["free"])}) + key, val = item + value = val or {"data": {"free": 0}} + assert key.params is not None + results.update({key.params[0]: Balance(value["data"]["free"])}) return results def get_current_block(self) -> int: @@ -1653,8 +1658,7 @@ def get_coldkey_swap_announcement_delay( storage_function="ColdkeySwapAnnouncementDelay", block_hash=block_hash, ) - value = getattr(query, "value", query) - return cast(int, value) if value is not None else 0 + return cast(int, query.value) or 0 def get_coldkey_swap_constants( self, @@ -1728,8 +1732,7 @@ def get_coldkey_swap_reannouncement_delay( storage_function="ColdkeySwapReannouncementDelay", block_hash=block_hash, ) - value = getattr(query, "value", query) - return cast(int, value) if value is not None else 0 + return cast(int, query.value) or 0 def get_coldkey_swap_dispute( self, @@ -2414,13 +2417,12 @@ def get_liquidity_list( sqrt_price = fixed_to_float(sqrt_price_query[1]) current_tick = price_to_tick(sqrt_price**2) - positions_values: list[tuple[dict, int, int]] = [] + positions_values: list[tuple[PositionResponse, int, int]] = [] positions_storage_keys: list[StorageKey] = [] - for _, p in positions_response: - position = p.value - - tick_low_idx = position["tick_low"][0] - tick_high_idx = position["tick_high"][0] + position: PositionResponse + for _, position in positions_response: + tick_low_idx = position["tick_low"] + tick_high_idx = position["tick_high"] tick_low_sk = self.substrate.create_storage_key( pallet="Swap", @@ -2499,19 +2501,15 @@ def get_liquidity_list( positions.append( LiquidityPosition( - **{ - "id": position.get("id")[0], - "price_low": Balance.from_tao( - tick_to_price(position.get("tick_low")[0]) - ), - "price_high": Balance.from_tao( - tick_to_price(position.get("tick_high")[0]) - ), - "liquidity": Balance.from_rao(position.get("liquidity")), - "fees_tao": fees_tao, - "fees_alpha": fees_alpha, - "netuid": position.get("netuid"), - } + id=position.get("id"), + price_low=Balance.from_tao(tick_to_price(position.get("tick_low"))), + price_high=Balance.from_tao( + tick_to_price(position.get("tick_high")) + ), + liquidity=Balance.from_rao(position.get("liquidity")), + fees_tao=fees_tao, + fees_alpha=fees_alpha, + netuid=position.get("netuid"), ) ) @@ -2543,7 +2541,7 @@ def get_mechanism_emission_split( params=[netuid], block_hash=block_hash, ) - if result is None: + if result.value is None: return None total = sum(result.value) return [round(i / total * 100) for i in result.value] @@ -2578,7 +2576,7 @@ def get_mechanism_count( params=[netuid], block_hash=block_hash, ) - return query.value if query is not None and hasattr(query, "value") else 1 + return query.value or 1 def get_metagraph_info( self, @@ -2928,7 +2926,7 @@ def get_owned_hotkeys( params=[coldkey_ss58], block_hash=block_hash, ) - return owned_hotkeys + return owned_hotkeys.value def get_parents( self, hotkey_ss58: str, netuid: int, block: Optional[int] = None @@ -3356,8 +3354,7 @@ def get_root_claimable_all_rates( params=[hotkey_ss58], block_hash=self.determine_block_hash(block), ) - query_value = getattr(query, "value", query) - bits_list = next(iter(cast(list[list[tuple[int, FixedPoint]]], query_value))) + bits_list = next(iter(cast(list[list[tuple[int, FixedPoint]]], query.value))) return {bits[0]: fixed_to_float(bits[1], frac_bits=32) for bits in bits_list} def get_root_claimable_stake( @@ -3667,7 +3664,7 @@ def get_staking_hotkeys( params=[coldkey_ss58], block_hash=self.determine_block_hash(block), ) - return result or [] + return result.value or [] def get_start_call_delay(self, block: Optional[int] = None) -> int: """ @@ -3953,7 +3950,6 @@ def get_timestamp(self, block: Optional[int] = None) -> datetime: datetime object for the timestamp of the block """ unix = self.query_module("Timestamp", "Now", block=block) - assert unix is not None return datetime.fromtimestamp(unix.value / 1000, tz=timezone.utc) def get_total_subnets(self, block: Optional[int] = None) -> Optional[int]: @@ -4857,7 +4853,7 @@ def validate_extrinsic_params( ) # Expected params from metadata - expected_params = func_meta.get_param_info() + expected_params = func_meta.get_param_info() # type: ignore provided_params = {} # Validate and filter parameters @@ -5004,6 +5000,7 @@ def sign_and_send_extrinsic( extrinsic_response.extrinsic_receipt = response if response.is_success: + assert response.total_fee_amount is not None extrinsic_response.extrinsic_fee = Balance.from_rao( response.total_fee_amount ) diff --git a/bittensor/core/types.py b/bittensor/core/types.py index a30cfe3b17..8ae791f9f3 100644 --- a/bittensor/core/types.py +++ b/bittensor/core/types.py @@ -5,6 +5,7 @@ import numpy as np from numpy.typing import NDArray +from scalecodec.utils.math import FixedPoint from bittensor.core import settings from bittensor.core.chain_data import NeuronInfo, NeuronInfoLite @@ -575,3 +576,14 @@ class BlockInfo: header: dict extrinsics: list explorer: str + + +# TypedDicts +class PositionResponse(TypedDict): + id: int + netuid: int + tick_low: int + tick_high: int + liquidity: int + fees_tao: FixedPoint + fees_alpha: FixedPoint diff --git a/bittensor/utils/balance.py b/bittensor/utils/balance.py index 6690f17fb3..393481b42f 100644 --- a/bittensor/utils/balance.py +++ b/bittensor/utils/balance.py @@ -1,6 +1,9 @@ -from typing import Optional, TypedDict, Union +from typing import Optional, Union -from scalecodec.utils.math import fixed_to_float as fixed_to_float +from scalecodec.utils.math import ( + fixed_to_float as fixed_to_float, + FixedPoint as FixedPoint, +) from bittensor.core import settings from bittensor.core.errors import BalanceTypeError, BalanceUnitMismatchError @@ -362,17 +365,6 @@ def set_unit(self, netuid: int): return self -class FixedPoint(TypedDict): - """ - Represents a fixed point ``U64F64`` number. - Where ``bits`` is a U128 representation of the fixed point number. - - This matches the type of the Alpha shares. - """ - - bits: int - - # lowercase is added for backwards compatibility to not break API units = UNITS = [ chr( diff --git a/bittensor/utils/liquidity.py b/bittensor/utils/liquidity.py index 16b778449e..3cc253c01d 100644 --- a/bittensor/utils/liquidity.py +++ b/bittensor/utils/liquidity.py @@ -5,9 +5,9 @@ """ import math -from typing import Any from dataclasses import dataclass +from bittensor.core.types import PositionResponse from bittensor.utils import ChainFeatureDisabledWarning, deprecated_message from bittensor.utils.balance import Balance, fixed_to_float @@ -155,7 +155,7 @@ def get_fees_in_range( def calculate_fees( - position: dict[str, Any], + position: PositionResponse, global_fees_tao: float, global_fees_alpha: float, tao_fees_below_low: float, diff --git a/pyproject.toml b/pyproject.toml index 7ddaf7e5ee..2e25751e25 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ dependencies = [ "retry==0.9.2", "requests>=2.0.0,<3.0", "pydantic>=2.3,<3", - "cyscale==0.2.0", + "cyscale==0.2.1", "uvicorn", "bittensor-drand>=1.3.0,<2.0.0", "bittensor-wallet==4.0.1", From 32247d55e4b8111ff54c100aa27f14deefff50d4 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Tue, 14 Apr 2026 21:43:47 +0200 Subject: [PATCH 062/118] Checkin --- bittensor/core/async_subtensor.py | 2 +- tests/unit_tests/test_async_subtensor.py | 84 +++++++++--------------- tests/unit_tests/test_subtensor.py | 12 ++-- 3 files changed, 37 insertions(+), 61 deletions(-) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index d9e4ef51e2..4b4cbb78f3 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -504,7 +504,7 @@ async def _runtime_method_exists( runtime = await self.substrate.init_runtime(block_hash=block_hash) if runtime.metadata_v15 is not None: try: - _ = runtime.runtime_api_map[method] + _ = runtime.runtime_api_map[api][method] return True except KeyError: return False diff --git a/tests/unit_tests/test_async_subtensor.py b/tests/unit_tests/test_async_subtensor.py index 8a24fd0d45..dda5957a91 100644 --- a/tests/unit_tests/test_async_subtensor.py +++ b/tests/unit_tests/test_async_subtensor.py @@ -2906,21 +2906,15 @@ async def test_get_metagraph_info_all_fields(subtensor, mocker): async_subtensor.MetagraphInfo, "from_dict", return_value="parsed_metagraph" ) mocked_runtime_metadata_v15 = { - "apis": [ - { - "name": "SubnetInfoRuntimeApi", - "methods": [ - {"name": "get_selective_metagraph"}, - {"name": "get_metagraph"}, - {"name": "get_selective_mechagraph"}, - ], - }, - ] + "SubnetInfoRuntimeApi": { + "get_selective_metagraph": {"name": "get_selective_metagraph"}, + "get_metagraph": {"name": "get_metagraph"}, + "get_selective_mechagraph": {"name": "get_selective_mechagraph"}, + }, } mocked_runtime = mocker.Mock(spec=Runtime) - mocked_metadata = mocker.Mock() - mocked_metadata.value.return_value = mocked_runtime_metadata_v15 - mocked_runtime.metadata_v15 = mocked_metadata + mocked_runtime.metadata_v15 = mocker.Mock() + mocked_runtime.runtime_api_map = mocked_runtime_metadata_v15 mocker.patch.object( subtensor.substrate, "init_runtime", @@ -2966,21 +2960,15 @@ async def test_get_metagraph_info_specific_fields(subtensor, mocker): async_subtensor.MetagraphInfo, "from_dict", return_value="parsed_metagraph" ) mocked_runtime_metadata_v15 = { - "apis": [ - { - "name": "SubnetInfoRuntimeApi", - "methods": [ - {"name": "get_selective_metagraph"}, - {"name": "get_metagraph"}, - {"name": "get_selective_mechagraph"}, - ], - }, - ] + "SubnetInfoRuntimeApi": { + "get_selective_metagraph": {"name": "get_selective_metagraph"}, + "get_metagraph": {"name": "get_metagraph"}, + "get_selective_mechagraph": {"name": "get_selective_mechagraph"}, + }, } mocked_runtime = mocker.Mock(spec=Runtime) - mocked_metadata = mocker.Mock() - mocked_metadata.value.return_value = mocked_runtime_metadata_v15 - mocked_runtime.metadata_v15 = mocked_metadata + mocked_runtime.metadata_v15 = mocker.Mock() + mocked_runtime.runtime_api_map = mocked_runtime_metadata_v15 mocker.patch.object( subtensor.substrate, "init_runtime", @@ -3019,21 +3007,15 @@ async def test_get_metagraph_info_subnet_not_exist(subtensor, mocker): return_value=None, ) mocked_runtime_metadata_v15 = { - "apis": [ - { - "name": "SubnetInfoRuntimeApi", - "methods": [ - {"name": "get_selective_metagraph"}, - {"name": "get_metagraph"}, - {"name": "get_selective_mechagraph"}, - ], - }, - ] + "SubnetInfoRuntimeApi": { + "get_selective_metagraph": {"name": "get_selective_metagraph"}, + "get_metagraph": {"name": "get_metagraph"}, + "get_selective_mechagraph": {"name": "get_selective_mechagraph"}, + }, } mocked_runtime = mocker.Mock(spec=Runtime) - mocked_metadata = mocker.Mock() - mocked_metadata.value.return_value = mocked_runtime_metadata_v15 - mocked_runtime.metadata_v15 = mocked_metadata + mocked_runtime.metadata_v15 = mocker.Mock() + mocked_runtime.runtime_api_map = mocked_runtime_metadata_v15 mocker.patch.object( subtensor.substrate, "init_runtime", @@ -3078,25 +3060,19 @@ async def test_get_metagraph_info_older_runtime_version( "runtime_call", ) mocked_runtime_metadata_v15 = { - "apis": [ - { - "name": "SubnetInfoRuntimeApi", - "methods": [ - {"name": "get_selective_metagraph"}, - {"name": "get_metagraph"}, - ], - }, - ] + "SubnetInfoRuntimeApi": { + "get_selective_metagraph": {"name": "get_selective_metagraph"}, + "get_metagraph": {"name": "get_metagraph"}, + }, } if block == 6_800_000: # only the newer block should have 'mechagraph' runtime - mocked_runtime_metadata_v15["apis"][0]["methods"].append( - {"name": "get_selective_mechagraph"} - ) + mocked_runtime_metadata_v15["SubnetInfoRuntimeApi"][ + "get_selective_mechagraph" + ] = {"name": "get_selective_mechagraph"} mocked_runtime = mocker.Mock(spec=Runtime) - mocked_metadata = mocker.Mock() - mocked_metadata.value.return_value = mocked_runtime_metadata_v15 - mocked_runtime.metadata_v15 = mocked_metadata + mocked_runtime.metadata_v15 = mocker.Mock() + mocked_runtime.runtime_api_map = mocked_runtime_metadata_v15 mocker.patch.object( subtensor.substrate, "init_runtime", diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index 3d34d2980f..3962c5d63c 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -2958,9 +2958,7 @@ def test_get_owned_hotkeys_happy_path(subtensor, mocker): # Prep fake_coldkey = "fake_hotkey" fake_hotkey = "fake_hotkey" - fake_hotkeys = [ - fake_hotkey, - ] + fake_hotkeys = mocker.MagicMock(spec=ScaleType, value=[fake_hotkey]) mocked_subtensor = mocker.Mock(return_value=fake_hotkeys) mocker.patch.object(subtensor.substrate, "query", new=mocked_subtensor) @@ -2974,15 +2972,17 @@ def test_get_owned_hotkeys_happy_path(subtensor, mocker): params=[fake_coldkey], block_hash=None, ) - assert result == fake_hotkeys + assert result == fake_hotkeys.value def test_get_owned_hotkeys_return_empty(subtensor, mocker): """Tests that the output of get_owned_hotkeys is empty.""" # Prep fake_coldkey = "fake_hotkey" - mocked_subtensor = mocker.Mock(return_value=[]) - mocker.patch.object(subtensor.substrate, "query", new=mocked_subtensor) + mocked_return = mocker.MagicMock(spec=ScaleType, value=[]) + mocked_subtensor = mocker.patch.object( + subtensor.substrate, "query", return_value=mocked_return + ) # Call result = subtensor.get_owned_hotkeys(fake_coldkey) From 6421b67b083c8193931ac755717686bf51f1af9f Mon Sep 17 00:00:00 2001 From: BD Himes Date: Wed, 15 Apr 2026 12:39:49 +0200 Subject: [PATCH 063/118] Unit tests passed --- bittensor/core/subtensor.py | 2 +- tests/unit_tests/test_subtensor.py | 112 +++++++++++------------------ 2 files changed, 43 insertions(+), 71 deletions(-) diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 28c97d3c31..c5a4f8c6af 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -396,7 +396,7 @@ def _runtime_method_exists(self, api: str, method: str, block_hash: str) -> bool runtime = self.substrate.init_runtime(block_hash=block_hash) if runtime.metadata_v15 is not None: try: - _ = runtime.runtime_api_map[method] + _ = runtime.runtime_api_map[api][method] return True except KeyError: return False diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index 3962c5d63c..44b178597f 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -3113,21 +3113,15 @@ def test_get_metagraph_info_all_fields(subtensor, mocker): subtensor_module.MetagraphInfo, "from_dict", return_value="parsed_metagraph" ) mocked_runtime_metadata_v15 = { - "apis": [ - { - "name": "SubnetInfoRuntimeApi", - "methods": [ - {"name": "get_selective_metagraph"}, - {"name": "get_metagraph"}, - {"name": "get_selective_mechagraph"}, - ], - }, - ] + "SubnetInfoRuntimeApi": { + "get_selective_metagraph": {"name": "get_selective_metagraph"}, + "get_metagraph": {"name": "get_metagraph"}, + "get_selective_mechagraph": {"name": "get_selective_mechagraph"}, + }, } mocked_runtime = mocker.Mock(spec=Runtime) - mocked_metadata = mocker.Mock() - mocked_metadata.value.return_value = mocked_runtime_metadata_v15 - mocked_runtime.metadata_v15 = mocked_metadata + mocked_runtime.metadata_v15 = mocker.Mock() + mocked_runtime.runtime_api_map = mocked_runtime_metadata_v15 mocker.patch.object( subtensor.substrate, "init_runtime", @@ -3169,21 +3163,15 @@ def test_get_metagraph_info_specific_fields(subtensor, mocker): return_value="0xfakechainhead", ) mocked_runtime_metadata_v15 = { - "apis": [ - { - "name": "SubnetInfoRuntimeApi", - "methods": [ - {"name": "get_selective_metagraph"}, - {"name": "get_metagraph"}, - {"name": "get_selective_mechagraph"}, - ], - }, - ] + "SubnetInfoRuntimeApi": { + "get_selective_metagraph": {"name": "get_selective_metagraph"}, + "get_metagraph": {"name": "get_metagraph"}, + "get_selective_mechagraph": {"name": "get_selective_mechagraph"}, + }, } mocked_runtime = mocker.Mock(spec=Runtime) - mocked_metadata = mocker.Mock() - mocked_metadata.value.return_value = mocked_runtime_metadata_v15 - mocked_runtime.metadata_v15 = mocked_metadata + mocked_runtime.metadata_v15 = mocker.Mock() + mocked_runtime.runtime_api_map = mocked_runtime_metadata_v15 mocker.patch.object( subtensor.substrate, "init_runtime", @@ -3224,21 +3212,15 @@ def test_get_metagraph_info_subnet_not_exist(subtensor, mocker): return_value=None, ) mocked_runtime_metadata_v15 = { - "apis": [ - { - "name": "SubnetInfoRuntimeApi", - "methods": [ - {"name": "get_selective_metagraph"}, - {"name": "get_metagraph"}, - {"name": "get_selective_mechagraph"}, - ], - }, - ] + "SubnetInfoRuntimeApi": { + "get_selective_metagraph": {"name": "get_selective_metagraph"}, + "get_metagraph": {"name": "get_metagraph"}, + "get_selective_mechagraph": {"name": "get_selective_mechagraph"}, + }, } mocked_runtime = mocker.Mock(spec=Runtime) - mocked_metadata = mocker.Mock() - mocked_metadata.value.return_value = mocked_runtime_metadata_v15 - mocked_runtime.metadata_v15 = mocked_metadata + mocked_runtime.metadata_v15 = mocker.Mock() + mocked_runtime.runtime_api_map = mocked_runtime_metadata_v15 mocker.patch.object( subtensor.substrate, "init_runtime", @@ -3282,25 +3264,19 @@ def test_get_metagraph_info_older_runtime_version( "runtime_call", ) mocked_runtime_metadata_v15 = { - "apis": [ - { - "name": "SubnetInfoRuntimeApi", - "methods": [ - {"name": "get_selective_metagraph"}, - {"name": "get_metagraph"}, - ], - }, - ] + "SubnetInfoRuntimeApi": { + "get_selective_metagraph": {"name": "get_selective_metagraph"}, + "get_metagraph": {"name": "get_metagraph"}, + }, } if block == 6_800_000: # only the newer block should have 'mechagraph' runtime - mocked_runtime_metadata_v15["apis"][0]["methods"].append( - {"name": "get_selective_mechagraph"} - ) + mocked_runtime_metadata_v15["SubnetInfoRuntimeApi"][ + "get_selective_mechagraph" + ] = {"name": "get_selective_mechagraph"} mocked_runtime = mocker.Mock(spec=Runtime) - mocked_metadata = mocker.Mock() - mocked_metadata.value.return_value = mocked_runtime_metadata_v15 - mocked_runtime.metadata_v15 = mocked_metadata + mocked_runtime.metadata_v15 = mocker.Mock() + mocked_runtime.runtime_api_map = mocked_runtime_metadata_v15 mocker.patch.object( subtensor.substrate, "init_runtime", @@ -3724,18 +3700,16 @@ def test_get_liquidity_list_happy_path(subtensor, fake_wallet, mocker): # Fake positions to return from query_map fake_positions = [ [ - (2,), - mocker.Mock( - value={ - "id": (2,), - "netuid": 2, - "tick_low": (206189,), - "tick_high": (208196,), - "liquidity": 1000000000000, - "fees_tao": {"bits": 0}, - "fees_alpha": {"bits": 0}, - } - ), + 2, + { + "id": 2, + "netuid": 2, + "tick_low": 206189, + "tick_high": 208196, + "liquidity": 1000000000000, + "fees_tao": {"bits": 0}, + "fees_alpha": {"bits": 0}, + }, ], ] fake_result = mocker.MagicMock(records=fake_positions, autospec=list) @@ -4197,7 +4171,7 @@ def test_get_timelocked_weight_commits(subtensor, mocker): @pytest.mark.parametrize( "query_return, expected_result", ( - ["value", [10, 90]], + [[6553, 58982], [10, 90]], [None, None], ), ) @@ -4205,9 +4179,7 @@ def test_get_mechanism_emission_split(subtensor, mocker, query_return, expected_ """Verify that get_mechanism_emission_split calls the correct methods.""" # Preps netuid = mocker.Mock() - query_return = ( - mocker.Mock(value=[6553, 58982]) if query_return == "value" else query_return - ) + query_return = mocker.Mock(value=query_return) mocked_determine_block_hash = mocker.patch.object(subtensor, "determine_block_hash") mocked_query = mocker.patch.object( subtensor.substrate, "query", return_value=query_return From e2b79f4e77050434bdc723e162cb7668b3d9c8ce Mon Sep 17 00:00:00 2001 From: BD Himes Date: Wed, 15 Apr 2026 14:49:35 +0200 Subject: [PATCH 064/118] Progress --- bittensor/core/async_subtensor.py | 106 ++++++++++------------ bittensor/core/chain_data/coldkey_swap.py | 9 +- bittensor/core/subtensor.py | 89 +++++++++--------- bittensor/core/types.py | 5 + bittensor/utils/__init__.py | 10 +- tests/e2e_tests/test_axon.py | 1 + 6 files changed, 107 insertions(+), 113 deletions(-) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index 4b4cbb78f3..d9fe2e9e12 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -147,6 +147,8 @@ SubtensorMixin, UIDs, Weights, + PositionResponse, + NeuronCertificateResponse, ) from bittensor.utils import ( Certificate, @@ -1678,13 +1680,13 @@ async def get_balance( Balance: The balance object containing the account's TAO balance. """ block_hash = await self.determine_block_hash(block, block_hash, reuse_block) - balance = await self.substrate.query( + balance: ScaleType[dict[str, Any]] = await self.substrate.query( module="System", storage_function="Account", params=[address], block_hash=block_hash, ) - return Balance(balance["data"]["free"]) + return Balance(balance.value["data"]["free"]) async def get_balances( self, @@ -1934,7 +1936,7 @@ async def get_coldkey_swap_announcement( block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> Optional["ColdkeySwapAnnouncementInfo"]: + ) -> Optional[ColdkeySwapAnnouncementInfo]: """ Retrieves coldkey swap announcement for a specific coldkey. @@ -1957,13 +1959,13 @@ async def get_coldkey_swap_announcement( - See: """ block_hash = await self.determine_block_hash(block, block_hash, reuse_block) - query = await self.substrate.query( + query: ScaleType[Optional[tuple[int, str]]] = await self.substrate.query( module="SubtensorModule", storage_function="ColdkeySwapAnnouncements", params=[coldkey_ss58], block_hash=block_hash, ) - if query is None: + if query.value is None: return None return ColdkeySwapAnnouncementInfo.from_query( coldkey_ss58=coldkey_ss58, query=query @@ -2969,11 +2971,12 @@ async def get_liquidity_list( current_tick = price_to_tick(sqrt_price**2) # Fetch positions - positions_values: list[tuple[dict, int, int]] = [] + positions_values: list[tuple[PositionResponse, int, int]] = [] positions_storage_keys: list[StorageKey] = [] + position: PositionResponse async for _, position in positions_response: - tick_low_idx = position.get("tick_low")[0] - tick_high_idx = position.get("tick_high")[0] + tick_low_idx = position.get("tick_low") + tick_high_idx = position.get("tick_high") positions_values.append((position, tick_low_idx, tick_high_idx)) tick_low_sk = await self.substrate.create_storage_key( pallet="Swap", @@ -3052,12 +3055,12 @@ async def get_liquidity_list( positions.append( LiquidityPosition( **{ - "id": position.get("id")[0], + "id": position.get("id"), "price_low": Balance.from_tao( - tick_to_price(position.get("tick_low")[0]) + tick_to_price(position.get("tick_low")) ), "price_high": Balance.from_tao( - tick_to_price(position.get("tick_high")[0]) + tick_to_price(position.get("tick_high")) ), "liquidity": Balance.from_rao(position.get("liquidity")), "fees_tao": fees_tao, @@ -3409,22 +3412,21 @@ async def get_neuron_certificate( This function is used for certificate discovery for setting up mutual tls communication between neurons. """ block_hash = await self.determine_block_hash(block, block_hash, reuse_block) - certificate = cast( - Union[str, dict], - await self.query_module( - module="SubtensorModule", - name="NeuronCertificates", - block_hash=block_hash, - reuse_block=reuse_block, - params=[netuid, hotkey_ss58], - ), + certificate_query: ScaleType[ + Optional[str | NeuronCertificateResponse] + ] = await self.query_module( + module="SubtensorModule", + name="NeuronCertificates", + block_hash=block_hash, + reuse_block=reuse_block, + params=[netuid, hotkey_ss58], ) - try: - if certificate: + certificate: Optional[NeuronCertificateResponse] = certificate_query.value + if certificate is not None: + try: return Certificate(certificate) - - except AttributeError: - return None + except AttributeError: + return None return None async def get_neuron_for_pubkey_and_subnet( @@ -4460,17 +4462,14 @@ async def get_start_call_delay( Return: Amount of blocks after the start call can be executed. """ - return cast( - int, - ( - await self.query_subtensor( - name="StartCallDelay", - block=block, - block_hash=block_hash, - reuse_block=reuse_block, - ) - ), - ) + return ( + await self.query_subtensor( + name="StartCallDelay", + block=block, + block_hash=block_hash, + reuse_block=reuse_block, + ) + ).value async def get_subnet_burn_cost( self, @@ -4957,20 +4956,17 @@ async def get_vote_data( network, particularly how proposals are received and acted upon by the governing body. """ block_hash = await self.determine_block_hash(block, block_hash, reuse_block) - vote_data = cast( - Optional[dict[str, Any]], - await self.substrate.query( - module="Triumvirate", - storage_function="Voting", - params=[proposal_hash], - block_hash=block_hash, - ), + vote_data: ScaleType[Optional[dict[str, Any]]] = await self.substrate.query( + module="Triumvirate", + storage_function="Voting", + params=[proposal_hash], + block_hash=block_hash, ) - if vote_data is None: + if vote_data.value is None: return None - return ProposalVoteData.from_dict(vote_data) + return ProposalVoteData.from_dict(vote_data.value) async def get_uid_for_hotkey_on_subnet( self, @@ -5328,7 +5324,11 @@ async def is_subnet_active( reuse_block=reuse_block, params=[netuid], ) - return True if query and query.value > 0 else False + qv: Optional[int] = query.value + if qv is None or qv <= 0: + return False + else: + return True async def last_drand_round(self) -> Optional[int]: """Retrieves the last drand round emitted in Bittensor. @@ -5608,16 +5608,10 @@ async def query_identity( block_hash=block_hash, ) - if not identity_info: - return None - - try: - identity_data = getattr(identity_info, "value", identity_info) - return ChainIdentity.from_dict( - decode_hex_identity_dict(cast(dict[str, Any], identity_data)), - ) - except TypeError: + identity_data: Optional[dict[str, Any]] = identity_info.value + if identity_info is None: return None + return ChainIdentity.from_dict(decode_hex_identity_dict(identity_data)) async def recycle( self, diff --git a/bittensor/core/chain_data/coldkey_swap.py b/bittensor/core/chain_data/coldkey_swap.py index 6388ce452b..7407a3abb9 100644 --- a/bittensor/core/chain_data/coldkey_swap.py +++ b/bittensor/core/chain_data/coldkey_swap.py @@ -31,7 +31,7 @@ class ColdkeySwapAnnouncementInfo: @classmethod def from_query( - cls, coldkey_ss58: str, query: Optional[ScaleType] + cls, coldkey_ss58: str, query: ScaleType ) -> Optional["ColdkeySwapAnnouncementInfo"]: """ Creates a ColdkeySwapAnnouncementInfo object from a Substrate query result. @@ -43,11 +43,10 @@ def from_query( Returns: ColdkeySwapAnnouncementInfo if announcement exists, None otherwise. """ - if not getattr(query, "value", None): + if query.value is None: return None - - execution_block = query.value[0] - new_coldkey_hash = "0x" + bytes(query.value[1][0]).hex() + qv: tuple[int, str] = query.value + execution_block, new_coldkey_hash = qv return cls( coldkey=coldkey_ss58, execution_block=execution_block, diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index c5a4f8c6af..1682275612 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -147,6 +147,7 @@ UIDs, Weights, PositionResponse, + NeuronCertificateResponse, ) from bittensor.utils import ( Certificate, @@ -520,8 +521,7 @@ def get_hyperparameter( params=[netuid], block_hash=block_hash, ) - - return getattr(result, "value", result) + return result.value @property def block(self) -> int: @@ -1340,13 +1340,13 @@ def get_balance(self, address: str, block: Optional[int] = None) -> Balance: Returns: Balance: The balance object containing the account's TAO balance. """ - balance = self.substrate.query( + balance: ScaleType[dict[str, Any]] = self.substrate.query( module="System", storage_function="Account", params=[address], block_hash=self.determine_block_hash(block), ) - return Balance(cast(dict[str, Any], balance)["data"]["free"]) + return Balance(balance.value["data"]["free"]) def get_balances( self, @@ -1591,16 +1591,16 @@ def get_coldkey_swap_announcement( - See: """ block_hash = self.determine_block_hash(block) - query = self.substrate.query( + query: ScaleType[Optional[tuple[int, str]]] = self.substrate.query( module="SubtensorModule", storage_function="ColdkeySwapAnnouncements", params=[coldkey_ss58], block_hash=block_hash, ) - if query is None: + if query.value is None: return None return ColdkeySwapAnnouncementInfo.from_query( - coldkey_ss58=coldkey_ss58, query=cast(ScaleType, query) + coldkey_ss58=coldkey_ss58, query=query ) def get_coldkey_swap_announcements( @@ -1727,12 +1727,12 @@ def get_coldkey_swap_reannouncement_delay( - See: """ block_hash = self.determine_block_hash(block) - query = self.substrate.query( + query: ScaleType[Optional[int]] = self.substrate.query( module="SubtensorModule", storage_function="ColdkeySwapReannouncementDelay", block_hash=block_hash, ) - return cast(int, query.value) or 0 + return query.value or 0 def get_coldkey_swap_dispute( self, @@ -2124,13 +2124,13 @@ def get_delegate_take(self, hotkey_ss58: str, block: Optional[int] = None) -> fl Notes: - """ - result = self.query_subtensor( + result: ScaleType[int] = self.query_subtensor( name="Delegates", block=block, params=[hotkey_ss58], ) - return u16_normalized_float(result.value) # type: ignore + return u16_normalized_float(result.value) def get_delegated( self, coldkey_ss58: str, block: Optional[int] = None @@ -2821,18 +2821,20 @@ def get_neuron_certificate( This function is used for certificate discovery for setting up mutual tls communication between neurons. """ - certificate_query = self.query_module( - module="SubtensorModule", - name="NeuronCertificates", - block=block, - params=[netuid, hotkey_ss58], + certificate_query: ScaleType[Optional[str | NeuronCertificateResponse]] = ( + self.query_module( + module="SubtensorModule", + name="NeuronCertificates", + block=block, + params=[netuid, hotkey_ss58], + ) ) - try: - if certificate_query: - certificate = cast(dict, certificate_query) + certificate: Optional[NeuronCertificateResponse] = certificate_query.value + if certificate is not None: + try: return Certificate(certificate) - except AttributeError: - return None + except AttributeError: + return None return None def get_neuron_for_pubkey_and_subnet( @@ -3676,13 +3678,10 @@ def get_start_call_delay(self, block: Optional[int] = None) -> int: Return: Amount of blocks after the start call can be executed. """ - return cast( - int, - self.query_subtensor( - name="StartCallDelay", - block=block, - ), - ) + return self.query_subtensor( + name="StartCallDelay", + block=block, + ).value def get_subnet_burn_cost(self, block: Optional[int] = None) -> Optional[Balance]: """ @@ -4064,20 +4063,17 @@ def get_vote_data( This function is important for tracking and understanding the decision-making processes within the Bittensor network, particularly how proposals are received and acted upon by the governing body. """ - vote_data = cast( - Optional[dict[str, Any]], - self.substrate.query( - module="Triumvirate", - storage_function="Voting", - params=[proposal_hash], - block_hash=self.determine_block_hash(block), - ), + vote_data: ScaleType[Optional[dict[str, Any]]] = self.substrate.query( + module="Triumvirate", + storage_function="Voting", + params=[proposal_hash], + block_hash=self.determine_block_hash(block), ) if vote_data is None: return None - return ProposalVoteData.from_dict(vote_data) + return ProposalVoteData.from_dict(vote_data.value) def get_uid_for_hotkey_on_subnet( self, hotkey_ss58: str, netuid: int, block: Optional[int] = None @@ -4348,7 +4344,11 @@ def is_subnet_active(self, netuid: int, block: Optional[int] = None) -> bool: block=block, params=[netuid], ) - return True if query and query.value > 0 else False + qv: Optional[int] = query.value + if qv is None or qv <= 0: + return False + else: + return True def last_drand_round(self) -> Optional[int]: """Retrieves the last drand round emitted in Bittensor. @@ -4574,17 +4574,10 @@ def query_identity( params=[coldkey_ss58], block_hash=self.determine_block_hash(block), ) - - if not identity_info: - return None - - try: - identity_data = identity_info.value - return ChainIdentity.from_dict( - decode_hex_identity_dict(cast(dict[str, Any], identity_data)), - ) - except TypeError: + identity_data: Optional[dict[str, dict[Any, Any] | str]] = identity_info.value + if identity_data is None: return None + return ChainIdentity.from_dict(decode_hex_identity_dict(identity_data)) def recycle(self, netuid: int, block: Optional[int] = None) -> Optional[Balance]: """Retrieves the 'Burn' hyperparameter for a specified subnet. diff --git a/bittensor/core/types.py b/bittensor/core/types.py index 8ae791f9f3..975f120ee2 100644 --- a/bittensor/core/types.py +++ b/bittensor/core/types.py @@ -587,3 +587,8 @@ class PositionResponse(TypedDict): liquidity: int fees_tao: FixedPoint fees_alpha: FixedPoint + + +class NeuronCertificateResponse(TypedDict): + public_key: str + algorithm: int diff --git a/bittensor/utils/__init__.py b/bittensor/utils/__init__.py index 54a11316a3..1134fb4025 100644 --- a/bittensor/utils/__init__.py +++ b/bittensor/utils/__init__.py @@ -14,7 +14,7 @@ from bittensor_wallet import Keypair from bittensor_wallet.errors import KeyFileError, PasswordError from bittensor_wallet.utils import SS58_FORMAT -from scalecodec import ( +from scalecodec.utils.ss58 import ( ss58_decode, ss58_encode, is_valid_ss58_address as _is_valid_ss58_address, @@ -22,6 +22,7 @@ from bittensor.core import settings from bittensor.utils.btlogging import logging +from bittensor.core.types import NeuronCertificateResponse from .registration import torch, use_torch from .version import check_version, VersionCheckError @@ -93,10 +94,11 @@ def get_netuid_and_mechid_by_storage_index(storage_index: int) -> tuple[int, int class Certificate(str): - def __new__(cls, data: Union[str, dict]): + def __new__(cls, data: str | NeuronCertificateResponse): if isinstance(data, dict): - tuple_ascii = data["public_key"][0] - string = chr(data["algorithm"]) + "".join(chr(i) for i in tuple_ascii) + pubkey: str = data["public_key"] + pubkey_bytes = bytes.fromhex(pubkey.removeprefix("0x")) + string = chr(data["algorithm"]) + "".join([chr(i) for i in pubkey_bytes]) else: string = data return str.__new__(cls, string) diff --git a/tests/e2e_tests/test_axon.py b/tests/e2e_tests/test_axon.py index e1574383cc..bea707675c 100644 --- a/tests/e2e_tests/test_axon.py +++ b/tests/e2e_tests/test_axon.py @@ -82,6 +82,7 @@ def test_axon(subtensor, templates, alice_wallet): ) +@pytest.mark.skip @pytest.mark.asyncio async def test_axon_async(async_subtensor, templates, alice_wallet): """ From 12f974579e8dcee1a2c6155410b63175e8ab9d3a Mon Sep 17 00:00:00 2001 From: BD Himes Date: Wed, 15 Apr 2026 15:24:57 +0200 Subject: [PATCH 065/118] Checkin --- bittensor/core/async_subtensor.py | 43 +++++++++++++++---------------- bittensor/core/subtensor.py | 35 ++++++++++++------------- bittensor/utils/__init__.py | 5 ++-- tests/e2e_tests/test_axon.py | 1 - 4 files changed, 40 insertions(+), 44 deletions(-) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index d9fe2e9e12..ba20800755 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -3095,15 +3095,15 @@ async def get_mechanism_emission_split( - """ block_hash = await self.determine_block_hash(block, block_hash, reuse_block) - result = await self._query_with_fallback( + result: ScaleType[Optional[list[int]]] = await self._query_with_fallback( ("SubtensorModule", "MechanismEmissionSplit", [netuid]), block_hash=block_hash, default_value=None, ) - if result is None or not hasattr(result, "value"): + if result.value is None: return None - - return [round(i / sum(result.value) * 100) for i in result.value] + total = sum(result.value) + return [round(i / total * 100) for i in result.value] async def get_mechanism_count( self, @@ -3127,12 +3127,12 @@ async def get_mechanism_count( - """ block_hash = await self.determine_block_hash(block, block_hash, reuse_block) - query = await self._query_with_fallback( + query: ScaleType[Optional[int]] = await self._query_with_fallback( ("SubtensorModule", "MechanismCountCurrent", [netuid]), block_hash=block_hash, default_value=None, ) - return getattr(query, "value", 1) + return query.value or 1 async def get_metagraph_info( self, @@ -3228,13 +3228,13 @@ async def get_metagraph_info( default_value=None, ) - if getattr(query, "value", None) is None: + if query is None: logging.error( f"Subnet mechanism {netuid}.{mechid if mechid else 0} does not exist." ) return None - return MetagraphInfo.from_dict(query.value) + return MetagraphInfo.from_dict(query) async def get_mev_shield_current_key( self, @@ -3261,16 +3261,16 @@ async def get_mev_shield_current_key( announced a key yet. """ block_hash = await self.determine_block_hash(block, block_hash, reuse_block) - query = await self.substrate.query( + query: ScaleType[Optional[bytearray]] = await self.substrate.query( module="MevShield", storage_function="CurrentKey", block_hash=block_hash, ) - if query is None: + if query.value is None: return None - public_key_bytes = bytes(next(iter(query))) + public_key_bytes = bytes(query.value_object) # Validate public_key size for ML-KEM-768 (must be exactly 1184 bytes) if len(public_key_bytes) != MLKEM768_PUBLIC_KEY_SIZE: @@ -3306,16 +3306,16 @@ async def get_mev_shield_next_key( announced the next key yet. """ block_hash = await self.determine_block_hash(block, block_hash, reuse_block) - query = await self.substrate.query( + query: ScaleType[Optional[bytearray]] = await self.substrate.query( module="MevShield", storage_function="NextKey", block_hash=block_hash, ) - if query is None: + if query.value is None: return None - public_key_bytes = bytes(next(iter(query))) + public_key_bytes = bytes(query.value_object) # Validate public_key size for ML-KEM-768 (must be exactly 1184 bytes) if len(public_key_bytes) != MLKEM768_PUBLIC_KEY_SIZE: @@ -3347,7 +3347,7 @@ async def get_minimum_required_stake(self): module="SubtensorModule", storage_function="NominatorMinRequiredStake" ) - return Balance.from_rao(getattr(result, "value", 0)) + return Balance.from_rao(result.value or 0) async def get_netuids_for_hotkey( self, @@ -3436,7 +3436,7 @@ async def get_neuron_for_pubkey_and_subnet( block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> "NeuronInfo": + ) -> NeuronInfo: """ Retrieves information about a neuron based on its public key (hotkey SS58 address) and the specific subnet UID (netuid). This function provides detailed neuron information for a particular subnet within the Bittensor @@ -3462,7 +3462,7 @@ async def get_neuron_for_pubkey_and_subnet( params=[netuid, hotkey_ss58], block_hash=block_hash, ) - if (uid := getattr(uid_query, "value", None)) is None: + if (uid := uid_query.value) is None: return NeuronInfo.get_null_neuron() else: return await self.neuron_for_uid( @@ -3540,7 +3540,7 @@ async def get_owned_hotkeys( block_hash=block_hash, ) - return owned_hotkeys or [] + return owned_hotkeys.value or [] async def get_parents( self, @@ -3575,7 +3575,7 @@ async def get_parents( params=[hotkey_ss58, netuid], block_hash=block_hash, ) - if parents: + if parents.value: formatted_parents = [] for proportion, formatted_child in parents.value: # Convert U64 to int @@ -3692,14 +3692,13 @@ async def get_proxy_announcement( - See: """ block_hash = await self.determine_block_hash(block, block_hash, reuse_block) - query = await self.substrate.query( + query: ScaleType[tuple[list[dict], int]] = await self.substrate.query( module="Proxy", storage_function="Announcements", params=[delegate_account_ss58], block_hash=block_hash, ) - query_value = getattr(query, "value", query) - return ProxyAnnouncementInfo.from_dict(cast(list[Any], query_value)[0]) + return ProxyAnnouncementInfo.from_dict(query.value) async def get_proxy_announcements( self, diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 1682275612..95f9e20487 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -2535,7 +2535,7 @@ def get_mechanism_emission_split( module, storage_function, block_hash=block_hash ): return None - result = self.substrate.query( + result: ScaleType[Optional[list[int]]] = self.substrate.query( module="SubtensorModule", storage_function="MechanismEmissionSplit", params=[netuid], @@ -2570,7 +2570,7 @@ def get_mechanism_count( module, storage_function, block_hash=block_hash ): return 1 - query = self.substrate.query( + query: ScaleType[Optional[int]] = self.substrate.query( module=module, storage_function=storage_function, params=[netuid], @@ -2665,13 +2665,13 @@ def get_metagraph_info( default_value=None, ) - if query is None or not hasattr(query, "value") or query.value is None: + if query is None: logging.error( f"Subnet mechanism {netuid}.{mechid if mechid else 0} does not exist." ) return None - return MetagraphInfo.from_dict(query.value) + return MetagraphInfo.from_dict(query) def get_mev_shield_current_key( self, block: Optional[int] = None @@ -2693,16 +2693,16 @@ def get_mev_shield_current_key( announced a key yet. """ block_hash = self.determine_block_hash(block=block) - query = self.substrate.query( + query: ScaleType[Optional[bytearray]] = self.substrate.query( module="MevShield", storage_function="CurrentKey", block_hash=block_hash, ) - if query is None: + if query.value is None: return None - public_key_bytes = bytes(next(iter(query))) + public_key_bytes = bytes(query.value_object) # Validate public_key size for ML-KEM-768 if len(public_key_bytes) != MLKEM768_PUBLIC_KEY_SIZE: @@ -2731,16 +2731,16 @@ def get_mev_shield_next_key(self, block: Optional[int] = None) -> Optional[bytes announced the next key yet. """ block_hash = self.determine_block_hash(block=block) - query = self.substrate.query( + query: ScaleType[Optional[bytearray]] = self.substrate.query( module="MevShield", storage_function="NextKey", block_hash=block_hash, ) - if query is None: + if query.value is None: return None - public_key_bytes = bytes(next(iter(query))) + public_key_bytes = bytes(query.value_object) # Validate public_key size for ML-KEM-768 (must be exactly 1184 bytes) if len(public_key_bytes) != MLKEM768_PUBLIC_KEY_SIZE: @@ -2772,7 +2772,7 @@ def get_minimum_required_stake(self) -> Balance: module="SubtensorModule", storage_function="NominatorMinRequiredStake" ) - return Balance.from_rao(getattr(result, "value", 0)) + return Balance.from_rao(result.value or 0) def get_netuids_for_hotkey( self, hotkey_ss58: str, block: Optional[int] = None @@ -2839,7 +2839,7 @@ def get_neuron_certificate( def get_neuron_for_pubkey_and_subnet( self, hotkey_ss58: str, netuid: int, block: Optional[int] = None - ) -> Optional["NeuronInfo"]: + ) -> NeuronInfo: """ Retrieves information about a neuron based on its public key (hotkey SS58 address) and the specific subnet UID (netuid). This function provides detailed neuron information for a particular subnet within the Bittensor @@ -2863,7 +2863,7 @@ def get_neuron_for_pubkey_and_subnet( params=[netuid, hotkey_ss58], block_hash=block_hash, ) - if (uid := getattr(uid_query, "value", None)) is None: + if (uid := uid_query.value) is None: return NeuronInfo.get_null_neuron() return self.neuron_for_uid( @@ -2928,7 +2928,7 @@ def get_owned_hotkeys( params=[coldkey_ss58], block_hash=block_hash, ) - return owned_hotkeys.value + return owned_hotkeys.value or [] def get_parents( self, hotkey_ss58: str, netuid: int, block: Optional[int] = None @@ -2955,7 +2955,7 @@ def get_parents( params=[hotkey_ss58, netuid], block_hash=self.determine_block_hash(block), ) - if parents: + if parents.value: formatted_parents = [] for proportion, formatted_child in parents.value: # Convert U64 to int @@ -3057,14 +3057,13 @@ def get_proxy_announcement( - See: """ block_hash = self.determine_block_hash(block) - query = self.substrate.query( + query: ScaleType[tuple[list[dict], int]] = self.substrate.query( module="Proxy", storage_function="Announcements", params=[delegate_account_ss58], block_hash=block_hash, ) - query_value = getattr(query, "value", query) - return ProxyAnnouncementInfo.from_dict(cast(list[Any], query_value)[0]) + return ProxyAnnouncementInfo.from_dict(query.value) def get_proxy_announcements( self, diff --git a/bittensor/utils/__init__.py b/bittensor/utils/__init__.py index 1134fb4025..6a2197132d 100644 --- a/bittensor/utils/__init__.py +++ b/bittensor/utils/__init__.py @@ -22,13 +22,12 @@ from bittensor.core import settings from bittensor.utils.btlogging import logging -from bittensor.core.types import NeuronCertificateResponse from .registration import torch, use_torch from .version import check_version, VersionCheckError if TYPE_CHECKING: from bittensor_wallet import Wallet - from bittensor.core.types import ExtrinsicResponse + from bittensor.core.types import ExtrinsicResponse, NeuronCertificateResponse # keep save from import analyzer as obvious aliases hex_to_ss58 = ss58_encode @@ -94,7 +93,7 @@ def get_netuid_and_mechid_by_storage_index(storage_index: int) -> tuple[int, int class Certificate(str): - def __new__(cls, data: str | NeuronCertificateResponse): + def __new__(cls, data: "str | NeuronCertificateResponse"): if isinstance(data, dict): pubkey: str = data["public_key"] pubkey_bytes = bytes.fromhex(pubkey.removeprefix("0x")) diff --git a/tests/e2e_tests/test_axon.py b/tests/e2e_tests/test_axon.py index bea707675c..e1574383cc 100644 --- a/tests/e2e_tests/test_axon.py +++ b/tests/e2e_tests/test_axon.py @@ -82,7 +82,6 @@ def test_axon(subtensor, templates, alice_wallet): ) -@pytest.mark.skip @pytest.mark.asyncio async def test_axon_async(async_subtensor, templates, alice_wallet): """ From 91d414de1a79349052b9c541cf597b0b1f253840 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Wed, 15 Apr 2026 15:56:48 +0200 Subject: [PATCH 066/118] Checkin --- bittensor/core/async_subtensor.py | 55 ++++++++++--------------------- bittensor/core/subtensor.py | 49 ++++++++------------------- 2 files changed, 32 insertions(+), 72 deletions(-) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index ba20800755..991e787cf8 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -853,7 +853,7 @@ async def query_module( block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> Optional[ScaleType[Any]]: + ) -> ScaleType[Any]: """Queries any module storage on the Bittensor blockchain with the specified parameters and block number. This function is a generic query interface that allows for flexible and diverse data retrieval from various blockchain modules. Use this function for nonstandard queries to storage defined within the Bittensor @@ -3868,7 +3868,7 @@ async def get_revealed_commitment_by_hotkey( block_hash=block_hash, reuse_block=reuse_block, ) - if query is None: + if query.value is None: return None return tuple(decode_revealed_commitment(pair) for pair in query) @@ -3878,7 +3878,7 @@ async def get_root_claim_type( block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> Union[str, dict]: + ) -> str | dict[str, dict[str, list[int]]]: """Return the configured root claim type for a given coldkey. The root claim type controls how dividends from staking to the Root Subnet (subnet 0) are processed when they @@ -3906,30 +3906,15 @@ async def get_root_claim_type( - See also: """ block_hash = await self.determine_block_hash(block, block_hash, reuse_block) - query = await self.substrate.query( + query: ScaleType[ + str | dict[str, dict[str, list[int]]] + ] = await self.substrate.query( module="SubtensorModule", storage_function="RootClaimType", params=[coldkey_ss58], block_hash=block_hash, ) - query_value = getattr(query, "value", query) - claim_type = cast(dict[str, Any], query_value) - # Query returns enum as dict: {"Swap": ()} or {"Keep": ()} or {"KeepSubnets": {"subnets": [1, 2, 3]}} - variant_name = next(iter(claim_type.keys())) - variant_value = claim_type[variant_name] - - # For simple variants (Swap, Keep), value is empty tuple, return string - if not variant_value or variant_value == (): - return variant_name - - # For KeepSubnets, value contains the data, return full dict structure - if isinstance(variant_value, dict) and "subnets" in variant_value: - subnets_raw = variant_value["subnets"] - subnets = list(subnets_raw[0]) - - return {variant_name: {"subnets": subnets}} - - return {variant_name: variant_value} + return query.value async def get_root_alpha_dividends_per_subnet( self, @@ -3955,14 +3940,13 @@ async def get_root_alpha_dividends_per_subnet( Balance: The root alpha dividends for this hotkey on this subnet in Rao, with unit set to netuid. """ block_hash = await self.determine_block_hash(block, block_hash, reuse_block) - query = await self.substrate.query( + query: ScaleType[int] = await self.substrate.query( module="SubtensorModule", storage_function="RootAlphaDividendsPerSubnet", params=[netuid, hotkey_ss58], block_hash=block_hash, ) - value = getattr(query, "value", query) - return Balance.from_rao(cast(int, value)).set_unit(netuid=netuid) + return Balance.from_rao(query.value, netuid=netuid) async def get_root_claimable_rate( self, @@ -4023,15 +4007,15 @@ async def get_root_claimable_all_rates( - See: """ block_hash = await self.determine_block_hash(block, block_hash, reuse_block) - query = await self.substrate.query( + query: ScaleType[list[tuple[int, FixedPoint]]] = await self.substrate.query( module="SubtensorModule", storage_function="RootClaimable", params=[hotkey_ss58], block_hash=block_hash, ) - query_value = getattr(query, "value", query) - bits_list = next(iter(cast(list[list[tuple[int, FixedPoint]]], query_value))) - return {bits[0]: fixed_to_float(bits[1], frac_bits=32) for bits in bits_list} + return { + netuid: fixed_to_float(bits, frac_bits=32) for (netuid, bits) in query.value + } async def get_root_claimable_stake( self, @@ -4122,14 +4106,13 @@ async def get_root_claimed( - See: """ block_hash = await self.determine_block_hash(block, block_hash, reuse_block) - query = await self.substrate.query( + query: ScaleType[int] = await self.substrate.query( module="SubtensorModule", storage_function="RootClaimed", params=[netuid, hotkey_ss58, coldkey_ss58], block_hash=block_hash, ) - value = getattr(query, "value", query) - return Balance.from_rao(cast(int, value)).set_unit(netuid=netuid) + return Balance.from_rao(query.value, netuid=netuid) async def get_stake( self, @@ -4290,7 +4273,7 @@ async def get_stake_info_for_coldkey( block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> list["StakeInfo"]: + ) -> list[StakeInfo]: """ Retrieves the stake information for a given coldkey. @@ -4324,7 +4307,7 @@ async def get_stake_info_for_coldkeys( block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> dict[str, list["StakeInfo"]]: + ) -> dict[str, list[StakeInfo]]: """ Retrieves the stake information for multiple coldkeys. @@ -4378,9 +4361,7 @@ async def get_stake_for_hotkey( block_hash=block_hash, reuse_block=reuse_block, ) - balance = Balance.from_rao(hotkey_alpha_query.value) - balance.set_unit(netuid=netuid) - return balance + return Balance.from_rao(hotkey_alpha_query.value, netuid=netuid) get_hotkey_stake = get_stake_for_hotkey diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 95f9e20487..872a1ccd6a 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -3215,7 +3215,7 @@ def get_revealed_commitment_by_hotkey( params=[netuid, hotkey_ss58], block=block, ) - if query is None: + if query.value is None: return None return tuple(decode_revealed_commitment(pair) for pair in query) @@ -3223,7 +3223,7 @@ def get_root_claim_type( self, coldkey_ss58: str, block: Optional[int] = None, - ) -> Union[str, dict]: + ) -> str | dict[str, dict[str, list[int]]]: """Return the configured root claim type for a given coldkey. The root claim type controls how dividends from staking to the Root Subnet (subnet 0) are processed when they @@ -3253,24 +3253,7 @@ def get_root_claim_type( params=[coldkey_ss58], block_hash=self.determine_block_hash(block), ) - query_value = getattr(query, "value", query) - claim_type = cast(dict[str, Any], query_value) - # Query returns enum as dict: {"Swap": ()} or {"Keep": ()} or {"KeepSubnets": {"subnets": [1, 2, 3]}} - variant_name = next(iter(claim_type.keys())) - variant_value = claim_type[variant_name] - - # For simple variants (Swap, Keep), value is empty tuple, return string - if not variant_value or variant_value == (): - return variant_name - - # For KeepSubnets, value contains the data, return full dict structure - if isinstance(variant_value, dict) and "subnets" in variant_value: - subnets_raw = variant_value["subnets"] - subnets = list(subnets_raw[0]) - - return {variant_name: {"subnets": subnets}} - - return {variant_name: variant_value} + return query.value def get_root_alpha_dividends_per_subnet( self, @@ -3291,14 +3274,13 @@ def get_root_alpha_dividends_per_subnet( Returns: Balance: The root alpha dividends for this hotkey on this subnet in Rao, with unit set to netuid. """ - query = self.substrate.query( + query: ScaleType[int] = self.substrate.query( module="SubtensorModule", storage_function="RootAlphaDividendsPerSubnet", params=[netuid, hotkey_ss58], block_hash=self.determine_block_hash(block), ) - value = getattr(query, "value", query) - return Balance.from_rao(cast(int, value)).set_unit(netuid=netuid) + return Balance.from_rao(query.value, netuid=netuid) def get_root_claimable_rate( self, @@ -3349,14 +3331,15 @@ def get_root_claimable_all_rates( Notes: - See: """ - query = self.substrate.query( + query: ScaleType[list[tuple[int, FixedPoint]]] = self.substrate.query( module="SubtensorModule", storage_function="RootClaimable", params=[hotkey_ss58], block_hash=self.determine_block_hash(block), ) - bits_list = next(iter(cast(list[list[tuple[int, FixedPoint]]], query.value))) - return {bits[0]: fixed_to_float(bits[1], frac_bits=32) for bits in bits_list} + return { + netuid: fixed_to_float(bits, frac_bits=32) for (netuid, bits) in query.value + } def get_root_claimable_stake( self, @@ -3431,14 +3414,13 @@ def get_root_claimed( Notes: - See: """ - query = self.substrate.query( + query: ScaleType[int] = self.substrate.query( module="SubtensorModule", storage_function="RootClaimed", params=[netuid, hotkey_ss58, coldkey_ss58], block_hash=self.determine_block_hash(block), ) - value = getattr(query, "value", query) - return Balance.from_rao(cast(int, value)).set_unit(netuid=netuid) + return Balance.from_rao(query.value, netuid=netuid) def get_stake( self, @@ -3507,7 +3489,7 @@ def get_stake_for_coldkey_and_hotkey( def get_stake_info_for_coldkey( self, coldkey_ss58: str, block: Optional[int] = None - ) -> list["StakeInfo"]: + ) -> list[StakeInfo]: """ Retrieves the stake information for a given coldkey. @@ -3531,7 +3513,7 @@ def get_stake_info_for_coldkey( def get_stake_info_for_coldkeys( self, coldkey_ss58s: list[str], block: Optional[int] = None - ) -> dict[str, list["StakeInfo"]]: + ) -> dict[str, list[StakeInfo]]: """ Retrieves the stake information for multiple coldkeys. @@ -3568,10 +3550,7 @@ def get_stake_for_hotkey( hotkey_alpha_query = self.query_subtensor( name="TotalHotkeyAlpha", params=[hotkey_ss58, netuid], block=block ) - assert hotkey_alpha_query is not None - balance = Balance.from_rao(hotkey_alpha_query.value) - balance.set_unit(netuid=netuid) - return balance + return Balance.from_rao(hotkey_alpha_query.value, netuid=netuid) get_hotkey_stake = get_stake_for_hotkey From bdaf49cab79411cbb78b08bb7dde03033784cb52 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Wed, 15 Apr 2026 16:28:03 +0200 Subject: [PATCH 067/118] Mypy --- bittensor/core/async_subtensor.py | 31 ++++++++++++++++++------------- bittensor/core/subtensor.py | 22 +++++++++++++--------- 2 files changed, 31 insertions(+), 22 deletions(-) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index 991e787cf8..f82c96f4c4 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -13,7 +13,7 @@ from bittensor_wallet.utils import SS58_FORMAT from scalecodec import GenericCall from scalecodec.base import ScaleType -from scalecodec.utils.math import FixedPoint +from scalecodec.utils.math import FixedPoint, fixed_to_decimal from bittensor.core.chain_data import ( ColdkeySwapAnnouncementInfo, @@ -1007,14 +1007,18 @@ async def all_subnets( if not block_hash and reuse_block: block_hash = self.substrate.last_block_hash - query = await self.substrate.runtime_call( - api="SubnetInfoRuntimeApi", - method="get_all_dynamic_info", - block_hash=block_hash, - ) - subnet_prices = await self.get_subnet_prices(block_hash=block_hash) + decoded: list[dict[str, Any]] + subnet_prices: dict[int, Balance] - decoded = query.decode() + decoded, subnet_prices = await asyncio.gather( + self.substrate.runtime_call( + api="SubnetInfoRuntimeApi", + method="get_all_dynamic_info", + block_hash=block_hash, + ), + self.get_subnet_prices(block_hash=block_hash), + return_exceptions=True, + ) if not isinstance(subnet_prices, (SubstrateRequestException, ValueError)): for sn in decoded: @@ -3270,7 +3274,9 @@ async def get_mev_shield_current_key( if query.value is None: return None - public_key_bytes = bytes(query.value_object) + value: bytearray = query.value + + public_key_bytes = bytes(value) # Validate public_key size for ML-KEM-768 (must be exactly 1184 bytes) if len(public_key_bytes) != MLKEM768_PUBLIC_KEY_SIZE: @@ -4654,11 +4660,10 @@ async def get_subnet_prices( ) prices = {} - async for id_, current_sqrt_price in current_sqrt_prices: - # TODO investigate if we need to use fixed_to_decimal here instead - current_sqrt_price = fixed_to_float(current_sqrt_price) + async for id_, current_sqrt_price_bits in current_sqrt_prices: + current_sqrt_price = fixed_to_decimal(current_sqrt_price_bits) current_price = current_sqrt_price * current_sqrt_price - current_price_in_tao = Balance.from_rao(int(current_price * 1e9)) + current_price_in_tao = Balance.from_tao(float(current_price)) prices.update({id_: current_price_in_tao}) # SN0 price is always 1 TAO diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 872a1ccd6a..48340e1668 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -11,7 +11,7 @@ from bittensor_drand import get_encrypted_commitment from bittensor_wallet.utils import SS58_FORMAT from scalecodec.base import ScaleType -from scalecodec.utils.math import FixedPoint +from scalecodec.utils.math import FixedPoint, fixed_to_decimal from bittensor.core.axon import Axon from bittensor.core.chain_data import ( @@ -2702,7 +2702,9 @@ def get_mev_shield_current_key( if query.value is None: return None - public_key_bytes = bytes(query.value_object) + value: bytearray = query.value + + public_key_bytes = bytes(value) # Validate public_key size for ML-KEM-768 if len(public_key_bytes) != MLKEM768_PUBLIC_KEY_SIZE: @@ -2740,7 +2742,9 @@ def get_mev_shield_next_key(self, block: Optional[int] = None) -> Optional[bytes if query.value is None: return None - public_key_bytes = bytes(query.value_object) + value: bytearray = query.value + + public_key_bytes = bytes(value) # Validate public_key size for ML-KEM-768 (must be exactly 1184 bytes) if len(public_key_bytes) != MLKEM768_PUBLIC_KEY_SIZE: @@ -2829,7 +2833,7 @@ def get_neuron_certificate( params=[netuid, hotkey_ss58], ) ) - certificate: Optional[NeuronCertificateResponse] = certificate_query.value + certificate: Optional[str | NeuronCertificateResponse] = certificate_query.value if certificate is not None: try: return Certificate(certificate) @@ -3819,10 +3823,10 @@ def get_subnet_prices( ) prices = {} - for id_, current_sqrt_price in current_sqrt_prices: - current_sqrt_price = fixed_to_float(current_sqrt_price) + for id_, current_sqrt_price_bits in current_sqrt_prices: + current_sqrt_price = fixed_to_decimal(current_sqrt_price_bits) current_price = current_sqrt_price * current_sqrt_price - current_price_in_tao = Balance.from_rao(int(current_price * 1e9)) + current_price_in_tao = Balance.from_tao(float(current_price)) prices.update({id_: current_price_in_tao}) # SN0 price is always 1 TAO @@ -4025,7 +4029,7 @@ def get_unstake_fee( def get_vote_data( self, proposal_hash: str, block: Optional[int] = None - ) -> Optional["ProposalVoteData"]: + ) -> Optional[ProposalVoteData]: # TODO: is this all deprecated? Didn't subtensor senate stuff get removed? """ Retrieves the voting data for a specific proposal on the Bittensor blockchain. This data includes information @@ -4048,7 +4052,7 @@ def get_vote_data( block_hash=self.determine_block_hash(block), ) - if vote_data is None: + if vote_data.value is None: return None return ProposalVoteData.from_dict(vote_data.value) From ca36644d48303041d9fe1dbae7245428b6d40f98 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Wed, 15 Apr 2026 17:40:29 +0200 Subject: [PATCH 068/118] Checkin --- bittensor/core/async_subtensor.py | 103 ++++++++++---------- bittensor/core/chain_data/crowdloan_info.py | 7 +- bittensor/core/chain_data/utils.py | 3 +- bittensor/core/subtensor.py | 86 ++++++++-------- bittensor/core/types.py | 24 +++++ 5 files changed, 127 insertions(+), 96 deletions(-) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index f82c96f4c4..14fe9382db 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -149,6 +149,8 @@ Weights, PositionResponse, NeuronCertificateResponse, + CommitmentOfResponse, + CrowdloansResponse, ) from bittensor.utils import ( Certificate, @@ -346,9 +348,9 @@ async def __aexit__(self, exc_type, exc_val, exc_tb): async def _decode_crowdloan_entry( self, crowdloan_id: int, - data: dict, + data: CrowdloansResponse, block_hash: Optional[str] = None, - ) -> "CrowdloanInfo": + ) -> CrowdloanInfo: """ Internal helper to parse and decode a single Crowdloan record. @@ -640,8 +642,7 @@ async def get_hyperparameter( params=[netuid], block_hash=block_hash, ) - - return getattr(result, "value", result) + return result.value async def sim_swap( self, @@ -1054,14 +1055,14 @@ async def blocks_since_last_step( Notes: - """ - query = await self.query_subtensor( + query: ScaleType[int] = await self.query_subtensor( name="BlocksSinceLastStep", block=block, block_hash=block_hash, reuse_block=reuse_block, params=[netuid], ) - return cast(Optional[int], getattr(query, "value", query)) + return query.value async def blocks_since_last_update( self, @@ -1085,14 +1086,14 @@ async def blocks_since_last_update( """ block_hash = await self.determine_block_hash(block, block_hash, reuse_block) block = block or await self.substrate.get_block_number(block_hash) - call = await self.get_hyperparameter( + call: list[int] = await self.get_hyperparameter( param_name="LastUpdate", netuid=netuid, block=block, block_hash=block_hash, reuse_block=reuse_block, ) - return None if call is None else (block - int(call[uid])) + return None if len(call) == 0 else (block - int(call[uid])) async def blocks_until_next_epoch( self, @@ -1176,8 +1177,10 @@ async def bonds( block_hash=block_hash, ) bond_map = [] + uid: int + bond: list[tuple[int, int]] async for uid, bond in b_map_encoded: - if bond is not None: + if len(bond) != 0: bond_map.append((uid, bond)) return bond_map @@ -1206,13 +1209,14 @@ async def commit_reveal_enabled( - """ block_hash = await self.determine_block_hash(block, block_hash, reuse_block) - call = await self.get_hyperparameter( + call: Optional[bool] = await self.get_hyperparameter( param_name="CommitRevealWeightsEnabled", block_hash=block_hash, netuid=netuid, reuse_block=reuse_block, ) - return True if call is True else False + assert call is not None + return call async def difficulty( self, @@ -1220,7 +1224,7 @@ async def difficulty( block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> Optional[int]: + ) -> int: """Retrieves the 'Difficulty' hyperparameter for a specified subnet in the Bittensor network. This parameter determines the computational challenge required for neurons to participate in consensus and @@ -1245,14 +1249,13 @@ async def difficulty( - """ block_hash = await self.determine_block_hash(block, block_hash, reuse_block) - call = await self.get_hyperparameter( + call: Optional[int] = await self.get_hyperparameter( param_name="Difficulty", netuid=netuid, block_hash=block_hash, reuse_block=reuse_block, ) - if call is None: - return None + assert call is not None return int(call) async def does_hotkey_exist( @@ -1284,19 +1287,13 @@ async def does_hotkey_exist( - """ block_hash = await self.determine_block_hash(block, block_hash, reuse_block) - result = await self.substrate.query( + result: ScaleType[str] = await self.substrate.query( module="SubtensorModule", storage_function="Owner", params=[hotkey_ss58], block_hash=block_hash, ) - return_val = ( - False - if result is None - # not the default key (0x0) - else result != "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM" - ) - return return_val + return result.value != "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM" async def get_admin_freeze_window( self, @@ -1323,19 +1320,19 @@ async def get_admin_freeze_window( - """ block_hash = await self.determine_block_hash(block, block_hash, reuse_block) - query = await self.substrate.query( + query: ScaleType[int] = await self.substrate.query( module="SubtensorModule", storage_function="AdminFreezeWindow", block_hash=block_hash, ) - return cast(int, getattr(query, "value", query)) + return query.value async def get_all_subnets_info( self, block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> list["SubnetInfo"]: + ) -> list[SubnetInfo]: """Retrieves detailed information about all subnets within the Bittensor network. Parameters: @@ -1410,6 +1407,8 @@ async def get_all_commitments( reuse_block=reuse_block, ) result = {} + id_: str + value: CommitmentOfResponse async for id_, value in query: try: result[id_] = decode_metadata(value) @@ -2242,13 +2241,11 @@ async def get_commitment( ) return "" - metadata = cast( - dict, - await self.get_commitment_metadata( - netuid, hotkey, block, block_hash, reuse_block - ), + metadata = await self.get_commitment_metadata( + netuid, hotkey, block, block_hash, reuse_block ) try: + assert not isinstance(metadata, str) return decode_metadata(metadata) except Exception as error: logging.error(error) @@ -2261,7 +2258,7 @@ async def get_commitment_metadata( block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> Union[str, dict]: + ) -> str | CommitmentOfResponse: # TODO: how to handle return data? need good example @roman """Fetches raw commitment metadata from specific subnet for given hotkey. @@ -2280,15 +2277,17 @@ async def get_commitment_metadata( - """ block_hash = await self.determine_block_hash(block, block_hash, reuse_block) - commit_data = await self.substrate.query( + commit_data: ScaleType[ + Optional[CommitmentOfResponse] + ] = await self.substrate.query( module="Commitments", storage_function="CommitmentOf", params=[netuid, hotkey_ss58], block_hash=block_hash, ) - if commit_data is None: + if commit_data.value is None: return "" - return cast(Union[str, dict], getattr(commit_data, "value", commit_data)) + return commit_data.value async def get_crowdloan_constants( self, @@ -2296,7 +2295,7 @@ async def get_crowdloan_constants( block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> "CrowdloanConstants": + ) -> CrowdloanConstants: """Retrieves runtime configuration constants governing crowdloan behavior and limits on the Bittensor blockchain. If a list of constant names is provided, only those constants will be queried. @@ -2350,7 +2349,7 @@ async def get_crowdloan_contributions( block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> dict[str, "Balance"]: + ) -> dict[str, Balance]: """Retrieves all contributions made to a specific crowdloan campaign. Returns a mapping of contributor coldkey addresses to their contribution amounts in Rao. @@ -2380,11 +2379,11 @@ async def get_crowdloan_contributions( ) result = {} - - if query.records: - async for record in query: - if record[1]: - result[record[0]] = Balance.from_rao(record[1]) + contributor: str + amount: int + async for contributor, amount in query: + if amount: + result[contributor] = Balance.from_rao(amount) return result @@ -2394,7 +2393,7 @@ async def get_crowdloan_by_id( block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> Optional["CrowdloanInfo"]: + ) -> Optional[CrowdloanInfo]: """Retrieves detailed information about a specific crowdloan campaign. Parameters: @@ -2414,13 +2413,13 @@ async def get_crowdloan_by_id( - Crowdloans Overview: """ block_hash = await self.determine_block_hash(block, block_hash, reuse_block) - query = await self.substrate.query( + query: ScaleType[Optional[CrowdloansResponse]] = await self.substrate.query( module="Crowdloan", storage_function="Crowdloans", params=[crowdloan_id], block_hash=block_hash, ) - if not query: + if not query.value: return None return await self._decode_crowdloan_entry( crowdloan_id=crowdloan_id, data=query.value, block_hash=block_hash @@ -2450,13 +2449,12 @@ async def get_crowdloan_next_id( - Crowdloan Tutorial: """ block_hash = await self.determine_block_hash(block, block_hash, reuse_block) - result = await self.substrate.query( + result: ScaleType[int] = await self.substrate.query( module="Crowdloan", storage_function="NextCrowdloanId", block_hash=block_hash, ) - value = getattr(result, "value", result) - return int(value or 0) + return result.value async def get_crowdloans( self, @@ -3271,10 +3269,10 @@ async def get_mev_shield_current_key( block_hash=block_hash, ) - if query.value is None: + if query.value_object is None: return None - value: bytearray = query.value + value: bytearray = query.value_object public_key_bytes = bytes(value) @@ -3318,10 +3316,11 @@ async def get_mev_shield_next_key( block_hash=block_hash, ) - if query.value is None: + if query.value_object is None: return None - public_key_bytes = bytes(query.value_object) + value: bytearray = query.value_object + public_key_bytes = bytes(value) # Validate public_key size for ML-KEM-768 (must be exactly 1184 bytes) if len(public_key_bytes) != MLKEM768_PUBLIC_KEY_SIZE: diff --git a/bittensor/core/chain_data/crowdloan_info.py b/bittensor/core/chain_data/crowdloan_info.py index db7185b78d..51951287af 100644 --- a/bittensor/core/chain_data/crowdloan_info.py +++ b/bittensor/core/chain_data/crowdloan_info.py @@ -1,8 +1,11 @@ from dataclasses import dataclass -from typing import Optional +from typing import Optional, TYPE_CHECKING from bittensor.utils.balance import Balance +if TYPE_CHECKING: + from bittensor.core.types import CrowdloansResponse + @dataclass class CrowdloanInfo: @@ -42,7 +45,7 @@ class CrowdloanInfo: contributors_count: int @classmethod - def from_dict(cls, idx: int, data: dict) -> "CrowdloanInfo": + def from_dict(cls, idx: int, data: "CrowdloansResponse") -> "CrowdloanInfo": """Returns a CrowdloanInfo object from decoded chain data.""" return cls( id=idx, diff --git a/bittensor/core/chain_data/utils.py b/bittensor/core/chain_data/utils.py index 2d991319b3..0f142d597e 100644 --- a/bittensor/core/chain_data/utils.py +++ b/bittensor/core/chain_data/utils.py @@ -11,6 +11,7 @@ if TYPE_CHECKING: from async_substrate_interface.sync_substrate import QueryMapResult + from bittensor.core.types import CommitmentOfResponse class ChainDataType(Enum): @@ -115,7 +116,7 @@ def process_stake_data(stake_data: list) -> dict: return decoded_stake_data -def decode_metadata(metadata: dict) -> str: +def decode_metadata(metadata: "CommitmentOfResponse") -> str: commitment = metadata["info"]["fields"][0] if isinstance(commitment, str): return "" diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 48340e1668..5c27ae0e4a 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -148,6 +148,8 @@ Weights, PositionResponse, NeuronCertificateResponse, + CommitmentOfResponse, + CrowdloansResponse, ) from bittensor.utils import ( Certificate, @@ -288,7 +290,7 @@ def __exit__(self, exc_type, exc_val, exc_tb): def _decode_crowdloan_entry( self, crowdloan_id: int, - data: dict, + data: CrowdloansResponse, block_hash: Optional[str] = None, ) -> "CrowdloanInfo": """ @@ -299,6 +301,7 @@ def _decode_crowdloan_entry( call_data = data.get("call") if call_data and "Inline" in call_data: try: + # TODO need a working crowdloan call to see what this actually is, but I probably need to just remove this section inline_bytes = bytes(call_data["Inline"][0][0]) decoded_call = self.substrate.create_scale_object( type_string="Call", @@ -861,10 +864,10 @@ def blocks_since_last_step( Notes: - """ - query = self.query_subtensor( + query: ScaleType[int] = self.query_subtensor( name="BlocksSinceLastStep", block=block, params=[netuid] ) - return cast(Optional[int], getattr(query, "value", query)) + return query.value def blocks_since_last_update( self, netuid: int, uid: int, block: Optional[int] = None @@ -880,10 +883,11 @@ def blocks_since_last_update( The number of blocks since the last update, or None if the subnetwork or UID does not exist. """ block = block or self.get_current_block() - call = self.get_hyperparameter( + call: Optional[list[int]] = self.get_hyperparameter( param_name="LastUpdate", netuid=netuid, block=block ) - return None if not call else (block - int(call[uid])) + assert call is not None + return None if len(call) == 0 else (block - int(call[uid])) def blocks_until_next_epoch( self, netuid: int, tempo: Optional[int] = None, block: Optional[int] = None @@ -953,8 +957,10 @@ def bonds( block_hash=self.determine_block_hash(block), ) bond_map = [] + uid: int + bond: list[tuple[int, int]] for uid, bond in b_map_encoded: - if bond.value is not None: + if len(bond) != 0: bond_map.append((uid, bond)) return bond_map @@ -973,12 +979,13 @@ def commit_reveal_enabled(self, netuid: int, block: Optional[int] = None) -> boo - - """ - call = self.get_hyperparameter( + call: Optional[bool] = self.get_hyperparameter( param_name="CommitRevealWeightsEnabled", block=block, netuid=netuid ) - return True if call is True else False + assert call is not None + return call - def difficulty(self, netuid: int, block: Optional[int] = None) -> Optional[int]: + def difficulty(self, netuid: int, block: Optional[int] = None) -> int: """Retrieves the 'Difficulty' hyperparameter for a specified subnet in the Bittensor network. This parameter determines the computational challenge required for neurons to participate in consensus and @@ -998,11 +1005,10 @@ def difficulty(self, netuid: int, block: Optional[int] = None) -> Optional[int]: - - """ - call = self.get_hyperparameter( + call: Optional[int] = self.get_hyperparameter( param_name="Difficulty", netuid=netuid, block=block ) - if call is None: - return None + assert call is not None return int(call) def does_hotkey_exist(self, hotkey_ss58: str, block: Optional[int] = None) -> bool: @@ -1024,19 +1030,13 @@ def does_hotkey_exist(self, hotkey_ss58: str, block: Optional[int] = None) -> bo Notes: - """ - result = self.substrate.query( + result: ScaleType[str] = self.substrate.query( module="SubtensorModule", storage_function="Owner", params=[hotkey_ss58], block_hash=self.determine_block_hash(block), ) - return_val = ( - False - if result is None - # not the default key (0x0) - else result != "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM" - ) - return return_val + return result.value != "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM" def get_admin_freeze_window(self, block: Optional[int] = None) -> int: """Returns the duration, in blocks, of the administrative freeze window at the end of each epoch. @@ -1055,14 +1055,14 @@ def get_admin_freeze_window(self, block: Optional[int] = None) -> int: - """ - query = self.substrate.query( + query: ScaleType[int] = self.substrate.query( module="SubtensorModule", storage_function="AdminFreezeWindow", block_hash=self.determine_block_hash(block), ) - return cast(int, getattr(query, "value", query)) + return query.value - def get_all_subnets_info(self, block: Optional[int] = None) -> list["SubnetInfo"]: + def get_all_subnets_info(self, block: Optional[int] = None) -> list[SubnetInfo]: """Retrieves detailed information about all subnets within the Bittensor network. Parameters: @@ -1116,6 +1116,8 @@ def get_all_commitments( block=block, ) result = {} + id_: str + value: CommitmentOfResponse for id_, value in query: try: result[id_] = decode_metadata(value) @@ -1825,8 +1827,9 @@ def get_commitment(self, netuid: int, uid: int, block: Optional[int] = None) -> ) return "" - metadata = cast(dict, self.get_commitment_metadata(netuid, hotkey, block)) + metadata = self.get_commitment_metadata(netuid, hotkey, block) try: + assert not isinstance(metadata, str) return decode_metadata(metadata) except Exception as error: logging.error(error) @@ -1834,7 +1837,7 @@ def get_commitment(self, netuid: int, uid: int, block: Optional[int] = None) -> def get_commitment_metadata( self, netuid: int, hotkey_ss58: str, block: Optional[int] = None - ) -> Union[str, dict]: + ) -> str | CommitmentOfResponse: # TODO: how to handle return data? need good example @roman """Fetches raw commitment metadata from specific subnet for given hotkey. @@ -1850,21 +1853,21 @@ def get_commitment_metadata( Notes: - """ - commit_data = self.substrate.query( + commit_data: ScaleType[Optional[CommitmentOfResponse]] = self.substrate.query( module="Commitments", storage_function="CommitmentOf", params=[netuid, hotkey_ss58], block_hash=self.determine_block_hash(block), ) - if commit_data is None: + if commit_data.value is None: return "" - return cast(Union[str, dict], getattr(commit_data, "value", commit_data)) + return commit_data.value def get_crowdloan_constants( self, constants: Optional[list[str]] = None, block: Optional[int] = None, - ) -> "CrowdloanConstants": + ) -> CrowdloanConstants: """Retrieves runtime configuration constants governing crowdloan behavior and limits on the Bittensor blockchain. If a list of constant names is provided, only those constants will be queried. @@ -1911,7 +1914,7 @@ def get_crowdloan_contributions( self, crowdloan_id: int, block: Optional[int] = None, - ) -> dict[str, "Balance"]: + ) -> dict[str, Balance]: """Retrieves all contributions made to a specific crowdloan campaign. Returns a mapping of contributor coldkey addresses to their contribution amounts in Rao. @@ -1938,14 +1941,16 @@ def get_crowdloan_contributions( block_hash=block_hash, ) result = {} - for contributor, amount in query.records: + contributor: str + amount: int + for contributor, amount in query: if amount: result[contributor] = Balance.from_rao(amount) return result def get_crowdloan_by_id( self, crowdloan_id: int, block: Optional[int] = None - ) -> Optional["CrowdloanInfo"]: + ) -> Optional[CrowdloanInfo]: """Retrieves detailed information about a specific crowdloan campaign. Parameters: @@ -1963,13 +1968,13 @@ def get_crowdloan_by_id( - Crowdloans Overview: """ block_hash = self.determine_block_hash(block) - query = self.substrate.query( + query: ScaleType[Optional[CrowdloansResponse]] = self.substrate.query( module="Crowdloan", storage_function="Crowdloans", params=[crowdloan_id], block_hash=block_hash, ) - if not query: + if not query.value: return None return self._decode_crowdloan_entry( crowdloan_id=crowdloan_id, data=query.value, block_hash=block_hash @@ -1995,13 +2000,12 @@ def get_crowdloan_next_id( - Crowdloan Tutorial: """ block_hash = self.determine_block_hash(block) - result = self.substrate.query( + result: ScaleType[int] = self.substrate.query( module="Crowdloan", storage_function="NextCrowdloanId", block_hash=block_hash, ) - value = cast(int, getattr(result, "value", result)) - return int(value) or 0 + return result.value def get_crowdloans( self, @@ -2699,10 +2703,10 @@ def get_mev_shield_current_key( block_hash=block_hash, ) - if query.value is None: + if query.value_object is None: return None - value: bytearray = query.value + value: bytearray = query.value_object public_key_bytes = bytes(value) @@ -2739,10 +2743,10 @@ def get_mev_shield_next_key(self, block: Optional[int] = None) -> Optional[bytes block_hash=block_hash, ) - if query.value is None: + if query.value_object is None: return None - value: bytearray = query.value + value: bytearray = query.value_object public_key_bytes = bytes(value) diff --git a/bittensor/core/types.py b/bittensor/core/types.py index 975f120ee2..18d9a9cb9f 100644 --- a/bittensor/core/types.py +++ b/bittensor/core/types.py @@ -592,3 +592,27 @@ class PositionResponse(TypedDict): class NeuronCertificateResponse(TypedDict): public_key: str algorithm: int + + +class _CommitmentFields(TypedDict): + fields: list[dict[str, str]] + + +class CommitmentOfResponse(TypedDict): + deposit: int + block: int + info: _CommitmentFields + + +class CrowdloansResponse(TypedDict): + creator: str + deposit: int + min_contribution: int + end: int + cap: int + funds_account: str + raised: int + target_address: str + call: Optional[dict] + finalized: bool + contributors_count: int From ddaaf294b3ac6b721606931f3ac34cf5b0a984f3 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Wed, 15 Apr 2026 18:16:26 +0200 Subject: [PATCH 069/118] Checkin --- bittensor/core/async_subtensor.py | 81 +++++++++++++++---------------- bittensor/core/subtensor.py | 39 ++++++++------- 2 files changed, 57 insertions(+), 63 deletions(-) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index 14fe9382db..8b877abd93 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -528,7 +528,7 @@ async def _query_with_fallback( *args: tuple[str, str, Optional[list[Any]]], block_hash: Optional[str] = None, default_value: Any = ValueError, - ): + ) -> ScaleType[Any] | Any: """ Queries the subtensor node with a given set of args, falling back to the next group if the method does not exist at the given block. This method exists to support backwards compatibility for blocks. @@ -2490,17 +2490,14 @@ async def get_crowdloans( ) crowdloans = [] - - if query.records: - async for c_id, value_obj in query: - data = value_obj - if not data: - continue - crowdloans.append( - await self._decode_crowdloan_entry( - crowdloan_id=c_id, data=data, block_hash=block_hash - ) + c_id: int + data: CrowdloansResponse + async for c_id, data in query: + crowdloans.append( + await self._decode_crowdloan_entry( + crowdloan_id=c_id, data=data, block_hash=block_hash ) + ) return crowdloans @@ -2608,14 +2605,14 @@ async def get_delegate_take( - """ block_hash = await self.determine_block_hash(block, block_hash, reuse_block) - result = await self.query_subtensor( + result: ScaleType[int] = await self.query_subtensor( name="Delegates", block_hash=block_hash, reuse_block=reuse_block, params=[hotkey_ss58], ) - return u16_normalized_float(result.value) # type: ignore + return u16_normalized_float(result.value) async def get_delegated( self, @@ -2721,7 +2718,7 @@ async def get_existential_deposit( - """ block_hash = await self.determine_block_hash(block, block_hash, reuse_block) - result = await self.substrate.get_constant( + result: Optional[ScaleType[int]] = await self.substrate.get_constant( module_name="Balances", constant_name="ExistentialDeposit", block_hash=block_hash, @@ -2730,7 +2727,7 @@ async def get_existential_deposit( if result is None: raise Exception("Unable to retrieve existential deposit amount.") - return Balance.from_rao(getattr(result, "value", 0)) + return Balance.from_rao(result.value) async def get_ema_tao_inflow( self, @@ -2761,7 +2758,7 @@ async def get_ema_tao_inflow( - EMA smoothing: """ block_hash = await self.determine_block_hash(block) - query = await self.substrate.query( + query: ScaleType[Optional[tuple[int, FixedPoint]]] = await self.substrate.query( module="SubtensorModule", storage_function="SubnetEmaTaoFlow", params=[netuid], @@ -2769,11 +2766,12 @@ async def get_ema_tao_inflow( ) # sn0 doesn't have EmaTaoInflow - if query is None: + if query.value is None: return None block_updated, tao_bits = query.value ema_value = int(fixed_to_float(tao_bits)) + # TODO verify this from rao, seems like we're just rounding down return block_updated, Balance.from_rao(ema_value) async def get_hotkey_owner( @@ -2803,17 +2801,15 @@ async def get_hotkey_owner( - """ block_hash = await self.determine_block_hash(block, block_hash, reuse_block) - hk_owner_query = await self.substrate.query( + hk_owner: ScaleType[str] = await self.substrate.query( module="SubtensorModule", storage_function="Owner", params=[hotkey_ss58], block_hash=block_hash, ) - exists = False - if hk_owner_query: - exists = await self.does_hotkey_exist(hotkey_ss58, block_hash=block_hash) - hotkey_owner = hk_owner_query if exists else None - return cast(Optional[str], getattr(hotkey_owner, "value", hotkey_owner)) + exists = await self.does_hotkey_exist(hotkey_ss58, block_hash=block_hash) + hotkey_owner = hk_owner.value if exists else None + return hotkey_owner async def get_last_bonds_reset( self, @@ -2822,7 +2818,7 @@ async def get_last_bonds_reset( block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> ScaleType[int]: + ) -> ScaleType[Optional[int]]: """Retrieves the block number when bonds were last reset for a specific hotkey on a subnet. Parameters: @@ -2833,14 +2829,15 @@ async def get_last_bonds_reset( reuse_block: Whether to use the last-used block. Do not set if using `block_hash` or `block`. Returns: - The block number when bonds were last reset, or `None` if no bonds reset has occurred. + A ScaleType object containing the block number when bonds were last reset, or `None` if no bonds reset + has occurred. Notes: - - """ block_hash = await self.determine_block_hash(block, block_hash, reuse_block) - block = await self.substrate.query( + block: ScaleType[Optional[int]] = await self.substrate.query( module="Commitments", storage_function="LastBondsReset", params=[netuid, hotkey_ss58], @@ -2881,7 +2878,7 @@ async def get_last_commitment_bonds_reset_block( block_data = await self.get_last_bonds_reset( netuid, hotkey, block, block_hash, reuse_block ) - return getattr(block_data, "value", None) + return block_data.value async def get_liquidity_list( self, @@ -3000,7 +2997,7 @@ async def get_liquidity_list( ) # iterator with just the values ticks = iter([x[1] for x in ticks_query]) - positions = [] + positions: list[LiquidityPosition] = [] for position, tick_low_idx, tick_high_idx in positions_values: tick_low = next(ticks) tick_high = next(ticks) @@ -3056,19 +3053,15 @@ async def get_liquidity_list( positions.append( LiquidityPosition( - **{ - "id": position.get("id"), - "price_low": Balance.from_tao( - tick_to_price(position.get("tick_low")) - ), - "price_high": Balance.from_tao( - tick_to_price(position.get("tick_high")) - ), - "liquidity": Balance.from_rao(position.get("liquidity")), - "fees_tao": fees_tao, - "fees_alpha": fees_alpha, - "netuid": position.get("netuid"), - } + id=position.get("id"), + price_low=Balance.from_tao(tick_to_price(position.get("tick_low"))), + price_high=Balance.from_tao( + tick_to_price(position.get("tick_high")) + ), + liquidity=Balance.from_rao(position.get("liquidity")), + fees_tao=fees_tao, + fees_alpha=fees_alpha, + netuid=position.get("netuid"), ) ) @@ -3097,12 +3090,14 @@ async def get_mechanism_emission_split( - """ block_hash = await self.determine_block_hash(block, block_hash, reuse_block) - result: ScaleType[Optional[list[int]]] = await self._query_with_fallback( + result: Optional[ + ScaleType[Optional[list[int]]] + ] = await self._query_with_fallback( ("SubtensorModule", "MechanismEmissionSplit", [netuid]), block_hash=block_hash, default_value=None, ) - if result.value is None: + if result is None or result.value is None: return None total = sum(result.value) return [round(i / total * 100) for i in result.value] diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 5c27ae0e4a..9afa8a3ce5 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -2037,11 +2037,9 @@ def get_crowdloans( ) crowdloans = [] - - for c_id, value_obj in getattr(query, "records", []): - data = value_obj.value - if not data: - continue + c_id: int + data: CrowdloansResponse + for c_id, data in query: crowdloans.append( self._decode_crowdloan_entry( crowdloan_id=c_id, data=data, block_hash=block_hash @@ -2169,7 +2167,7 @@ def get_delegated( return DelegatedInfo.list_from_dicts(result) - def get_delegates(self, block: Optional[int] = None) -> list["DelegateInfo"]: + def get_delegates(self, block: Optional[int] = None) -> list[DelegateInfo]: """Fetches all delegates registered on the chain. Delegates are validators that accept stake from other TAO holders (nominators/delegators). This method @@ -2197,7 +2195,7 @@ def get_delegates(self, block: Optional[int] = None) -> list["DelegateInfo"]: else: return [] - def get_existential_deposit(self, block: Optional[int] = None) -> Optional[Balance]: + def get_existential_deposit(self, block: Optional[int] = None) -> Balance: """Retrieves the existential deposit amount for the Bittensor blockchain. The existential deposit is the minimum amount of TAO required for an account to exist on the blockchain. @@ -2213,7 +2211,7 @@ def get_existential_deposit(self, block: Optional[int] = None) -> Optional[Balan Notes: - """ - result = self.substrate.get_constant( + result: Optional[ScaleType[int]] = self.substrate.get_constant( module_name="Balances", constant_name="ExistentialDeposit", block_hash=self.determine_block_hash(block), @@ -2222,7 +2220,7 @@ def get_existential_deposit(self, block: Optional[int] = None) -> Optional[Balan if result is None: raise Exception("Unable to retrieve existential deposit amount.") - return Balance.from_rao(getattr(result, "value", 0)) + return Balance.from_rao(result.value) def get_ema_tao_inflow( self, @@ -2266,6 +2264,7 @@ def get_ema_tao_inflow( block_updated, tao_bits = query.value ema_value = int(fixed_to_float(tao_bits)) + # TODO verify this from rao, seems like we're just rounding down return block_updated, Balance.from_rao(ema_value) def get_hotkey_owner( @@ -2283,21 +2282,19 @@ def get_hotkey_owner( Returns: The SS58 address of the owner if the hotkey exists, or `None` if it doesn't. """ - hk_owner_query = self.substrate.query( + hk_owner: ScaleType[str] = self.substrate.query( module="SubtensorModule", storage_function="Owner", params=[hotkey_ss58], block_hash=self.determine_block_hash(block), ) - exists = False - if hk_owner_query: - exists = self.does_hotkey_exist(hotkey_ss58, block=block) - hotkey_owner = hk_owner_query if exists else None - return cast(Optional[str], getattr(hotkey_owner, "value", hotkey_owner)) + exists = self.does_hotkey_exist(hotkey_ss58, block=block) + hotkey_owner = hk_owner.value if exists else None + return hotkey_owner def get_last_bonds_reset( self, netuid: int, hotkey_ss58: str, block: Optional[int] = None - ): + ) -> ScaleType[Optional[int]]: """Retrieves the block number when bonds were last reset for a specific hotkey on a subnet. Parameters: @@ -2306,18 +2303,20 @@ def get_last_bonds_reset( block: The block number to query. If `None`, queries the current chain head. Returns: - The block number when bonds were last reset, or `None` if no bonds reset has occurred. + A ScaleType object containing the block number when bonds were last reset, or `None` if no bonds reset + has occurred. Notes: - - """ - return self.substrate.query( + block_: ScaleType[Optional[int]] = self.substrate.query( module="Commitments", storage_function="LastBondsReset", params=[netuid, hotkey_ss58], block_hash=self.determine_block_hash(block), ) + return block_ def get_last_commitment_bonds_reset_block( self, @@ -2346,7 +2345,7 @@ def get_last_commitment_bonds_reset_block( ) return None block_data = self.get_last_bonds_reset(netuid, hotkey_ss58, block) - return getattr(block_data, "value", None) + return block_data.value def get_liquidity_list( self, @@ -2448,7 +2447,7 @@ def get_liquidity_list( ) # iterator with just the values ticks = iter([x[1] for x in ticks_query]) - positions = [] + positions: list[LiquidityPosition] = [] for position, tick_low_idx, tick_high_idx in positions_values: tick_low = next(ticks) tick_high = next(ticks) From d02a096aebe73e9ec6cbfb68722a900c51239695 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Wed, 15 Apr 2026 18:20:31 +0200 Subject: [PATCH 070/118] Checkin --- bittensor/core/async_subtensor.py | 9 ++++----- bittensor/core/subtensor.py | 7 +++---- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index 8b877abd93..a9823baf46 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -3326,7 +3326,7 @@ async def get_mev_shield_next_key( return public_key_bytes - async def get_minimum_required_stake(self): + async def get_minimum_required_stake(self) -> Balance: """Returns the minimum required stake threshold for nominator cleanup operations. This threshold is used ONLY for cleanup after unstaking operations. If a nominator's remaining stake @@ -3379,6 +3379,8 @@ async def get_netuids_for_hotkey( block_hash=block_hash, ) netuids = [] + netuid: int + is_member: bool if result.records: async for netuid, is_member in result: if is_member: @@ -3423,10 +3425,7 @@ async def get_neuron_certificate( ) certificate: Optional[NeuronCertificateResponse] = certificate_query.value if certificate is not None: - try: - return Certificate(certificate) - except AttributeError: - return None + return Certificate(certificate) return None async def get_neuron_for_pubkey_and_subnet( diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 9afa8a3ce5..77e746b588 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -2804,6 +2804,8 @@ def get_netuids_for_hotkey( block_hash=self.determine_block_hash(block), ) netuids = [] + netuid: int + is_member: bool if result.records: for netuid, is_member in result: if is_member: @@ -2838,10 +2840,7 @@ def get_neuron_certificate( ) certificate: Optional[str | NeuronCertificateResponse] = certificate_query.value if certificate is not None: - try: - return Certificate(certificate) - except AttributeError: - return None + return Certificate(certificate) return None def get_neuron_for_pubkey_and_subnet( From 3a9e0c0cf36b1d645bee77f2b2c9a79bf96b320d Mon Sep 17 00:00:00 2001 From: BD Himes Date: Wed, 15 Apr 2026 20:31:44 +0200 Subject: [PATCH 071/118] Async unit tests passing --- bittensor/core/async_subtensor.py | 7 +- bittensor/core/subtensor.py | 5 +- .../chain_data/test_coldkey_swap.py | 14 +- tests/unit_tests/test_async_subtensor.py | 178 +++++++++--------- 4 files changed, 101 insertions(+), 103 deletions(-) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index a9823baf46..ff0f1bec8a 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -2807,7 +2807,10 @@ async def get_hotkey_owner( params=[hotkey_ss58], block_hash=block_hash, ) - exists = await self.does_hotkey_exist(hotkey_ss58, block_hash=block_hash) + if hk_owner.value != "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM": + exists = await self.does_hotkey_exist(hotkey_ss58, block_hash=block_hash) + else: + exists = False hotkey_owner = hk_owner.value if exists else None return hotkey_owner @@ -5587,7 +5590,7 @@ async def query_identity( ) identity_data: Optional[dict[str, Any]] = identity_info.value - if identity_info is None: + if identity_data is None: return None return ChainIdentity.from_dict(decode_hex_identity_dict(identity_data)) diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 77e746b588..d3ae97eb45 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -2288,7 +2288,10 @@ def get_hotkey_owner( params=[hotkey_ss58], block_hash=self.determine_block_hash(block), ) - exists = self.does_hotkey_exist(hotkey_ss58, block=block) + if hk_owner.value != "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM": + exists = self.does_hotkey_exist(hotkey_ss58, block=block) + else: + exists = False hotkey_owner = hk_owner.value if exists else None return hotkey_owner diff --git a/tests/unit_tests/chain_data/test_coldkey_swap.py b/tests/unit_tests/chain_data/test_coldkey_swap.py index db93a8ee6e..f5a2aa33b6 100644 --- a/tests/unit_tests/chain_data/test_coldkey_swap.py +++ b/tests/unit_tests/chain_data/test_coldkey_swap.py @@ -10,7 +10,7 @@ def test_coldkey_swap_announcement_info_from_query_none(mocker): """Test from_query returns None when query has no value.""" # Prep coldkey_ss58 = mocker.Mock(spec=str) - query = None + query = mocker.Mock(spec=ScaleType, value=None) # Call from_query = ColdkeySwapAnnouncementInfo.from_query(coldkey_ss58, query) @@ -24,23 +24,17 @@ def test_coldkey_swap_announcement_info_from_query_happy_path(mocker): # Prep coldkey_ss58 = mocker.Mock(spec=str) fake_block = mocker.Mock(spec=int) - fake_hash_data = mocker.Mock(spec=list) - query = mocker.Mock(value=(fake_block, (fake_hash_data,))) - - mocked_bytes = mocker.patch("bittensor.core.chain_data.coldkey_swap.bytes") + fake_hash_data = mocker.Mock(spec=str) + query = mocker.Mock(value=(fake_block, fake_hash_data)) # Call from_query = ColdkeySwapAnnouncementInfo.from_query(coldkey_ss58, query) # Asserts - mocked_bytes.assert_called_once_with(fake_hash_data) assert from_query is not None, "Should return ColdkeySwapAnnouncementInfo object" assert from_query.coldkey == coldkey_ss58 assert from_query.execution_block == fake_block - assert ( - from_query.new_coldkey_hash - == mocked_bytes.return_value.hex.return_value.__radd__.return_value - ) + assert from_query.new_coldkey_hash == fake_hash_data def test_coldkey_swap_dispute_info_from_query_none(mocker): diff --git a/tests/unit_tests/test_async_subtensor.py b/tests/unit_tests/test_async_subtensor.py index dda5957a91..9e4fadfe3b 100644 --- a/tests/unit_tests/test_async_subtensor.py +++ b/tests/unit_tests/test_async_subtensor.py @@ -583,7 +583,7 @@ async def test_get_balance(subtensor, mocker): block_hash=mocked_determine_block_hash.return_value, ) mocked_balance.assert_called_once_with( - subtensor.substrate.query.return_value.__getitem__.return_value.__getitem__.return_value + subtensor.substrate.query.return_value.value.__getitem__.return_value.__getitem__.return_value ) assert result == mocked_balance.return_value @@ -1050,7 +1050,7 @@ async def test_get_neuron_for_pubkey_and_subnet_uid_not_found(subtensor, mocker) mocker.patch.object( subtensor.substrate, "query", - return_value=None, + return_value=mocker.Mock(spec=ScaleType, value=None), ) mocked_get_null_neuron = mocker.patch.object( async_subtensor.NeuronInfo, "get_null_neuron", return_value="null_neuron" @@ -1361,38 +1361,11 @@ async def test_query_identity_no_info(subtensor, mocker): # Preps fake_coldkey_ss58 = "test_key" - mocked_query = mocker.AsyncMock(return_value=None) - subtensor.substrate.query = mocked_query - - # Call - result = await subtensor.query_identity(coldkey_ss58=fake_coldkey_ss58) - - # Asserts - mocked_query.assert_called_once_with( - module="SubtensorModule", - storage_function="IdentitiesV2", - params=[fake_coldkey_ss58], - block_hash=None, + mocked_query = mocker.AsyncMock( + return_value=mocker.Mock(spec=ScaleType, value=None) ) - assert result is None - - -@pytest.mark.asyncio -async def test_query_identity_type_error(subtensor, mocker): - """Tests query_identity method when a TypeError occurs during decoding.""" - # Preps - fake_coldkey_ss58 = "test_key" - fake_identity_info = {"info": {"rank": (b"\xff\xfe",)}} - - mocked_query = mocker.AsyncMock(return_value=fake_identity_info) subtensor.substrate.query = mocked_query - mocker.patch.object( - async_subtensor, - "decode_hex_identity_dict", - side_effect=TypeError, - ) - # Call result = await subtensor.query_identity(coldkey_ss58=fake_coldkey_ss58) @@ -1497,7 +1470,9 @@ async def test_does_hotkey_exist_false_for_specific_account(subtensor, mocker): """Tests does_hotkey_exist method when the hotkey exists but matches the specific account ID to ignore.""" # Preps fake_hotkey_ss58 = "fake_hotkey" - fake_query_result = "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM" + fake_query_result = mocker.Mock( + spec=ScaleType, value="5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM" + ) mocked_query = mocker.patch.object( subtensor.substrate, "query", return_value=fake_query_result @@ -1523,7 +1498,9 @@ async def test_get_hotkey_owner_successful(subtensor, mocker): fake_hotkey_ss58 = "valid_hotkey" fake_block_hash = "block_hash" - mocked_query = mocker.AsyncMock(return_value="decoded_owner_account_id") + mocked_query = mocker.AsyncMock( + return_value=mocker.Mock(spec=ScaleType, value="decoded_owner_account_id") + ) subtensor.substrate.query = mocked_query mocked_does_hotkey_exist = mocker.AsyncMock(return_value=True) @@ -1554,7 +1531,11 @@ async def test_get_hotkey_owner_non_existent_hotkey(subtensor, mocker): fake_hotkey_ss58 = "non_existent_hotkey" fake_block_hash = "block_hash" - mocked_query = mocker.AsyncMock(return_value=None) + mocked_query = mocker.AsyncMock( + return_value=mocker.Mock( + spec=ScaleType, value="5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM" + ) + ) subtensor.substrate.query = mocked_query # Call @@ -1939,7 +1920,9 @@ async def test_get_parents_no_parents(subtensor, mocker): fake_netuid = 1 fake_parents = [] - mocked_query = mocker.AsyncMock(return_value=fake_parents) + mocked_query = mocker.AsyncMock( + return_value=mocker.Mock(spec=ScaleType, value=fake_parents) + ) subtensor.substrate.query = mocked_query # Call @@ -2100,7 +2083,9 @@ async def test_get_vote_data_success(subtensor, mocker): fake_block_hash = "block_hash" fake_vote_data = {"ayes": ["senate_member_1"], "nays": ["senate_member_2"]} - mocked_query = mocker.AsyncMock(return_value=fake_vote_data) + mocked_query = mocker.AsyncMock( + return_value=mocker.Mock(spec=ScaleType, value=fake_vote_data) + ) subtensor.substrate.query = mocked_query mocked_proposal_vote_data = mocker.Mock() @@ -2132,7 +2117,9 @@ async def test_get_vote_data_no_data(subtensor, mocker): fake_proposal_hash = "invalid_proposal_hash" fake_block_hash = "block_hash" - mocked_query = mocker.AsyncMock(return_value=None) + mocked_query = mocker.AsyncMock( + return_value=mocker.Mock(spec=ScaleType, value=None) + ) subtensor.substrate.query = mocked_query # Call @@ -2404,7 +2391,7 @@ async def test_blocks_since_last_update_no_last_update(subtensor, mocker): # Preps fake_netuid = 1 fake_uid = 5 - fake_result = None + fake_result = [] mocked_get_hyperparameter = mocker.patch.object( subtensor, @@ -2436,7 +2423,9 @@ async def test_commit_reveal_enabled(subtensor, mocker): netuid = 1 block_hash = "block_hash" mocked_get_hyperparameter = mocker.patch.object( - subtensor, "get_hyperparameter", return_value=mocker.AsyncMock() + subtensor, + "get_hyperparameter", + return_value=False, ) # Call @@ -2756,7 +2745,9 @@ async def test_get_owned_hotkeys_happy_path(subtensor, mocker): fake_coldkey = "fake_hotkey" fake_hotkey = "fake_hotkey" fake_hotkeys = [fake_hotkey] - mocked_subtensor = mocker.AsyncMock(return_value=fake_hotkeys) + mocked_subtensor = mocker.AsyncMock( + return_value=mocker.Mock(spec=ScaleType, value=fake_hotkeys) + ) mocker.patch.object(subtensor.substrate, "query", new=mocked_subtensor) # Call @@ -2777,7 +2768,9 @@ async def test_get_owned_hotkeys_return_empty(subtensor, mocker): """Tests that the output of get_owned_hotkeys is empty.""" # Prep fake_coldkey = "fake_hotkey" - mocked_subtensor = mocker.AsyncMock(return_value=[]) + mocked_subtensor = mocker.AsyncMock( + return_value=mocker.Mock(spec=ScaleType, value=[]) + ) mocker.patch.object(subtensor.substrate, "query", new=mocked_subtensor) # Call @@ -2895,7 +2888,7 @@ async def test_get_metagraph_info_all_fields(subtensor, mocker): mock_runtime_call = mocker.patch.object( subtensor.substrate, "runtime_call", - return_value=mocker.AsyncMock(value=mock_value), + return_value=mock_value, ) mock_chain_head = mocker.patch.object( subtensor.substrate, @@ -2949,7 +2942,7 @@ async def test_get_metagraph_info_specific_fields(subtensor, mocker): mock_runtime_call = mocker.patch.object( subtensor.substrate, "runtime_call", - return_value=mocker.AsyncMock(value=mock_value), + return_value=mock_value, ) mock_chain_head = mocker.patch.object( subtensor.substrate, @@ -3120,7 +3113,9 @@ async def test_blocks_since_last_step_is_none(subtensor, mocker): # preps netuid = 1 block = 123 - mocked_query_subtensor = mocker.AsyncMock(return_value=None) + mocked_query_subtensor = mocker.AsyncMock( + return_value=mocker.Mock(spec=ScaleType, value=None) + ) subtensor.query_subtensor = mocked_query_subtensor # call @@ -3480,10 +3475,10 @@ async def test_get_liquidity_list_happy_path(subtensor, fake_wallet, mocker): [ (2,), { - "id": (2,), + "id": 2, "netuid": 2, - "tick_low": (206189,), - "tick_high": (208196,), + "tick_low": 206189, + "tick_high": 208196, "liquidity": 1000000000000, "fees_tao": {"bits": 0}, "fees_alpha": {"bits": 0}, @@ -3492,10 +3487,10 @@ async def test_get_liquidity_list_happy_path(subtensor, fake_wallet, mocker): [ 2, { - "id": (2,), + "id": 2, "netuid": 2, - "tick_low": (216189,), - "tick_high": (198196,), + "tick_low": 216189, + "tick_high": 198196, "liquidity": 2000000000000, "fees_tao": {"bits": 0}, "fees_alpha": {"bits": 0}, @@ -3504,10 +3499,10 @@ async def test_get_liquidity_list_happy_path(subtensor, fake_wallet, mocker): [ 2, { - "id": (2,), + "id": 2, "netuid": 2, - "tick_low": (226189,), - "tick_high": (188196,), + "tick_low": 226189, + "tick_high": 188196, "liquidity": 3000000000000, "fees_tao": {"bits": 0}, "fees_alpha": {"bits": 0}, @@ -3756,11 +3751,8 @@ async def test_all_subnets(subtensor, mocker): "get_subnet_prices", return_value={0: Balance.from_tao(1), 1: Balance.from_tao(0.029258617)}, ) - mocked_decode = mocker.Mock(return_value=[{"netuid": 0}, {"netuid": 1}]) - mocked_runtime_call = mocker.Mock(decode=mocked_decode) - mocker.patch.object( - subtensor.substrate, "runtime_call", return_value=mocked_runtime_call - ) + mocked_decode = [{"netuid": 0}, {"netuid": 1}] + mocker.patch.object(subtensor.substrate, "runtime_call", return_value=mocked_decode) # Call result = await subtensor.all_subnets() @@ -4554,12 +4546,10 @@ async def test_get_crowdloan_contributions(mocker, subtensor): async def test_get_crowdloan_by_id(mocker, subtensor, query_return, expected_result): """Tests subtensor `get_crowdloan_by_id` method.""" # Preps - fake_crowdloan_id = mocker.Mock(spec=int) + fake_crowdloan_id = mocker.Mock(spec=ScaleType, value=mocker.Mock(spec=int)) mocked_determine_block_hash = mocker.patch.object(subtensor, "determine_block_hash") - mocked_query_return = ( - None if query_return is None else mocker.Mock(value=query_return) - ) + mocked_query_return = mocker.Mock(value=query_return) mocked_query = mocker.patch.object( subtensor.substrate, "query", return_value=mocked_query_return ) @@ -4698,23 +4688,21 @@ async def test_commit_weights_with_zero_max_attempts( @pytest.mark.parametrize( "fake_result, expected_result", [ - ({"Swap": ()}, "Swap"), - ({"Keep": ()}, "Keep"), + ("Swap", "Swap"), + ("Keep", "Keep"), ( { "KeepSubnets": { - "subnets": ( - ( - 2, - 3, - ), - ) + "subnets": [ + 2, + 3, + ], } }, {"KeepSubnets": {"subnets": [2, 3]}}, ), ( - {"KeepSubnets": {"subnets": ((2,),)}}, + {"KeepSubnets": {"subnets": [2]}}, { "KeepSubnets": { "subnets": [ @@ -4734,7 +4722,9 @@ async def test_get_root_claim_type(mocker, subtensor, fake_result, expected_resu fake_coldkey_ss58 = mocker.Mock(spec=str) mocked_determine_block_hash = mocker.patch.object(subtensor, "determine_block_hash") mocked_map = mocker.patch.object( - subtensor.substrate, "query", return_value=fake_result + subtensor.substrate, + "query", + return_value=mocker.Mock(spec=ScaleType, value=fake_result), ) # call @@ -4782,8 +4772,8 @@ async def test_get_root_claimable_all_rates(mocker, subtensor): # Preps hotkey_ss58 = mocker.Mock(spec=str) mocked_determine_block_hash = mocker.patch.object(subtensor, "determine_block_hash") - fake_value = [((14, {"bits": 6520190}),)] - fake_result = mocker.MagicMock(value=fake_value) + fake_value = [(14, {"bits": 6520190})] + fake_result = mocker.MagicMock(spec=ScaleType, value=fake_value) fake_result.__iter__ = fake_value mocked_query = mocker.patch.object( subtensor.substrate, "query", return_value=fake_result @@ -5128,7 +5118,7 @@ async def test_get_proxy_announcement(subtensor, mocker): params=[fake_delegate_account_ss58], block_hash=mocked_determine_block_hash.return_value, ) - mocked_from_dict.assert_called_once_with(mocked_query.return_value.value[0]) + mocked_from_dict.assert_called_once_with(mocked_query.return_value.value) assert result == mocked_from_dict.return_value @@ -5772,12 +5762,13 @@ async def test_get_mev_shield_current_key_success(subtensor, mocker): # Prep fake_block = 123 fake_block_hash = "0x123abc" - fake_public_key_bytes = b"\x00" * 1184 # ML-KEM-768 public key size + fake_public_key_bytes = bytearray(b"\x00" * 1184) # ML-KEM-768 public key size mocked_determine_block_hash = mocker.AsyncMock(return_value=fake_block_hash) mocker.patch.object(subtensor, "determine_block_hash", mocked_determine_block_hash) - mocked_query = mocker.AsyncMock() - mocked_query.return_value = iter([fake_public_key_bytes]) + mocked_query = mocker.AsyncMock( + return_value=mocker.Mock(spec=ScaleType, value_object=fake_public_key_bytes) + ) subtensor.substrate.query = mocked_query # Call @@ -5802,7 +5793,9 @@ async def test_get_mev_shield_current_key_none(subtensor, mocker): mocked_determine_block_hash = mocker.AsyncMock(return_value=fake_block_hash) mocker.patch.object(subtensor, "determine_block_hash", mocked_determine_block_hash) - mocked_query = mocker.AsyncMock(return_value=None) + mocked_query = mocker.AsyncMock( + return_value=mocker.Mock(spec=ScaleType, value_object=None) + ) subtensor.substrate.query = mocked_query # Call @@ -5824,12 +5817,13 @@ async def test_get_mev_shield_current_key_invalid_size(subtensor, mocker): # Prep fake_block = 123 fake_block_hash = "0x123abc" - fake_public_key_bytes = b"\x00" * 1000 # Invalid size + fake_public_key_bytes = bytearray(b"\x00" * 1000) # Invalid size mocked_determine_block_hash = mocker.AsyncMock(return_value=fake_block_hash) mocker.patch.object(subtensor, "determine_block_hash", mocked_determine_block_hash) - mocked_query = mocker.AsyncMock() - mocked_query.return_value = iter([fake_public_key_bytes]) + mocked_query = mocker.AsyncMock( + return_value=mocker.Mock(spec=ScaleType, value_object=fake_public_key_bytes) + ) subtensor.substrate.query = mocked_query # Call & Assert @@ -5851,12 +5845,13 @@ async def test_get_mev_shield_next_key_success(subtensor, mocker): # Prep fake_block = 123 fake_block_hash = "0x123abc" - fake_public_key_bytes = b"\x00" * 1184 # ML-KEM-768 public key size + fake_public_key_bytes = bytearray(b"\x00" * 1184) # ML-KEM-768 public key size mocked_determine_block_hash = mocker.AsyncMock(return_value=fake_block_hash) mocker.patch.object(subtensor, "determine_block_hash", mocked_determine_block_hash) - mocked_query = mocker.AsyncMock() - mocked_query.return_value = iter([fake_public_key_bytes]) + mocked_query = mocker.AsyncMock( + return_value=mocker.Mock(spec=ScaleType, value_object=fake_public_key_bytes) + ) subtensor.substrate.query = mocked_query # Call @@ -5881,7 +5876,9 @@ async def test_get_mev_shield_next_key_none(subtensor, mocker): mocked_determine_block_hash = mocker.AsyncMock(return_value=fake_block_hash) mocker.patch.object(subtensor, "determine_block_hash", mocked_determine_block_hash) - mocked_query = mocker.AsyncMock(return_value=None) + mocked_query = mocker.AsyncMock( + return_value=mocker.Mock(spec=ScaleType, value_object=None) + ) subtensor.substrate.query = mocked_query # Call @@ -5903,12 +5900,13 @@ async def test_get_mev_shield_next_key_invalid_size(subtensor, mocker): # Prep fake_block = 123 fake_block_hash = "0x123abc" - fake_public_key_bytes = b"\x00" * 1000 # Invalid size + fake_public_key_bytes = bytearray(b"\x00" * 1000) # Invalid size mocked_determine_block_hash = mocker.AsyncMock(return_value=fake_block_hash) mocker.patch.object(subtensor, "determine_block_hash", mocked_determine_block_hash) - mocked_query = mocker.AsyncMock() - mocked_query.return_value = iter([fake_public_key_bytes]) + mocked_query = mocker.AsyncMock( + return_value=mocker.Mock(spec=ScaleType, value_object=fake_public_key_bytes) + ) subtensor.substrate.query = mocked_query # Call & Assert @@ -6039,7 +6037,7 @@ async def test_get_start_call_delay(subtensor, mocker): block_hash=None, reuse_block=False, ) - assert result == mocked_query_subtensor.return_value + assert result == mocked_query_subtensor.return_value.value @pytest.mark.asyncio From bd884a8867a445be890f01ec7eece4d5b059aee7 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Wed, 15 Apr 2026 21:05:41 +0200 Subject: [PATCH 072/118] Sync unit tests passing --- tests/unit_tests/test_async_subtensor.py | 6 +- tests/unit_tests/test_subtensor.py | 219 +++++++++-------------- 2 files changed, 90 insertions(+), 135 deletions(-) diff --git a/tests/unit_tests/test_async_subtensor.py b/tests/unit_tests/test_async_subtensor.py index 9e4fadfe3b..fbd2ba0cb3 100644 --- a/tests/unit_tests/test_async_subtensor.py +++ b/tests/unit_tests/test_async_subtensor.py @@ -4509,12 +4509,12 @@ async def test_get_crowdloan_constants(mocker, subtensor): async def test_get_crowdloan_contributions(mocker, subtensor): """Tests subtensor `get_crowdloan_contributions` method.""" # Preps - fake_hk_array = mocker.Mock(spec=list) + fake_hk = mocker.Mock(spec=str) fake_contribution = mocker.Mock(value=mocker.Mock(spec=Balance)) fake_crowdloan_id = mocker.Mock(spec=int) mocked_determine_block_hash = mocker.patch.object(subtensor, "determine_block_hash") - records = [(fake_hk_array, fake_contribution)] + records = [(fake_hk, fake_contribution)] fake_result = mocker.AsyncMock(autospec=list) fake_result.records = records fake_result.__aiter__.return_value = iter(records) @@ -4536,7 +4536,7 @@ async def test_get_crowdloan_contributions(mocker, subtensor): params=[fake_crowdloan_id], block_hash=mocked_determine_block_hash.return_value, ) - assert result == {fake_hk_array: mocked_from_rao.return_value} + assert result == {fake_hk: mocked_from_rao.return_value} @pytest.mark.parametrize( diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index 44b178597f..94efccdc0d 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -41,6 +41,16 @@ def fake_call_params(): return call_params() +@pytest.fixture +def scale_type_none(mocker): + return mocker.Mock(spec=ScaleType, value=None) + + +@pytest.fixture +def scale_type(mocker): + return mocker.Mock(spec=ScaleType) + + def call_params(): return AxonServeCallParams( version=settings.version_as_int, @@ -291,10 +301,10 @@ def test_hyperparameter_subnet_does_not_exist(subtensor, mocker): subtensor.subnet_exists.assert_called_once_with(1, block=None) -def test_hyperparameter_result_is_none(subtensor, mocker): +def test_hyperparameter_result_is_none(subtensor, mocker, scale_type_none): """Tests when query_subtensor returns None.""" subtensor.subnet_exists = mocker.MagicMock(return_value=True) - subtensor.substrate.query = mocker.MagicMock(return_value=None) + subtensor.substrate.query = mocker.MagicMock(return_value=scale_type_none) assert subtensor.get_hyperparameter("Difficulty", 1, None) is None subtensor.subnet_exists.assert_called_once_with(1, block=None) subtensor.substrate.query.assert_called_once_with( @@ -305,10 +315,10 @@ def test_hyperparameter_result_is_none(subtensor, mocker): ) -def test_hyperparameter_result_has_no_value(subtensor, mocker): +def test_hyperparameter_result_has_no_value(subtensor, mocker, scale_type_none): """Test when the result has no 'value' attribute.""" subtensor.subnet_exists = mocker.MagicMock(return_value=True) - subtensor.substrate.query = mocker.MagicMock(return_value=None) + subtensor.substrate.query = mocker.MagicMock(return_value=scale_type_none) assert subtensor.get_hyperparameter("Difficulty", 1, None) is None subtensor.subnet_exists.assert_called_once_with(1, block=None) subtensor.substrate.query.assert_called_once_with( @@ -544,7 +554,9 @@ def test_commit_reveal_enabled(subtensor, mocker): # Preps netuid = 1 block = 123 - mocked_get_hyperparameter = mocker.patch.object(subtensor, "get_hyperparameter") + mocked_get_hyperparameter = mocker.patch.object( + subtensor, "get_hyperparameter", return_value=False + ) # Call result = subtensor.commit_reveal_enabled(netuid, block) @@ -1789,28 +1801,6 @@ def test_difficulty_success(subtensor, mocker): assert result == int(mocked_get_hyperparameter.return_value) -def test_difficulty_none(subtensor, mocker): - """Tests difficulty method with None result.""" - # Preps - mocked_get_hyperparameter = mocker.patch.object( - subtensor, "get_hyperparameter", return_value=None - ) - fake_netuid = 1 - fake_block = 2 - - # Call - result = subtensor.difficulty(fake_netuid, fake_block) - - # Asserts - mocked_get_hyperparameter.assert_called_once_with( - param_name="Difficulty", - netuid=fake_netuid, - block=fake_block, - ) - - assert result is None - - def test_recycle_success(subtensor, mocker): """Tests recycle method with successfully result.""" # Preps @@ -2024,43 +2014,19 @@ def test_does_hotkey_exist_true(mocker, subtensor): assert result is True -def test_does_hotkey_exist_no_value(mocker, subtensor): - """Test when query_subtensor returns no value.""" - # Mock data - fake_hotkey_ss58 = "fake_hotkey" - fake_block = 123 - - # Mocks - mock_query_subtensor = mocker.patch.object( - subtensor.substrate, "query", return_value=None - ) - - # Call - result = subtensor.does_hotkey_exist(fake_hotkey_ss58, block=fake_block) - - # Assertions - mock_query_subtensor.assert_called_once_with( - module="SubtensorModule", - storage_function="Owner", - params=[fake_hotkey_ss58], - block_hash=subtensor.substrate.get_block_hash.return_value, - ) - subtensor.substrate.get_block_hash.assert_called_once_with(fake_block) - assert result is False - - -def test_does_hotkey_exist_special_id(mocker, subtensor): +def test_does_hotkey_exist_special_id(mocker, subtensor, scale_type): """Test when query_subtensor returns the special invalid owner identifier.""" # Mock data fake_hotkey_ss58 = "fake_hotkey" fake_owner = "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM" fake_block = 123 + scale_type.value = fake_owner # Mocks mock_query_subtensor = mocker.patch.object( subtensor.substrate, "query", - return_value=fake_owner, + return_value=scale_type, ) # Call result = subtensor.does_hotkey_exist(fake_hotkey_ss58, block=fake_block) @@ -2102,16 +2068,17 @@ def test_does_hotkey_exist_latest_block(mocker, subtensor): assert result is True -def test_get_hotkey_owner_success(mocker, subtensor): +def test_get_hotkey_owner_success(mocker, subtensor, scale_type): """Test when hotkey exists and owner is found.""" # Mock data fake_hotkey_ss58 = "fake_hotkey" fake_coldkey_ss58 = "fake_coldkey" fake_block = 123 + scale_type.value = fake_coldkey_ss58 # Mocks mock_query_subtensor = mocker.patch.object( - subtensor.substrate, "query", return_value=fake_coldkey_ss58 + subtensor.substrate, "query", return_value=scale_type ) mock_does_hotkey_exist = mocker.patch.object( subtensor, "does_hotkey_exist", return_value=True @@ -2132,37 +2099,6 @@ def test_get_hotkey_owner_success(mocker, subtensor): assert result == fake_coldkey_ss58 -def test_get_hotkey_owner_no_value(mocker, subtensor): - """Test when query_subtensor returns no value.""" - # Mock data - fake_hotkey_ss58 = "fake_hotkey" - fake_block = 123 - - # Mocks - mock_query_subtensor = mocker.patch.object( - subtensor.substrate, - "query", - return_value=None, - ) - mock_does_hotkey_exist = mocker.patch.object( - subtensor, "does_hotkey_exist", return_value=True - ) - - # Call - result = subtensor.get_hotkey_owner(fake_hotkey_ss58, block=fake_block) - - # Assertions - mock_query_subtensor.assert_called_once_with( - module="SubtensorModule", - storage_function="Owner", - params=[fake_hotkey_ss58], - block_hash=subtensor.substrate.get_block_hash.return_value, - ) - mock_does_hotkey_exist.assert_not_called() - subtensor.substrate.get_block_hash.assert_called_once_with(fake_block) - assert result is None - - def test_get_hotkey_owner_does_not_exist(mocker, subtensor): """Test when hotkey does not exist.""" # Mock data @@ -2194,15 +2130,16 @@ def test_get_hotkey_owner_does_not_exist(mocker, subtensor): assert result is None -def test_get_hotkey_owner_latest_block(mocker, subtensor): +def test_get_hotkey_owner_latest_block(mocker, subtensor, scale_type): """Test when no block is provided (latest block).""" # Mock data fake_hotkey_ss58 = "fake_hotkey" fake_coldkey_ss58 = "fake_coldkey" + scale_type.value = fake_coldkey_ss58 # Mocks mock_query_subtensor = mocker.patch.object( - subtensor.substrate, "query", return_value=fake_coldkey_ss58 + subtensor.substrate, "query", return_value=scale_type ) mock_does_hotkey_exist = mocker.patch.object( subtensor, "does_hotkey_exist", return_value=True @@ -2283,7 +2220,7 @@ def test_get_minimum_required_stake_invalid_result(mocker, subtensor): mock_query.assert_called_once_with( module="SubtensorModule", storage_function="NominatorMinRequiredStake" ) - mock_balance_from_rao.assert_called_once_with(fake_invalid_stake) + mock_balance_from_rao.assert_called_once_with(0) assert result == mock_balance_from_rao.return_value @@ -3102,7 +3039,7 @@ def test_get_metagraph_info_all_fields(subtensor, mocker): mock_runtime_call = mocker.patch.object( subtensor.substrate, "runtime_call", - return_value=mocker.Mock(value=mock_value), + return_value=mock_value, ) mock_chain_head = mocker.patch.object( subtensor.substrate, @@ -3155,7 +3092,7 @@ def test_get_metagraph_info_specific_fields(subtensor, mocker): mock_runtime_call = mocker.patch.object( subtensor.substrate, "runtime_call", - return_value=mocker.Mock(value=mock_value), + return_value=mock_value, ) mock_chain_head = mocker.patch.object( subtensor.substrate, @@ -3315,12 +3252,12 @@ def test_blocks_since_last_step_with_value(subtensor, mocker): assert result == mocked_query_subtensor.return_value.value -def test_blocks_since_last_step_is_none(subtensor, mocker): +def test_blocks_since_last_step_is_none(subtensor, mocker, scale_type_none): """Test blocks_since_last_step returns None correctly.""" # preps netuid = 1 block = 123 - mocked_query_subtensor = mocker.MagicMock(return_value=None) + mocked_query_subtensor = mocker.MagicMock(return_value=scale_type_none) subtensor.query_subtensor = mocked_query_subtensor # call @@ -3559,14 +3496,15 @@ def test_get_parents_success(subtensor, mocker): assert result == expected_formatted_parents -def test_get_parents_no_parents(subtensor, mocker): +def test_get_parents_no_parents(subtensor, mocker, scale_type): """Tests get_parents when there are no parents to retrieve.""" # Preps fake_hotkey = "valid_hotkey" fake_netuid = 1 fake_parents = [] + scale_type.value = fake_parents - mocked_query = mocker.MagicMock(return_value=fake_parents) + mocked_query = mocker.MagicMock(return_value=scale_type) subtensor.substrate.query = mocked_query # Call @@ -4652,9 +4590,14 @@ def test_get_crowdloan_contributions(mocker, subtensor): fake_contribution = mocker.Mock(spec=int) fake_crowdloan_id = mocker.Mock(spec=int) + records = [(fake_hk, fake_contribution)] + fake_result = mocker.MagicMock(autospec=list) + fake_result.records = records + fake_result.__iter__.return_value = iter(records) mocked_determine_block_hash = mocker.patch.object(subtensor, "determine_block_hash") - mocked_query_map = mocker.patch.object(subtensor.substrate, "query_map") - mocked_query_map.return_value.records = [(fake_hk, fake_contribution)] + mocked_query_map = mocker.patch.object( + subtensor.substrate, "query_map", return_value=fake_result + ) mocked_from_rao = mocker.patch.object(subtensor_module.Balance, "from_rao") # Call @@ -4674,9 +4617,7 @@ def test_get_crowdloan_by_id(mocker, subtensor, query_return, expected_result): fake_crowdloan_id = mocker.Mock(spec=int) mocked_determine_block_hash = mocker.patch.object(subtensor, "determine_block_hash") - mocked_query_return = ( - None if query_return is None else mocker.Mock(value=query_return) - ) + mocked_query_return = mocker.Mock(value=query_return) mocked_query = mocker.patch.object( subtensor.substrate, "query", return_value=mocked_query_return ) @@ -4728,13 +4669,17 @@ def test_get_crowdloans(mocker, subtensor): """Tests subtensor `get_crowdloans` method.""" # Preps fake_id = mocker.Mock(spec=int) - fake_crowdloan = mocker.Mock(value=mocker.Mock(spec=dict)) + fake_crowdloan = mocker.Mock(spec=dict) mocked_determine_block_hash = mocker.patch.object(subtensor, "determine_block_hash") + records = [(fake_id, fake_crowdloan)] + fake_result = mocker.MagicMock(autospec=list) + fake_result.records = records + fake_result.__iter__.return_value = iter(records) mocked_query_map = mocker.patch.object( subtensor.substrate, "query_map", - return_value=mocker.Mock(records=[(fake_id, fake_crowdloan)]), + return_value=fake_result, ) mocked_decode_crowdloan_entry = mocker.patch.object( subtensor, "_decode_crowdloan_entry" @@ -4752,7 +4697,7 @@ def test_get_crowdloans(mocker, subtensor): ) mocked_decode_crowdloan_entry.assert_called_once_with( crowdloan_id=fake_id, - data=fake_crowdloan.value, + data=fake_crowdloan, block_hash=mocked_determine_block_hash.return_value, ) assert result == [mocked_decode_crowdloan_entry.return_value] @@ -4807,23 +4752,14 @@ def test_commit_weights_with_zero_max_attempts( @pytest.mark.parametrize( "fake_result, expected_result", [ - ({"Swap": ()}, "Swap"), - ({"Keep": ()}, "Keep"), + ("Swap", "Swap"), + ("Keep", "Keep"), ( - { - "KeepSubnets": { - "subnets": ( - ( - 2, - 3, - ), - ) - } - }, + {"KeepSubnets": {"subnets": [2, 3]}}, {"KeepSubnets": {"subnets": [2, 3]}}, ), ( - {"KeepSubnets": {"subnets": ((2,),)}}, + {"KeepSubnets": {"subnets": [2]}}, { "KeepSubnets": { "subnets": [ @@ -4834,13 +4770,16 @@ def test_commit_weights_with_zero_max_attempts( ), ], ) -def test_get_root_claim_type(mocker, subtensor, fake_result, expected_result): +def test_get_root_claim_type( + mocker, subtensor, scale_type, fake_result, expected_result +): """Tests that `get_root_claim_type` calls proper methods and returns the correct value.""" # Preps fake_coldkey_ss58 = mocker.Mock(spec=str) mocked_determine_block_hash = mocker.patch.object(subtensor, "determine_block_hash") + scale_type.value = fake_result mocked_map = mocker.patch.object( - subtensor.substrate, "query", return_value=fake_result + subtensor.substrate, "query", return_value=scale_type ) # call @@ -4889,7 +4828,7 @@ def test_get_root_claimable_all_rates(mocker, subtensor): # Preps hotkey_ss58 = mocker.Mock(spec=str) mocked_determine_block_hash = mocker.patch.object(subtensor, "determine_block_hash") - fake_value = [((14, {"bits": 6520190}),)] + fake_value = [(14, {"bits": 6520190})] fake_result = mocker.MagicMock(value=fake_value) fake_result.__iter__ = fake_value mocked_query = mocker.patch.object( @@ -5229,7 +5168,7 @@ def test_get_proxy_announcement(subtensor, mocker): params=[fake_delegate_account_ss58], block_hash=mocked_determine_block_hash.return_value, ) - mocked_from_dict.assert_called_once_with(mocked_query.return_value.value[0]) + mocked_from_dict.assert_called_once_with(mocked_query.return_value.value) assert result == mocked_from_dict.return_value @@ -5833,13 +5772,15 @@ def test_get_mev_shield_current_key_success(subtensor, mocker): # Prep fake_block = 123 fake_block_hash = "0x123abc" - fake_public_key_bytes = b"\x00" * 1184 # ML-KEM-768 public key size + fake_public_key_bytes = bytearray(b"\x00" * 1184) # ML-KEM-768 public key size mocked_determine_block_hash = mocker.patch.object( subtensor, "determine_block_hash", return_value=fake_block_hash ) mocked_query = mocker.patch.object(subtensor.substrate, "query") - mocked_query.return_value = iter([fake_public_key_bytes]) + mocked_query.return_value = mocker.Mock( + spec=ScaleType, value_object=fake_public_key_bytes + ) # Call result = subtensor.get_mev_shield_current_key(block=fake_block) @@ -5863,7 +5804,11 @@ def test_get_mev_shield_current_key_none(subtensor, mocker): mocked_determine_block_hash = mocker.patch.object( subtensor, "determine_block_hash", return_value=fake_block_hash ) - mocked_query = mocker.patch.object(subtensor.substrate, "query", return_value=None) + mocked_query = mocker.patch.object( + subtensor.substrate, + "query", + return_value=mocker.Mock(spec=ScaleType, value_object=None), + ) # Call result = subtensor.get_mev_shield_current_key(block=fake_block) @@ -5883,13 +5828,15 @@ def test_get_mev_shield_current_key_invalid_size(subtensor, mocker): # Prep fake_block = 123 fake_block_hash = "0x123abc" - fake_public_key_bytes = b"\x00" * 1000 # Invalid size + fake_public_key_bytes = bytearray(b"\x00" * 1000) # Invalid size mocked_determine_block_hash = mocker.patch.object( subtensor, "determine_block_hash", return_value=fake_block_hash ) mocked_query = mocker.patch.object(subtensor.substrate, "query") - mocked_query.return_value = iter([fake_public_key_bytes]) + mocked_query.return_value = mocker.Mock( + spec=ScaleType, value_object=fake_public_key_bytes + ) # Call & Assert with pytest.raises(ValueError, match="Invalid ML-KEM-768 public key size"): @@ -5909,13 +5856,15 @@ def test_get_mev_shield_next_key_success(subtensor, mocker): # Prep fake_block = 123 fake_block_hash = "0x123abc" - fake_public_key_bytes = b"\x00" * 1184 # ML-KEM-768 public key size + fake_public_key_bytes = bytearray(b"\x00" * 1184) # ML-KEM-768 public key size mocked_determine_block_hash = mocker.patch.object( subtensor, "determine_block_hash", return_value=fake_block_hash ) mocked_query = mocker.patch.object(subtensor.substrate, "query") - mocked_query.return_value = iter([fake_public_key_bytes]) + mocked_query.return_value = mocker.Mock( + spec=ScaleType, value_object=fake_public_key_bytes + ) # Call result = subtensor.get_mev_shield_next_key(block=fake_block) @@ -5939,7 +5888,11 @@ def test_get_mev_shield_next_key_none(subtensor, mocker): mocked_determine_block_hash = mocker.patch.object( subtensor, "determine_block_hash", return_value=fake_block_hash ) - mocked_query = mocker.patch.object(subtensor.substrate, "query", return_value=None) + mocked_query = mocker.patch.object( + subtensor.substrate, + "query", + return_value=mocker.Mock(spec=ScaleType, value_object=None), + ) # Call result = subtensor.get_mev_shield_next_key(block=fake_block) @@ -5959,13 +5912,15 @@ def test_get_mev_shield_next_key_invalid_size(subtensor, mocker): # Prep fake_block = 123 fake_block_hash = "0x123abc" - fake_public_key_bytes = b"\x00" * 1000 # Invalid size + fake_public_key_bytes = bytearray(b"\x00" * 1000) # Invalid size mocked_determine_block_hash = mocker.patch.object( subtensor, "determine_block_hash", return_value=fake_block_hash ) mocked_query = mocker.patch.object(subtensor.substrate, "query") - mocked_query.return_value = iter([fake_public_key_bytes]) + mocked_query.return_value = mocker.Mock( + spec=ScaleType, value_object=fake_public_key_bytes + ) # Call & Assert with pytest.raises(ValueError, match="Invalid ML-KEM-768 public key size"): @@ -6084,7 +6039,7 @@ def test_get_start_call_delay(subtensor, mocker): # Asserts mocked_query_subtensor.assert_called_once_with(name="StartCallDelay", block=None) - assert result == mocked_query_subtensor.return_value + assert result == mocked_query_subtensor.return_value.value def test_get_coldkey_swap_announcement(subtensor, mocker): From ecfb7426b31760ec8bd6b1198946da70b0880334 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Wed, 15 Apr 2026 21:10:11 +0200 Subject: [PATCH 073/118] Bump mypy --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 689cc09eff..3eade8f88f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,7 +49,7 @@ dev = [ "pytest-cov==4.0.0", "ddt==1.6.0", "hypothesis==6.81.1", - "mypy==1.8.0", + "mypy==1.20.1", "types-retry==0.9.9.4", "typing_extensions>= 4.0.0; python_version<'3.11'", "freezegun==1.5.0", From c240424e82a0102f7553dda7fe59a6076cc2185d Mon Sep 17 00:00:00 2001 From: BD Himes Date: Wed, 15 Apr 2026 21:15:59 +0200 Subject: [PATCH 074/118] Almost all integration tests working --- bittensor/core/chain_data/metagraph_info.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/bittensor/core/chain_data/metagraph_info.py b/bittensor/core/chain_data/metagraph_info.py index 989bfd6861..31107310d6 100644 --- a/bittensor/core/chain_data/metagraph_info.py +++ b/bittensor/core/chain_data/metagraph_info.py @@ -10,6 +10,7 @@ get_netuid_and_mechid_by_storage_index, u64_normalized_float as u64tf, u16_normalized_float as u16tf, + deprecated_message, ) from bittensor.utils.balance import Balance, fixed_to_float @@ -50,6 +51,9 @@ def process_nested( data: Union[tuple, dict], chr_transform ) -> Optional[Union[list, dict]]: """Processes nested data structures by applying a transformation function to their elements.""" + deprecated_message( + "This function is deprecated as it is no longer needed with the new decoding." + ) if isinstance(data, (list, tuple)): if len(data) > 0: return [ @@ -192,11 +196,6 @@ def _from_dict(cls, decoded: dict) -> "MetagraphInfo": if decoded.get("identities") is not None: ii_list.append("identities") - for key in ii_list: - raw_data = decoded.get(key) - processed = process_nested(raw_data, _chr_str) - decoded.update({key: processed}) - return cls( # Subnet index netuid=_netuid, From 29222027aa3a8eed7ca1ec5530e3a04682d905c0 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Wed, 15 Apr 2026 21:38:27 +0200 Subject: [PATCH 075/118] Some Test fixes --- bittensor/core/async_subtensor.py | 5 ++--- bittensor/core/chain_data/proxy.py | 2 +- bittensor/core/subtensor.py | 3 +-- tests/e2e_tests/test_metagraph.py | 24 ++++++++++++------------ 4 files changed, 16 insertions(+), 18 deletions(-) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index ff0f1bec8a..6108303727 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -1180,8 +1180,7 @@ async def bonds( uid: int bond: list[tuple[int, int]] async for uid, bond in b_map_encoded: - if len(bond) != 0: - bond_map.append((uid, bond)) + bond_map.append((uid, bond)) return bond_map @@ -4622,7 +4621,7 @@ async def get_subnet_price( params=[netuid], block_hash=block_hash, ) - price_rao = call.value + price_rao = call return Balance.from_rao(price_rao) async def get_subnet_prices( diff --git a/bittensor/core/chain_data/proxy.py b/bittensor/core/chain_data/proxy.py index 465e6b9dfd..662aad1e6b 100644 --- a/bittensor/core/chain_data/proxy.py +++ b/bittensor/core/chain_data/proxy.py @@ -238,7 +238,7 @@ def from_query(cls, query: Any) -> tuple[list["ProxyInfo"], Balance]: See: """ # proxies data is always in that path - proxies = query.value[0][0] + proxies = query.value[0] # balance data is always in that path balance = query.value[1] return cls.from_tuple(proxies), Balance.from_rao(balance) diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index d3ae97eb45..0394f0cce7 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -960,8 +960,7 @@ def bonds( uid: int bond: list[tuple[int, int]] for uid, bond in b_map_encoded: - if len(bond) != 0: - bond_map.append((uid, bond)) + bond_map.append((uid, bond)) return bond_map diff --git a/tests/e2e_tests/test_metagraph.py b/tests/e2e_tests/test_metagraph.py index 6606ec62c5..274f8672ea 100644 --- a/tests/e2e_tests/test_metagraph.py +++ b/tests/e2e_tests/test_metagraph.py @@ -1137,8 +1137,8 @@ def test_metagraph_info_with_indexes(subtensor, alice_wallet, bob_wallet): name="omron", owner_hotkey=alice_wallet.hotkey.ss58_address, owner_coldkey=alice_wallet.coldkey.ss58_address, - active=(True,), - axons=( + active=[True], + axons=[ { "block": 0, "ip": 0, @@ -1149,7 +1149,7 @@ def test_metagraph_info_with_indexes(subtensor, alice_wallet, bob_wallet): "protocol": 0, "version": 0, }, - ), + ], symbol=None, identity=None, network_registered_at=None, @@ -1240,8 +1240,8 @@ def test_metagraph_info_with_indexes(subtensor, alice_wallet, bob_wallet): name="omron", owner_hotkey=alice_wallet.hotkey.ss58_address, owner_coldkey=alice_wallet.coldkey.ss58_address, - active=(True, True), - axons=( + active=[True, True], + axons=[ { "block": 0, "ip": 0, @@ -1262,7 +1262,7 @@ def test_metagraph_info_with_indexes(subtensor, alice_wallet, bob_wallet): "protocol": 0, "version": 0, }, - ), + ], symbol=None, identity=None, network_registered_at=None, @@ -1366,8 +1366,8 @@ async def test_metagraph_info_with_indexes_async( name="omron", owner_hotkey=alice_wallet.hotkey.ss58_address, owner_coldkey=alice_wallet.coldkey.ss58_address, - active=(True,), - axons=( + active=[True], + axons=[ { "block": 0, "ip": 0, @@ -1378,7 +1378,7 @@ async def test_metagraph_info_with_indexes_async( "protocol": 0, "version": 0, }, - ), + ], symbol=None, identity=None, network_registered_at=None, @@ -1471,8 +1471,8 @@ async def test_metagraph_info_with_indexes_async( name="omron", owner_hotkey=alice_wallet.hotkey.ss58_address, owner_coldkey=alice_wallet.coldkey.ss58_address, - active=(True, True), - axons=( + active=[True, True], + axons=[ { "block": 0, "ip": 0, @@ -1493,7 +1493,7 @@ async def test_metagraph_info_with_indexes_async( "protocol": 0, "version": 0, }, - ), + ], symbol=None, identity=None, network_registered_at=None, From 7cc31a67fd9675d07ec0a1e5e9a38e90482fb457 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Wed, 15 Apr 2026 21:43:09 +0200 Subject: [PATCH 076/118] Some Test fixes --- 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 fbd2ba0cb3..0ee02b727a 100644 --- a/tests/unit_tests/test_async_subtensor.py +++ b/tests/unit_tests/test_async_subtensor.py @@ -3686,7 +3686,7 @@ async def test_get_subnet_price(subtensor, mocker): fake_price = 29258617 expected_price = Balance.from_tao(0.029258617) mocked_query = mocker.patch.object( - subtensor.substrate, "runtime_call", return_value=mocker.Mock(value=fake_price) + subtensor.substrate, "runtime_call", return_value=fake_price ) # Call From c37667373b036d3f4299af1d85fe95e7bfac7269 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Wed, 15 Apr 2026 21:52:01 +0200 Subject: [PATCH 077/118] Metagraph e2e test fix --- tests/e2e_tests/test_metagraph.py | 48 +++++++++++++++---------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/tests/e2e_tests/test_metagraph.py b/tests/e2e_tests/test_metagraph.py index 274f8672ea..0247f11d7d 100644 --- a/tests/e2e_tests/test_metagraph.py +++ b/tests/e2e_tests/test_metagraph.py @@ -671,7 +671,7 @@ def test_metagraph_info(subtensor, alice_wallet, bob_wallet): hotkeys=["5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM"], coldkeys=["5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM"], identities=[None], - axons=( + axons=[ { "block": 0, "version": 0, @@ -682,18 +682,18 @@ def test_metagraph_info(subtensor, alice_wallet, bob_wallet): "placeholder1": 0, "placeholder2": 0, }, - ), - active=(True,), - validator_permit=(False,), + ], + active=[True], + validator_permit=[False], pruning_score=[], - last_update=(0,), + last_update=[0], emission=[Balance(0).set_unit(1)], dividends=[0.0], incentives=[0.0], consensus=[0.0], trust=[], rank=[], - block_at_registration=(0,), + block_at_registration=[0], alpha_stake=[Balance.from_tao(1.0).set_unit(1)], tao_stake=[Balance(0)], total_stake=[Balance.from_tao(1.0).set_unit(1)], @@ -768,19 +768,19 @@ def test_metagraph_info(subtensor, alice_wallet, bob_wallet): bonds_moving_avg=4.87890977618477e-14, hotkeys=[], coldkeys=[], - identities={}, - axons=(), - active=(), - validator_permit=(), + identities=[], + axons=[], + active=[], + validator_permit=[], pruning_score=[], - last_update=(), + last_update=[], emission=[], dividends=[], incentives=[], consensus=[], trust=[], rank=[], - block_at_registration=(), + block_at_registration=[], alpha_stake=[], tao_stake=[], total_stake=[], @@ -922,7 +922,7 @@ async def test_metagraph_info_async(async_subtensor, alice_wallet, bob_wallet): hotkeys=["5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM"], coldkeys=["5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM"], identities=[None], - axons=( + axons=[ { "block": 0, "version": 0, @@ -933,18 +933,18 @@ async def test_metagraph_info_async(async_subtensor, alice_wallet, bob_wallet): "placeholder1": 0, "placeholder2": 0, }, - ), - active=(True,), - validator_permit=(False,), + ], + active=[True], + validator_permit=[False], pruning_score=[], - last_update=(0,), + last_update=[0], emission=[Balance(0).set_unit(1)], dividends=[0.0], incentives=[0.0], consensus=[0.0], trust=[], rank=[], - block_at_registration=(0,), + block_at_registration=[0], alpha_stake=[Balance.from_tao(1.0).set_unit(1)], tao_stake=[Balance(0)], total_stake=[Balance.from_tao(1.0).set_unit(1)], @@ -1019,19 +1019,19 @@ async def test_metagraph_info_async(async_subtensor, alice_wallet, bob_wallet): bonds_moving_avg=4.87890977618477e-14, hotkeys=[], coldkeys=[], - identities={}, - axons=(), - active=(), - validator_permit=(), + identities=[], + axons=[], + active=[], + validator_permit=[], pruning_score=[], - last_update=(), + last_update=[], emission=[], dividends=[], incentives=[], consensus=[], trust=[], rank=[], - block_at_registration=(), + block_at_registration=[], alpha_stake=[], tao_stake=[], total_stake=[], From d679d983f441ec02bbdd85728f92978729e30521 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Wed, 15 Apr 2026 21:58:20 +0200 Subject: [PATCH 078/118] Removes httpx from dev reqs --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 21579e3984..ff393128bd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,7 +52,6 @@ dev = [ "types-retry==0.9.9.4", "typing_extensions>= 4.0.0; python_version<'3.11'", "freezegun==1.5.0", - "httpx==0.27.0", "ruff==0.11.5", "aioresponses==0.7.6", "factory-boy==3.3.0", From eebb35296f32d52e2a47f0f22217a4b7744557c0 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Wed, 15 Apr 2026 22:05:24 +0200 Subject: [PATCH 079/118] Test fix --- bittensor/core/async_subtensor.py | 29 +++++++++++++++++------------ bittensor/core/subtensor.py | 4 ++-- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index 6108303727..ba5d16b63d 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -5653,20 +5653,25 @@ async def subnet( if not block_hash and reuse_block: block_hash = self.substrate.last_block_hash - query = await self.substrate.runtime_call( - "SubnetInfoRuntimeApi", - "get_dynamic_info", - params=[netuid], - block_hash=block_hash, - ) - price = await self.get_subnet_price( - netuid=netuid, - block=block, - block_hash=block_hash, - reuse_block=reuse_block, + decoded: Optional[dict] + price: Optional[Balance] + decoded, price = await asyncio.gather( + self.substrate.runtime_call( + "SubnetInfoRuntimeApi", + "get_dynamic_info", + params=[netuid], + block_hash=block_hash, + ), + self.get_subnet_price( + netuid=netuid, + block=block, + block_hash=block_hash, + reuse_block=reuse_block, + ), + return_exceptions=True, ) - if isinstance(decoded := query.decode(), dict): + if isinstance(decoded, dict): if isinstance(price, (SubstrateRequestException, ValueError)): price = None return DynamicInfo.from_dict({**decoded, "price": price}) diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 0394f0cce7..0d78e94f8f 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -4598,14 +4598,14 @@ def subnet(self, netuid: int, block: Optional[int] = None) -> Optional[DynamicIn """ block_hash = self.determine_block_hash(block=block) - query = self.substrate.runtime_call( + decoded: Optional[dict] = self.substrate.runtime_call( api="SubnetInfoRuntimeApi", method="get_dynamic_info", params=[netuid], block_hash=block_hash, ) - if isinstance(decoded := query.decode(), dict): + if isinstance(decoded, dict): try: price = self.get_subnet_price(netuid=netuid, block=block) except (SubstrateRequestException, ValueError): From f146100822996fd73a410832323a3dacbdad0e9b Mon Sep 17 00:00:00 2001 From: BD Himes Date: Wed, 15 Apr 2026 22:07:37 +0200 Subject: [PATCH 080/118] Adds note instead --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index ff393128bd..cb3bf7f19c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,6 +52,7 @@ dev = [ "types-retry==0.9.9.4", "typing_extensions>= 4.0.0; python_version<'3.11'", "freezegun==1.5.0", + "httpx==0.27.0", # used by tests/unit_tests/test_axon: The starlette.testclient module requires the httpx package to be installed. "ruff==0.11.5", "aioresponses==0.7.6", "factory-boy==3.3.0", From cb95640d68dcee0c26eea01791eb1c60b83f45c3 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Thu, 16 Apr 2026 12:59:28 +0200 Subject: [PATCH 081/118] Dendrite Test fix --- 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 0d78e94f8f..23a1378dce 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -3799,7 +3799,7 @@ def get_subnet_price( method="current_alpha_price", params=[netuid], block_hash=block_hash, - ).value + ) return Balance.from_rao(price_rao) def get_subnet_prices( From 6f893ab6ddb0e0568e14d631c00b701cf7ec295a Mon Sep 17 00:00:00 2001 From: BD Himes Date: Thu, 16 Apr 2026 14:30:34 +0200 Subject: [PATCH 082/118] Metagraph Test fix --- bittensor/core/async_subtensor.py | 4 ++-- bittensor/core/subtensor.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index ba5d16b63d..42d4b1385b 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -1491,10 +1491,10 @@ async def get_all_metagraphs_info( method=method, block_hash=block_hash, ) - if query is None or not hasattr(query, "value"): + if query is None: return None - return MetagraphInfo.list_from_dicts(query.value) + return MetagraphInfo.list_from_dicts(query) async def get_all_neuron_certificates( self, diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 23a1378dce..9560acf503 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -1189,10 +1189,10 @@ def get_all_metagraphs_info( method=method, block_hash=block_hash, ) - if query is None or not hasattr(query, "value"): + if query is None: return None - return MetagraphInfo.list_from_dicts(query.value) + return MetagraphInfo.list_from_dicts(query) def get_all_neuron_certificates( self, netuid: int, block: Optional[int] = None From 45ea74bc8adbd2384bd00449cb25bf4de26c7821 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Thu, 16 Apr 2026 15:07:42 +0200 Subject: [PATCH 083/118] Unit test fixes --- tests/unit_tests/test_async_subtensor.py | 3 +-- tests/unit_tests/test_subtensor.py | 5 ++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/unit_tests/test_async_subtensor.py b/tests/unit_tests/test_async_subtensor.py index 0ee02b727a..6ab924869d 100644 --- a/tests/unit_tests/test_async_subtensor.py +++ b/tests/unit_tests/test_async_subtensor.py @@ -3786,8 +3786,7 @@ async def test_subnet(subtensor, mocker): mocked_get_subnet_price = mocker.patch.object( subtensor, "get_subnet_price", return_value=Balance.from_tao(100.0) ) - mocked_decode = mocker.Mock(return_value={"netuid": netuid}) - mocked_runtime_call = mocker.Mock(decode=mocked_decode) + mocked_runtime_call = {"netuid": netuid} mocker.patch.object( subtensor.substrate, "runtime_call", return_value=mocked_runtime_call ) diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index 94efccdc0d..d08f4c41fa 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -3848,7 +3848,7 @@ def test_get_subnet_price(subtensor, mocker): fake_price = 29258617 expected_price = Balance.from_tao(0.029258617) mocked_query = mocker.patch.object( - subtensor.substrate, "runtime_call", return_value=mocker.Mock(value=fake_price) + subtensor.substrate, "runtime_call", return_value=fake_price ) # Call @@ -3940,8 +3940,7 @@ def test_subnet(subtensor, mocker): mocked_get_subnet_price = mocker.patch.object( subtensor, "get_subnet_price", return_value=Balance.from_tao(100.0) ) - mocked_decode = mocker.Mock(return_value={"netuid": netuid}) - mocked_runtime_call = mocker.Mock(decode=mocked_decode) + mocked_runtime_call = {"netuid": netuid} mocker.patch.object( subtensor.substrate, "runtime_call", return_value=mocked_runtime_call ) From 11896d4885fc37f379de651e5fb82c749de789a0 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Thu, 16 Apr 2026 15:10:25 +0200 Subject: [PATCH 084/118] Neuron certificate fix --- bittensor/utils/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bittensor/utils/__init__.py b/bittensor/utils/__init__.py index 6a2197132d..374f1aa67b 100644 --- a/bittensor/utils/__init__.py +++ b/bittensor/utils/__init__.py @@ -96,8 +96,7 @@ class Certificate(str): def __new__(cls, data: "str | NeuronCertificateResponse"): if isinstance(data, dict): pubkey: str = data["public_key"] - pubkey_bytes = bytes.fromhex(pubkey.removeprefix("0x")) - string = chr(data["algorithm"]) + "".join([chr(i) for i in pubkey_bytes]) + string = chr(data["algorithm"]) + pubkey else: string = data return str.__new__(cls, string) From 9b1886bd900aa59de6fb6cbfe4aeae63a7fb74a9 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Thu, 16 Apr 2026 15:14:18 +0200 Subject: [PATCH 085/118] Revealed commitment fixes --- bittensor/core/async_subtensor.py | 6 ++++-- bittensor/core/subtensor.py | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index 42d4b1385b..33609d1b81 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -3869,9 +3869,11 @@ async def get_revealed_commitment_by_hotkey( block_hash=block_hash, reuse_block=reuse_block, ) - if query.value is None: + if query.value_serialized is None: return None - return tuple(decode_revealed_commitment(pair) for pair in query) + return tuple( + decode_revealed_commitment(pair) for pair in query.value_serialized + ) async def get_root_claim_type( self, diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 9560acf503..ab4f82cecf 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -3223,9 +3223,11 @@ def get_revealed_commitment_by_hotkey( params=[netuid, hotkey_ss58], block=block, ) - if query.value is None: + if query.value_serialized is None: return None - return tuple(decode_revealed_commitment(pair) for pair in query) + return tuple( + decode_revealed_commitment(pair) for pair in query.value_serialized + ) def get_root_claim_type( self, From afede701820a27c5bad45f1376a49f633f4ddc4c Mon Sep 17 00:00:00 2001 From: BD Himes Date: Thu, 16 Apr 2026 15:17:00 +0200 Subject: [PATCH 086/118] subnet identity set fix --- bittensor/core/chain_data/dynamic_info.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/bittensor/core/chain_data/dynamic_info.py b/bittensor/core/chain_data/dynamic_info.py index b92f9fa816..92373ecf1b 100644 --- a/bittensor/core/chain_data/dynamic_info.py +++ b/bittensor/core/chain_data/dynamic_info.py @@ -76,18 +76,15 @@ def _from_dict(cls, decoded: dict) -> "DynamicInfo": if subnet_identity := decoded.get("subnet_identity"): # we need to check it for keep backwards compatibility - logo_bytes = subnet_identity.get("logo_url") - si_logo_url = bytes(logo_bytes).decode() if logo_bytes else None - subnet_identity = SubnetIdentity( - subnet_name=bytes(subnet_identity["subnet_name"]).decode(), - github_repo=bytes(subnet_identity["github_repo"]).decode(), - subnet_contact=bytes(subnet_identity["subnet_contact"]).decode(), - subnet_url=bytes(subnet_identity["subnet_url"]).decode(), - logo_url=si_logo_url, - discord=bytes(subnet_identity["discord"]).decode(), - description=bytes(subnet_identity["description"]).decode(), - additional=bytes(subnet_identity["additional"]).decode(), + subnet_name=subnet_identity["subnet_name"], + github_repo=subnet_identity["github_repo"], + subnet_contact=subnet_identity["subnet_contact"], + subnet_url=subnet_identity["subnet_url"], + logo_url=subnet_identity.get("logo_url", ""), + discord=subnet_identity["discord"], + description=subnet_identity["description"], + additional=subnet_identity["additional"], ) else: subnet_identity = None From bca99f9c5da579f444de131c287e2b800d168f07 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Thu, 16 Apr 2026 15:28:11 +0200 Subject: [PATCH 087/118] Bump cyscale --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 424b32d763..73425fa330 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ dependencies = [ "retry==0.9.2", "requests>=2.0.0,<3.0", "pydantic>=2.3,<3", - "cyscale==0.2.1", + "cyscale==0.2.2", "uvicorn", "bittensor-drand>=1.3.0,<2.0.0", "bittensor-wallet==4.0.1", From 84a0be3175dfff8608774be4d345d2a7bc9fd674 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Thu, 16 Apr 2026 15:28:28 +0200 Subject: [PATCH 088/118] Bump reqs to safe versions --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 73425fa330..b75919f75c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ requires-python = ">=3.10,<3.15" dependencies = [ "wheel", "setuptools~=70.0", - "aiohttp>=3.9,<4.0", + "aiohttp>=3.13.4,<4.0", "asyncstdlib~=3.13.0", "colorama~=0.4.6", "fastapi>=0.110.1", @@ -27,7 +27,7 @@ dependencies = [ "pycryptodome>=3.18.0,<4.0.0", "pyyaml>=6.0", "retry==0.9.2", - "requests>=2.0.0,<3.0", + "requests>=2.33.0,<3.0", "pydantic>=2.3,<3", "cyscale==0.2.2", "uvicorn", From d79e81e9eca00c3641026b7e85a3bd73d62d5b1d Mon Sep 17 00:00:00 2001 From: BD Himes Date: Thu, 16 Apr 2026 17:22:48 +0200 Subject: [PATCH 089/118] Type fixes --- bittensor/core/async_subtensor.py | 21 ++++++---- bittensor/core/subtensor.py | 70 ++++++++++++++++++------------- bittensor/core/types.py | 38 ++++++++++++++++- 3 files changed, 91 insertions(+), 38 deletions(-) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index 33609d1b81..ec49f9461b 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -11,7 +11,7 @@ from async_substrate_interface.utils.storage import StorageKey from bittensor_drand import get_encrypted_commitment from bittensor_wallet.utils import SS58_FORMAT -from scalecodec import GenericCall +from scalecodec import GenericCall, ScaleValue from scalecodec.base import ScaleType from scalecodec.utils.math import FixedPoint, fixed_to_decimal @@ -151,6 +151,7 @@ NeuronCertificateResponse, CommitmentOfResponse, CrowdloansResponse, + DynamicInfoResponse, ) from bittensor.utils import ( Certificate, @@ -528,7 +529,7 @@ async def _query_with_fallback( *args: tuple[str, str, Optional[list[Any]]], block_hash: Optional[str] = None, default_value: Any = ValueError, - ) -> ScaleType[Any] | Any: + ) -> ScaleType[ScaleValue] | Any: """ Queries the subtensor node with a given set of args, falling back to the next group if the method does not exist at the given block. This method exists to support backwards compatibility for blocks. @@ -753,7 +754,7 @@ async def query_constant( block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> Optional[ScaleType[Any]]: + ) -> Optional[ScaleType[ScaleValue]]: """Retrieves a constant from the specified module on the Bittensor blockchain. Use this function for nonstandard queries to constants defined within the Bittensor blockchain, if these cannot @@ -854,7 +855,7 @@ async def query_module( block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> ScaleType[Any]: + ) -> ScaleType[ScaleValue]: """Queries any module storage on the Bittensor blockchain with the specified parameters and block number. This function is a generic query interface that allows for flexible and diverse data retrieval from various blockchain modules. Use this function for nonstandard queries to storage defined within the Bittensor @@ -922,7 +923,7 @@ async def query_subtensor( block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> Optional[ScaleType[Any]]: + ) -> ScaleType[ScaleValue]: """Queries named storage from the Subtensor module on the Bittensor blockchain. Use this function for nonstandard queries to storage defined within the Bittensor blockchain, if these cannot @@ -1008,7 +1009,7 @@ async def all_subnets( if not block_hash and reuse_block: block_hash = self.substrate.last_block_hash - decoded: list[dict[str, Any]] + decoded: list[DynamicInfoResponse] subnet_prices: dict[int, Balance] decoded, subnet_prices = await asyncio.gather( @@ -1728,7 +1729,9 @@ async def get_balances( ) for address in addresses ] - batch_call = await self.substrate.query_multi(calls, block_hash=block_hash) + batch_call: list[tuple[StorageKey, dict]] = await self.substrate.query_multi( + calls, block_hash=block_hash + ) # type: ignore[assignment] results = {} for item in batch_call: value = item[1] or {"data": {"free": 0}} @@ -3693,7 +3696,7 @@ async def get_proxy_announcement( - See: """ block_hash = await self.determine_block_hash(block, block_hash, reuse_block) - query: ScaleType[tuple[list[dict], int]] = await self.substrate.query( + query: ScaleType[tuple[list[dict], int]] = await self.substrate.query( # type: ignore[assignment] module="Proxy", storage_function="Announcements", params=[delegate_account_ss58], @@ -5655,7 +5658,7 @@ async def subnet( if not block_hash and reuse_block: block_hash = self.substrate.last_block_hash - decoded: Optional[dict] + decoded: Optional[DynamicInfoResponse] price: Optional[Balance] decoded, price = await asyncio.gather( self.substrate.runtime_call( diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index ab4f82cecf..3c748b922b 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -10,6 +10,7 @@ from async_substrate_interface.utils.storage import StorageKey from bittensor_drand import get_encrypted_commitment from bittensor_wallet.utils import SS58_FORMAT +from scalecodec import ScaleValue from scalecodec.base import ScaleType from scalecodec.utils.math import FixedPoint, fixed_to_decimal @@ -150,6 +151,7 @@ NeuronCertificateResponse, CommitmentOfResponse, CrowdloansResponse, + DynamicInfoResponse, ) from bittensor.utils import ( Certificate, @@ -636,7 +638,7 @@ def sim_swap( def query_constant( self, module_name: str, constant_name: str, block: Optional[int] = None - ) -> Optional[ScaleType[Any]]: + ) -> Optional[ScaleType[ScaleValue]]: """Retrieves a constant from the specified module on the Bittensor blockchain. Use this function for nonstandard queries to constants defined within the Bittensor blockchain, if these cannot @@ -773,7 +775,7 @@ def query_subtensor( name: str, params: Optional[list] = None, block: Optional[int] = None, - ) -> ScaleType[Any]: + ) -> ScaleType[ScaleValue]: """Queries named storage from the Subtensor module on the Bittensor blockchain. Use this function for nonstandard queries to constants defined within the Bittensor blockchain, if these cannot @@ -832,10 +834,13 @@ def all_subnets(self, block: Optional[int] = None) -> Optional[list["DynamicInfo a subnet, or None if the query fails. """ block_hash = self.determine_block_hash(block=block) - decoded = self.substrate.runtime_call( - api="SubnetInfoRuntimeApi", - method="get_all_dynamic_info", - block_hash=block_hash, + decoded = cast( + list[DynamicInfoResponse], + self.substrate.runtime_call( + api="SubnetInfoRuntimeApi", + method="get_all_dynamic_info", + block_hash=block_hash, + ), ) try: subnet_prices = self.get_subnet_prices(block=block) @@ -864,7 +869,7 @@ def blocks_since_last_step( Notes: - """ - query: ScaleType[int] = self.query_subtensor( + query: ScaleType[int] = self.query_subtensor( # type: ignore[assignment] name="BlocksSinceLastStep", block=block, params=[netuid] ) return query.value @@ -1191,7 +1196,7 @@ def get_all_metagraphs_info( ) if query is None: return None - + assert isinstance(query, list) return MetagraphInfo.list_from_dicts(query) def get_all_neuron_certificates( @@ -1377,7 +1382,9 @@ def get_balances( ) for address in addresses ] - batch_call = self.substrate.query_multi(calls, block_hash=block_hash) + batch_call: list[tuple[StorageKey, dict]] = self.substrate.query_multi( # type: ignore[assignment] + calls, block_hash=block_hash + ) results = {} key: StorageKey val: dict @@ -1552,8 +1559,7 @@ def get_children_pending( block_hash=self.determine_block_hash(block), ) children, cooldown = cast( - tuple[list[tuple[int, Any]], int], - getattr(pending_query, "value", pending_query), + tuple[list[tuple[int, Any]], int], pending_query.value ) return ( @@ -2125,7 +2131,7 @@ def get_delegate_take(self, hotkey_ss58: str, block: Optional[int] = None) -> fl Notes: - """ - result: ScaleType[int] = self.query_subtensor( + result: ScaleType[int] = self.query_subtensor( # type: ignore[assignment] name="Delegates", block=block, params=[hotkey_ss58], @@ -2210,7 +2216,7 @@ def get_existential_deposit(self, block: Optional[int] = None) -> Balance: Notes: - """ - result: Optional[ScaleType[int]] = self.substrate.get_constant( + result: Optional[ScaleType[int]] = self.substrate.get_constant( # type: ignore[assignment] module_name="Balances", constant_name="ExistentialDeposit", block_hash=self.determine_block_hash(block), @@ -2417,9 +2423,12 @@ def get_liquidity_list( ) ) - fee_global_tao = fixed_to_float(fee_global_tao_query[1]) - fee_global_alpha = fixed_to_float(fee_global_alpha_query[1]) - sqrt_price = fixed_to_float(sqrt_price_query[1]) + fee_global_tao_raw: FixedPoint = fee_global_tao_query[1] # type: ignore[assignment] + fee_global_alpha_raw: FixedPoint = fee_global_alpha_query[1] # type: ignore[assignment] + sqrt_price_raw: FixedPoint = sqrt_price_query[1] # type: ignore[assignment] + fee_global_tao = fixed_to_float(fee_global_tao_raw) + fee_global_alpha = fixed_to_float(fee_global_alpha_raw) + sqrt_price = fixed_to_float(sqrt_price_raw) current_tick = price_to_tick(sqrt_price**2) positions_values: list[tuple[PositionResponse, int, int]] = [] @@ -2448,7 +2457,8 @@ def get_liquidity_list( positions_storage_keys, block_hash=block_hash ) # iterator with just the values - ticks = iter([x[1] for x in ticks_query]) + tick_values: list[dict] = [x[1] for x in ticks_query] # type: ignore + ticks = iter(tick_values) positions: list[LiquidityPosition] = [] for position, tick_low_idx, tick_high_idx in positions_values: tick_low = next(ticks) @@ -3557,7 +3567,7 @@ def get_stake_for_hotkey( netuid: The subnet ID to query for. block: The block number at which to query the stake information. """ - hotkey_alpha_query = self.query_subtensor( + hotkey_alpha_query: ScaleType[int] = self.query_subtensor( # type: ignore[assignment] name="TotalHotkeyAlpha", params=[hotkey_ss58, netuid], block=block ) return Balance.from_rao(hotkey_alpha_query.value, netuid=netuid) @@ -3666,10 +3676,11 @@ def get_start_call_delay(self, block: Optional[int] = None) -> int: Return: Amount of blocks after the start call can be executed. """ - return self.query_subtensor( + query: ScaleType[int] = self.query_subtensor( # type: ignore[assignment] name="StartCallDelay", block=block, - ).value + ) + return query.value def get_subnet_burn_cost(self, block: Optional[int] = None) -> Optional[Balance]: """ @@ -3796,7 +3807,7 @@ def get_subnet_price( return Balance.from_tao(1) block_hash = self.determine_block_hash(block=block) - price_rao = self.substrate.runtime_call( + price_rao: int = self.substrate.runtime_call( # type: ignore[assignment] api="SwapRuntimeApi", method="current_alpha_price", params=[netuid], @@ -3924,7 +3935,7 @@ def get_timelocked_weight_commits( ) commits = result.records[0][1] if result.records else [] - return [WeightCommitInfo.from_vec_u8_v2(commit) for commit in commits] + return [WeightCommitInfo.from_vec_u8_v2(commit) for commit in commits] # type: ignore[arg-type,union-attr] def get_timestamp(self, block: Optional[int] = None) -> datetime: """ @@ -4332,7 +4343,7 @@ def is_subnet_active(self, netuid: int, block: Optional[int] = None) -> bool: block=block, params=[netuid], ) - qv: Optional[int] = query.value + qv: Optional[int] = query.value # type: ignore[assignment] if qv is None or qv <= 0: return False else: @@ -4600,11 +4611,14 @@ def subnet(self, netuid: int, block: Optional[int] = None) -> Optional[DynamicIn """ block_hash = self.determine_block_hash(block=block) - decoded: Optional[dict] = self.substrate.runtime_call( - api="SubnetInfoRuntimeApi", - method="get_dynamic_info", - params=[netuid], - block_hash=block_hash, + decoded = cast( + Optional[DynamicInfoResponse], + self.substrate.runtime_call( + api="SubnetInfoRuntimeApi", + method="get_dynamic_info", + params=[netuid], + block_hash=block_hash, + ), ) if isinstance(decoded, dict): diff --git a/bittensor/core/types.py b/bittensor/core/types.py index 18d9a9cb9f..238aa1d269 100644 --- a/bittensor/core/types.py +++ b/bittensor/core/types.py @@ -1,7 +1,7 @@ import argparse from abc import ABC from dataclasses import dataclass -from typing import Any, Literal, Optional, TypedDict, Union, TYPE_CHECKING +from typing import Any, Literal, Optional, TypedDict, Union, TYPE_CHECKING, NotRequired import numpy as np from numpy.typing import NDArray @@ -616,3 +616,39 @@ class CrowdloansResponse(TypedDict): call: Optional[dict] finalized: bool contributors_count: int + + +class SubnetIdentityResponse(TypedDict): + subnet_name: str + github_repo: str + subnet_contact: str + subnet_url: str + discord: str + description: str + logo_url: str + additional: str + + +class DynamicInfoResponse(TypedDict): + netuid: int + owner_hotkey: str + owner_coldkey: str + subnet_name: list[int] # needs bytes.decode('utf-8') to stringify + token_symbol: list[int] # needs bytes.decode('utf-8') to stringify + tempo: int + last_step: int + blocks_since_last_step: int + emission: int + alpha_in: int + alpha_out: int + tao_in: int + alpha_out_emission: int + alpha_in_emission: int + tao_in_emission: int + pending_alpha_emission: int + pending_root_emission: int + subnet_volume: int + network_registered_at: int + subnet_identity: SubnetIdentityResponse + moving_price: FixedPoint + price: NotRequired[Balance] From bf7a7301874d1fe86ea0b9daa37896d27bde16cd Mon Sep 17 00:00:00 2001 From: BD Himes Date: Thu, 16 Apr 2026 17:26:26 +0200 Subject: [PATCH 090/118] Import fixes --- bittensor/core/types.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/bittensor/core/types.py b/bittensor/core/types.py index 238aa1d269..67ce355cf4 100644 --- a/bittensor/core/types.py +++ b/bittensor/core/types.py @@ -1,7 +1,7 @@ import argparse from abc import ABC from dataclasses import dataclass -from typing import Any, Literal, Optional, TypedDict, Union, TYPE_CHECKING, NotRequired +from typing import Any, Literal, Optional, TypedDict, Union, TYPE_CHECKING import numpy as np from numpy.typing import NDArray @@ -21,6 +21,12 @@ ) from bittensor.utils.btlogging import logging +try: + from typing import NotRequired +except ImportError: + # fallback to typing_extensions if Python < 3.11 + from typing_extensions import NotRequired + if TYPE_CHECKING: from bittensor_wallet import Wallet from bittensor.utils.balance import Balance @@ -651,4 +657,4 @@ class DynamicInfoResponse(TypedDict): network_registered_at: int subnet_identity: SubnetIdentityResponse moving_price: FixedPoint - price: NotRequired[Balance] + price: NotRequired["Balance"] From ee92840ab17bfd73724a6a50aceab046be49c2db Mon Sep 17 00:00:00 2001 From: BD Himes Date: Thu, 16 Apr 2026 19:11:16 +0200 Subject: [PATCH 091/118] Fixes _decode_crowdloan_entry and adds a test --- bittensor/core/async_subtensor.py | 13 +- bittensor/core/chain_data/crowdloan_info.py | 2 +- bittensor/core/subtensor.py | 13 +- tests/helpers/integration_websocket_data.py | 115 ++++++++++++++++++ .../test_subtensor_integration.py | 54 +++++++- 5 files changed, 182 insertions(+), 15 deletions(-) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index ec49f9461b..5c0a02ae69 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -360,14 +360,15 @@ async def _decode_crowdloan_entry( call_data = data.get("call") if call_data and "Inline" in call_data: try: - inline_bytes = bytes(call_data["Inline"][0][0]) - scale_object = await self.substrate.create_scale_object( - type_string="Call", - data=scalecodec.ScaleBytes(inline_bytes), + runtime = await self.substrate.init_runtime(block_hash=block_hash) + call_obj = await self.substrate.create_scale_object( + "Call", + data=scalecodec.ScaleBytes(call_data["Inline"]), block_hash=block_hash, + runtime=runtime, ) - decoded_call = scale_object.decode() - data["call"] = decoded_call + call_value = call_obj.decode() + data["call"] = call_value except Exception as e: data["call"] = {"decode_error": str(e), "raw": call_data} diff --git a/bittensor/core/chain_data/crowdloan_info.py b/bittensor/core/chain_data/crowdloan_info.py index 51951287af..701972bad2 100644 --- a/bittensor/core/chain_data/crowdloan_info.py +++ b/bittensor/core/chain_data/crowdloan_info.py @@ -40,7 +40,7 @@ class CrowdloanInfo: funds_account: str raised: Balance target_address: Optional[str] - call: Optional[str] + call: Optional[dict] finalized: bool contributors_count: int diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 3c748b922b..5313a4195c 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -303,14 +303,13 @@ def _decode_crowdloan_entry( call_data = data.get("call") if call_data and "Inline" in call_data: try: - # TODO need a working crowdloan call to see what this actually is, but I probably need to just remove this section - inline_bytes = bytes(call_data["Inline"][0][0]) - decoded_call = self.substrate.create_scale_object( - type_string="Call", - data=scalecodec.ScaleBytes(inline_bytes), + call_obj = self.substrate.create_scale_object( + "Call", + data=scalecodec.ScaleBytes(call_data["Inline"]), block_hash=block_hash, - ).decode() - data["call"] = decoded_call + ) + call_value = call_obj.decode() + data["call"] = call_value except Exception as e: data["call"] = {"decode_error": str(e), "raw": call_data} diff --git a/tests/helpers/integration_websocket_data.py b/tests/helpers/integration_websocket_data.py index 307436fd86..a83492dc03 100644 --- a/tests/helpers/integration_websocket_data.py +++ b/tests/helpers/integration_websocket_data.py @@ -12427,4 +12427,119 @@ } }, }, + "decode_crowdloan_entry": { + "chain_getHead": { + "[]": { + "result": "0x8563f9d378caf2943368320b79ab5a90ec51ffb2ba7ab23002a811819297316d" + } + }, + "chain_getHeader": { + '["0xe0abc804366b36f9293c2a00fbc7a911c19247885f39a99a20f5c06389a4093e"]': { + "result": { + "parentHash": "0xf287b6afbb10dd480373d62a6d47f2b8f7c0419cc6ac5d61ba0c0b7f189d17ba", + "number": "0x79ccc2", + "stateRoot": "0x3247e495b19698fae0064a9bc61c7723d28adbb8c6806f8605ec7bab74bde028", + "extrinsicsRoot": "0xb79368ee28d3ca69081559d11928b9c718d4a9df865e2e49861ecd9d64e67b41", + "digest": { + "logs": [ + "0x066175726120c6c1d20800000000", + "0x0466726f6e890201f2206a7ea2500ddf7de73fb26e223298f68422086014b3a76e93b43af11fd8c410a39ff5f50fa3b06912db770e3dc5345aeb4e8e1811918362aa52268c2d508411d7058ab4599ff651cc7af3e46d62305f8cc0fc7cfe9010cad57b0456df620d42858bd66a675981f36caab57b5d45466a0dcddfd0447a83231dfdf38c70a34dc6247105ed2a0dfb4c02614ce6b405fd600cfe7a3f5d4dd578a8e0f87c712fec81", + "0x0561757261010114610206165bf247c62abbec8218aadf695afe1df9894195795d510eae5a2b1241a93d5e8f8262a6bb6a4d6d1eacd408609b2d925d770adbfa47f78f7efb0985", + ] + }, + } + }, + '["0x8563f9d378caf2943368320b79ab5a90ec51ffb2ba7ab23002a811819297316d"]': { + "result": { + "parentHash": "0xe0abc804366b36f9293c2a00fbc7a911c19247885f39a99a20f5c06389a4093e", + "number": "0x79ccc3", + "stateRoot": "0xf79f46321b59ee29fdb69327bf18e3d6bb7886ffdb972ba4a862a666c4d5ced6", + "extrinsicsRoot": "0xd4b2304c575e1d6d0a1e2baf6d7a1288415121e0b30d37319d182e95358565cf", + "digest": { + "logs": [ + "0x066175726120c7c1d20800000000", + "0x0466726f6e890201a781ed2bf8f8c5a308a7c69e1a2c1668c07fa1fd6105f57df141b282f34fb6b510a2973c5c0c1949c76e622fab9f33c8a31e63a16773ae1b7917babc0fc916304483ecd3e5ba88ad18abbee85b43ab2952d3ee8823dc7357db4cb2c20f2612b4fc29e6837aa829a90e91b78a072a91208f66b06d388347cc250e1475b9470f3c71bb26aa52583dca3f6f258fe1440fdac6044c6e6e3c3971c5e3cabbc75cb86790", + "0x05617572610101e059e901febcad1547d1f1809f6f663f56e6e14e2eac77e6899387b43ade5b023f0c4fc1bc7153be4c725b6c34fae5397ac994980550638ea480b0095294f08d", + ] + }, + } + }, + }, + "state_getRuntimeVersion": { + '["0xf287b6afbb10dd480373d62a6d47f2b8f7c0419cc6ac5d61ba0c0b7f189d17ba"]': { + "result": { + "specName": "node-subtensor", + "implName": "node-subtensor", + "authoringVersion": 1, + "specVersion": 393, + "implVersion": 1, + "apis": [ + ["0xdf6acb689907609b", 5], + ["0x37e397fc7c91f5e4", 2], + ["0x40fe3ad401f8959a", 6], + ["0xfbc577b9d747efd6", 1], + ["0xd2bc9897eed08f15", 3], + ["0xf78b278be53f454c", 2], + ["0xdd718d5cc53262d4", 1], + ["0xab3c0572291feb8b", 1], + ["0xed99c5acb25eedf5", 3], + ["0xbc9d89904f5b923f", 1], + ["0x37c8bb1350a9a2a8", 4], + ["0xf3ff14d5ab527059", 3], + ["0x582211f65bb14b89", 6], + ["0xe65b00e46cedd0aa", 2], + ["0x68b66ba122c93fa7", 2], + ["0x42e62be4a39e5b60", 1], + ["0x806df4ccaa9ed485", 1], + ["0x8375104b299b74c5", 1], + ["0x5d1fbfbe852f2807", 1], + ["0xc6886e2f8e598b0a", 1], + ["0xcbca25e39f142387", 2], + ["0xa8b093e6508d9e9c", 1], + ["0x1c4585bd5c707202", 1], + ], + "transactionVersion": 1, + "systemVersion": 1, + "stateVersion": 1, + } + }, + '["0xe0abc804366b36f9293c2a00fbc7a911c19247885f39a99a20f5c06389a4093e"]': { + "result": { + "specName": "node-subtensor", + "implName": "node-subtensor", + "authoringVersion": 1, + "specVersion": 393, + "implVersion": 1, + "apis": [ + ["0xdf6acb689907609b", 5], + ["0x37e397fc7c91f5e4", 2], + ["0x40fe3ad401f8959a", 6], + ["0xfbc577b9d747efd6", 1], + ["0xd2bc9897eed08f15", 3], + ["0xf78b278be53f454c", 2], + ["0xdd718d5cc53262d4", 1], + ["0xab3c0572291feb8b", 1], + ["0xed99c5acb25eedf5", 3], + ["0xbc9d89904f5b923f", 1], + ["0x37c8bb1350a9a2a8", 4], + ["0xf3ff14d5ab527059", 3], + ["0x582211f65bb14b89", 6], + ["0xe65b00e46cedd0aa", 2], + ["0x68b66ba122c93fa7", 2], + ["0x42e62be4a39e5b60", 1], + ["0x806df4ccaa9ed485", 1], + ["0x8375104b299b74c5", 1], + ["0x5d1fbfbe852f2807", 1], + ["0xc6886e2f8e598b0a", 1], + ["0xcbca25e39f142387", 2], + ["0xa8b093e6508d9e9c", 1], + ["0x1c4585bd5c707202", 1], + ], + "transactionVersion": 1, + "systemVersion": 1, + "stateVersion": 1, + } + }, + }, + }, } diff --git a/tests/integration_tests/test_subtensor_integration.py b/tests/integration_tests/test_subtensor_integration.py index 527076f07b..8f3708a3b0 100644 --- a/tests/integration_tests/test_subtensor_integration.py +++ b/tests/integration_tests/test_subtensor_integration.py @@ -1,7 +1,8 @@ import pytest -from bittensor.core.chain_data import AxonInfo, NeuronInfo +from bittensor.core.chain_data import AxonInfo, NeuronInfo, CrowdloanInfo from bittensor.core.subtensor import Subtensor +from bittensor.core.types import CrowdloansResponse from bittensor.utils.balance import Balance from tests.helpers.helpers import FakeWebsocket from bittensor.utils.mock.subtensor_mock import MockSubtensor @@ -161,3 +162,54 @@ async def test_archive_node_retry(mocker): current_block = subtensor.substrate.get_block_number() old_block = current_block - 1000 assert isinstance((subtensor.substrate.get_block(block_number=old_block)), dict) + + +@pytest.mark.asyncio +async def test_decode_crowdloan_entry(mocker): + entry = CrowdloansResponse( + **{ + "creator": "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY", + "deposit": 10000000000, + "min_contribution": 1000000000, + "end": 10055, + "cap": 100000000000, + "funds_account": "5EYCAe5fvncWtwXjyNBBHFPvNVDH5LPQ2harKS7KdAGbezkb", + "raised": 10000000000, + "target_address": None, + "call": { + "Inline": "0x0500008eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a4802286bee" + }, + "finalized": False, + "contributors_count": 1, + } + ) + subtensor = await prepare_test(mocker, "decode_crowdloan_entry") + actual = subtensor._decode_crowdloan_entry(17, entry) + expected = CrowdloanInfo( + id=17, + creator="5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY", + deposit=Balance.from_tao(10.0), + min_contribution=Balance.from_tao(1.0), + end=10055, + cap=Balance.from_tao(100.0), + funds_account="5EYCAe5fvncWtwXjyNBBHFPvNVDH5LPQ2harKS7KdAGbezkb", + raised=Balance.from_tao(10.0), + target_address=None, + call={ + "call_index": "0x0500", + "call_function": "transfer_allow_death", + "call_module": "Balances", + "call_args": [ + { + "name": "dest", + "type": "AccountIdLookupOf", + "value": "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty", + }, + {"name": "value", "type": "Balance", "value": 1000000000}, + ], + "call_hash": "0x117349ae93488150fa503b1ff7a0a94bfaa3ba193950a3d812ce32b9bb69fb02", + }, + finalized=False, + contributors_count=1, + ) + assert actual == expected From c68624f557ea035e71f002d52aa5f3bf176aefb8 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Fri, 17 Apr 2026 17:01:04 +0200 Subject: [PATCH 092/118] Bump cyscale --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b75919f75c..2f6d7d60ed 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ dependencies = [ "retry==0.9.2", "requests>=2.33.0,<3.0", "pydantic>=2.3,<3", - "cyscale==0.2.2", + "cyscale==0.3.0", "uvicorn", "bittensor-drand>=1.3.0,<2.0.0", "bittensor-wallet==4.0.1", From 70aafddc45d366e7946029597879130d3f4e0071 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Fri, 17 Apr 2026 19:48:37 +0200 Subject: [PATCH 093/118] trigger ci From b226a4f3f556e77072ab1d598b9f01183f999eb2 Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Fri, 17 Apr 2026 11:19:36 -0700 Subject: [PATCH 094/118] add `register_limit` to pallets --- .../extrinsics/pallets/subtensor_module.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/bittensor/core/extrinsics/pallets/subtensor_module.py b/bittensor/core/extrinsics/pallets/subtensor_module.py index a54d73b13a..019ecc58f0 100644 --- a/bittensor/core/extrinsics/pallets/subtensor_module.py +++ b/bittensor/core/extrinsics/pallets/subtensor_module.py @@ -274,6 +274,27 @@ def register( work=work, ) + def register_limit( + self, + netuid: int, + hotkey: str, + limit_price: int, + ) -> Call: + """Returns GenericCall instance for Subtensor function SubtensorModule.register_limit. + + Parameters: + netuid: The netuid of the subnet to register on. + hotkey: The hotkey SS58 address associated with the neuron. + limit_price: Maximum acceptable burn price in RAO. If on-chain burn exceeds this, + the transaction fails with RegistrationPriceLimitExceeded. + + Returns: + GenericCall instance. + """ + return self.create_composed_call( + netuid=netuid, hotkey=hotkey, limit_price=limit_price + ) + def register_network(self, hotkey: str) -> Call: """Returns GenericCall instance for Subtensor function SubtensorModule.register_network. From e934bff5f732fda613b7eb65a28c2ea3a1e1c9df Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Fri, 17 Apr 2026 12:11:56 -0700 Subject: [PATCH 095/118] add extrinsics --- .../core/extrinsics/asyncex/registration.py | 149 +++++++++++++++++- bittensor/core/extrinsics/registration.py | 144 ++++++++++++++++- 2 files changed, 291 insertions(+), 2 deletions(-) diff --git a/bittensor/core/extrinsics/asyncex/registration.py b/bittensor/core/extrinsics/asyncex/registration.py index 2e9531380f..5b4897fbf9 100644 --- a/bittensor/core/extrinsics/asyncex/registration.py +++ b/bittensor/core/extrinsics/asyncex/registration.py @@ -5,11 +5,12 @@ import asyncio from typing import Optional, Union, TYPE_CHECKING -from bittensor.core.errors import RegistrationError +from bittensor.core.errors import BalanceTypeError, RegistrationError from bittensor.core.extrinsics.asyncex.mev_shield import submit_encrypted_extrinsic from bittensor.core.extrinsics.pallets import SubtensorModule from bittensor.core.settings import DEFAULT_MEV_PROTECTION from bittensor.core.types import ExtrinsicResponse +from bittensor.utils.balance import Balance from bittensor.utils.btlogging import logging from bittensor.utils.registration import create_pow_async, log_no_torch_error, torch @@ -158,6 +159,152 @@ async def burned_register_extrinsic( return ExtrinsicResponse.from_exception(raise_error=raise_error, error=error) +async def register_limit_extrinsic( + subtensor: "AsyncSubtensor", + wallet: "Wallet", + netuid: int, + limit_price: Balance, + *, + mev_protection: bool = DEFAULT_MEV_PROTECTION, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + wait_for_revealed_execution: bool = True, +) -> ExtrinsicResponse: + """Registers the wallet to chain by recycling TAO, with a maximum burn price limit. + + Parameters: + subtensor: Subtensor instance. + wallet: Bittensor wallet object. + netuid: The ``netuid`` of the subnet to register on. + limit_price: Maximum acceptable burn price as a Balance instance. If the on-chain burn price exceeds + this value, the transaction will fail with RegistrationPriceLimitExceeded. + mev_protection: If True, encrypts and submits the transaction through the MEV Shield pallet to protect + against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators + decrypt and execute it. If False, submits the transaction directly without encryption. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You can + think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + """ + try: + if not ( + unlocked := ExtrinsicResponse.unlock_wallet( + wallet, raise_error, unlock_type="both" + ) + ).success: + return unlocked + + if not isinstance(limit_price, Balance): + raise BalanceTypeError("`limit_price` must be an instance of Balance.") + + block_hash = await subtensor.substrate.get_chain_head() + if not await subtensor.subnet_exists(netuid=netuid, block_hash=block_hash): + return ExtrinsicResponse( + False, f"Subnet {netuid} does not exist." + ).with_log() + + neuron, old_balance, recycle_amount = await asyncio.gather( + subtensor.get_neuron_for_pubkey_and_subnet( + netuid=netuid, + hotkey_ss58=wallet.hotkey.ss58_address, + block_hash=block_hash, + ), + subtensor.get_balance( + address=wallet.coldkeypub.ss58_address, block_hash=block_hash + ), + subtensor.recycle(netuid=netuid, block_hash=block_hash), + ) + + if not neuron.is_null: + message = "Already registered." + logging.debug(f"[green]{message}[/green]") + logging.debug(f"\t\tuid: [blue]{neuron.uid}[/blue]") + logging.debug(f"\t\tnetuid: [blue]{neuron.netuid}[/blue]") + logging.debug(f"\t\thotkey: [blue]{neuron.hotkey}[/blue]") + logging.debug(f"\t\tcoldkey: [blue]{neuron.coldkey}[/blue]") + return ExtrinsicResponse( + message=message, data={"neuron": neuron, "old_balance": old_balance} + ) + + logging.debug(f"Recycling {recycle_amount} to register on subnet:{netuid}") + + call = await SubtensorModule(subtensor).register_limit( + netuid=netuid, + hotkey=wallet.hotkey.ss58_address, + limit_price=limit_price.rao, + ) + + if mev_protection: + response = await submit_encrypted_extrinsic( + subtensor=subtensor, + wallet=wallet, + call=call, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + else: + response = await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + extrinsic_fee = response.extrinsic_fee + logging.debug( + f"The registration fee for SN #[blue]{netuid}[/blue] is [blue]{extrinsic_fee}[/blue]." + ) + if not response.success: + logging.error(f"[red]{response.message}[/red]") + await asyncio.sleep(0.5) + return response + + new_balance = await subtensor.get_balance( + address=wallet.coldkeypub.ss58_address + ) + + logging.debug( + f"Balance: [blue]{old_balance}[/blue] :arrow_right: [green]{new_balance}[/green]" + ) + is_registered = await subtensor.is_hotkey_registered( + netuid=netuid, hotkey_ss58=wallet.hotkey.ss58_address + ) + + response.data = { + "neuron": neuron, + "balance_before": old_balance, + "balance_after": new_balance, + "recycle_amount": recycle_amount, + } + + if is_registered: + logging.debug("[green]Registered.[/green]") + return response + + message = f"Neuron with hotkey {wallet.hotkey.ss58_address} not found in subnet {netuid} after registration." + return ExtrinsicResponse( + success=False, + message=message, + extrinsic=response.extrinsic, + error=RegistrationError(message), + ).with_log() + + except Exception as error: + return ExtrinsicResponse.from_exception(raise_error=raise_error, error=error) + + async def register_subnet_extrinsic( subtensor: "AsyncSubtensor", wallet: "Wallet", diff --git a/bittensor/core/extrinsics/registration.py b/bittensor/core/extrinsics/registration.py index fd8952bbf8..cb24a6345c 100644 --- a/bittensor/core/extrinsics/registration.py +++ b/bittensor/core/extrinsics/registration.py @@ -5,11 +5,12 @@ import time from typing import Optional, Union, TYPE_CHECKING -from bittensor.core.errors import RegistrationError +from bittensor.core.errors import BalanceTypeError, RegistrationError from bittensor.core.extrinsics.mev_shield import submit_encrypted_extrinsic from bittensor.core.extrinsics.pallets import SubtensorModule from bittensor.core.settings import DEFAULT_MEV_PROTECTION from bittensor.core.types import ExtrinsicResponse +from bittensor.utils.balance import Balance from bittensor.utils.btlogging import logging from bittensor.utils.registration import create_pow, log_no_torch_error, torch @@ -153,6 +154,147 @@ def burned_register_extrinsic( return ExtrinsicResponse.from_exception(raise_error=raise_error, error=error) +def register_limit_extrinsic( + subtensor: "Subtensor", + wallet: "Wallet", + netuid: int, + limit_price: Balance, + *, + mev_protection: bool = DEFAULT_MEV_PROTECTION, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + wait_for_revealed_execution: bool = True, +) -> ExtrinsicResponse: + """Registers the wallet to chain by recycling TAO, with a maximum burn price limit. + + Parameters: + subtensor: Subtensor instance. + wallet: Bittensor wallet object. + netuid: The ``netuid`` of the subnet to register on. + limit_price: Maximum acceptable burn price as a Balance instance. If the on-chain burn price exceeds + this value, the transaction will fail with RegistrationPriceLimitExceeded. + mev_protection: If True, encrypts and submits the transaction through the MEV Shield pallet to protect + against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators + decrypt and execute it. If False, submits the transaction directly without encryption. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You can + think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + """ + try: + if not ( + unlocked := ExtrinsicResponse.unlock_wallet( + wallet, raise_error, unlock_type="both" + ) + ).success: + return unlocked + + if not isinstance(limit_price, Balance): + raise BalanceTypeError("`limit_price` must be an instance of Balance.") + + block = subtensor.get_current_block() + if not subtensor.subnet_exists(netuid=netuid, block=block): + return ExtrinsicResponse( + False, f"Subnet {netuid} does not exist." + ).with_log() + + neuron = subtensor.get_neuron_for_pubkey_and_subnet( + netuid=netuid, hotkey_ss58=wallet.hotkey.ss58_address, block=block + ) + + old_balance = subtensor.get_balance( + address=wallet.coldkeypub.ss58_address, block=block + ) + + if not neuron.is_null: + message = "Already registered." + logging.debug(f"[green]{message}[/green]") + logging.debug(f"\t\tuid: [blue]{neuron.uid}[/blue]") + logging.debug(f"\t\tnetuid: [blue]{neuron.netuid}[/blue]") + logging.debug(f"\t\thotkey: [blue]{neuron.hotkey}[/blue]") + logging.debug(f"\t\tcoldkey: [blue]{neuron.coldkey}[/blue]") + return ExtrinsicResponse( + message=message, data={"neuron": neuron, "old_balance": old_balance} + ) + + recycle_amount = subtensor.recycle(netuid=netuid, block=block) + logging.debug(f"Recycling {recycle_amount} to register on subnet:{netuid}") + + call = SubtensorModule(subtensor).register_limit( + netuid=netuid, + hotkey=wallet.hotkey.ss58_address, + limit_price=limit_price.rao, + ) + + if mev_protection: + response = submit_encrypted_extrinsic( + subtensor=subtensor, + wallet=wallet, + call=call, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + else: + response = subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + extrinsic_fee = response.extrinsic_fee + logging.debug( + f"The registration fee for SN #[blue]{netuid}[/blue] is [blue]{extrinsic_fee}[/blue]." + ) + if not response.success: + logging.error(f"[red]{response.message}[/red]") + time.sleep(0.5) + return response + + new_balance = subtensor.get_balance(address=wallet.coldkeypub.ss58_address) + + logging.debug( + f"Balance: [blue]{old_balance}[/blue] :arrow_right: [green]{new_balance}[/green]" + ) + is_registered = subtensor.is_hotkey_registered( + netuid=netuid, hotkey_ss58=wallet.hotkey.ss58_address + ) + + response.data = { + "neuron": neuron, + "balance_before": old_balance, + "balance_after": new_balance, + "recycle_amount": recycle_amount, + } + + if is_registered: + logging.debug("[green]Registered.[/green]") + return response + + message = f"Neuron with hotkey {wallet.hotkey.ss58_address} not found in subnet {netuid} after registration." + return ExtrinsicResponse( + success=False, + message=message, + extrinsic=response.extrinsic, + error=RegistrationError(message), + ).with_log() + + except Exception as error: + return ExtrinsicResponse.from_exception(raise_error=raise_error, error=error) + + def register_subnet_extrinsic( subtensor: "Subtensor", wallet: "Wallet", From 98278984b8e76942501eecf17e2b86e4b80bcf94 Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Fri, 17 Apr 2026 12:12:13 -0700 Subject: [PATCH 096/118] add async/subtensor methods --- bittensor/core/async_subtensor.py | 58 +++++++++++++++++++++++++++++++ bittensor/core/subtensor.py | 57 ++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index 0f22901573..a6d9c76639 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -99,6 +99,7 @@ from bittensor.core.extrinsics.asyncex.registration import ( burned_register_extrinsic, register_extrinsic, + register_limit_extrinsic, register_subnet_extrinsic, set_subnet_identity_extrinsic, ) @@ -7967,6 +7968,63 @@ async def register( wait_for_revealed_execution=wait_for_revealed_execution, ) + async def register_limit( + self, + wallet: "Wallet", + netuid: int, + limit_price: Balance, + *, + mev_protection: bool = DEFAULT_MEV_PROTECTION, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + wait_for_revealed_execution: bool = True, + ) -> ExtrinsicResponse: + """ + Registers a neuron on the Bittensor network by recycling TAO, with a maximum burn price limit. + + Unlike ``burned_register``, this method includes a ``limit_price`` parameter that ensures the registration + will only proceed if the current on-chain burn price does not exceed the specified maximum. This protects + against unexpected price spikes between reading the price and submitting the transaction. + + Parameters: + wallet: The wallet associated with the neuron to be registered. + netuid: The unique identifier of the subnet. + limit_price: Maximum acceptable burn price as a Balance instance. If the on-chain burn price exceeds + this value, the transaction will fail with RegistrationPriceLimitExceeded. + mev_protection: If ``True``, encrypts and submits the transaction through the MEV Shield pallet to protect + against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators + decrypt and execute it. If ``False``, submits the transaction directly without encryption. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You + can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning ``False`` if unsuccessful. + wait_for_inclusion: Waits for the transaction to be included in a block. + wait_for_finalization: Waits for the transaction to be finalized on the blockchain. + wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + Notes: + - Rate Limits: + """ + check_balance_amount(limit_price) + async with self: + return await register_limit_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + limit_price=limit_price, + mev_protection=mev_protection, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + async def register_subnet( self: "AsyncSubtensor", wallet: "Wallet", diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 35a8818833..a071e06eab 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -98,6 +98,7 @@ from bittensor.core.extrinsics.registration import ( burned_register_extrinsic, register_extrinsic, + register_limit_extrinsic, register_subnet_extrinsic, set_subnet_identity_extrinsic, ) @@ -6760,6 +6761,62 @@ def register( wait_for_revealed_execution=wait_for_revealed_execution, ) + def register_limit( + self, + wallet: "Wallet", + netuid: int, + limit_price: Balance, + *, + mev_protection: bool = DEFAULT_MEV_PROTECTION, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + wait_for_revealed_execution: bool = True, + ) -> ExtrinsicResponse: + """ + Registers a neuron on the Bittensor network by recycling TAO, with a maximum burn price limit. + + Unlike ``burned_register``, this method includes a ``limit_price`` parameter that ensures the registration + will only proceed if the current on-chain burn price does not exceed the specified maximum. This protects + against unexpected price spikes between reading the price and submitting the transaction. + + Parameters: + wallet: The wallet associated with the neuron to be registered. + netuid: The unique identifier of the subnet. + limit_price: Maximum acceptable burn price as a Balance instance. If the on-chain burn price exceeds + this value, the transaction will fail with RegistrationPriceLimitExceeded. + mev_protection: If ``True``, encrypts and submits the transaction through the MEV Shield pallet to protect + against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators + decrypt and execute it. If ``False``, submits the transaction directly without encryption. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You + can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning ``False`` if unsuccessful. + wait_for_inclusion: Waits for the transaction to be included in a block. + wait_for_finalization: Waits for the transaction to be finalized on the blockchain. + wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + Notes: + - Rate Limits: + """ + check_balance_amount(limit_price) + return register_limit_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + limit_price=limit_price, + mev_protection=mev_protection, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + def register_subnet( self, wallet: "Wallet", From 3467083d89d596ffac065abef8c4971ecec61765 Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Fri, 17 Apr 2026 12:12:28 -0700 Subject: [PATCH 097/118] update SubtensorApi --- bittensor/extras/subtensor_api/extrinsics.py | 1 + bittensor/extras/subtensor_api/subnets.py | 1 + 2 files changed, 2 insertions(+) diff --git a/bittensor/extras/subtensor_api/extrinsics.py b/bittensor/extras/subtensor_api/extrinsics.py index d9a3e59f84..878a638ec9 100644 --- a/bittensor/extras/subtensor_api/extrinsics.py +++ b/bittensor/extras/subtensor_api/extrinsics.py @@ -27,6 +27,7 @@ def __init__(self, subtensor: Union["_Subtensor", "_AsyncSubtensor"]): self.move_stake = subtensor.move_stake self.refund_crowdloan = subtensor.refund_crowdloan self.register = subtensor.register + self.register_limit = subtensor.register_limit self.register_subnet = subtensor.register_subnet self.remove_liquidity = subtensor.remove_liquidity self.reveal_weights = subtensor.reveal_weights diff --git a/bittensor/extras/subtensor_api/subnets.py b/bittensor/extras/subtensor_api/subnets.py index 1c969fef00..37cf2ccece 100644 --- a/bittensor/extras/subtensor_api/subnets.py +++ b/bittensor/extras/subtensor_api/subnets.py @@ -48,6 +48,7 @@ def __init__(self, subtensor: Union["_Subtensor", "_AsyncSubtensor"]): self.min_allowed_weights = subtensor.min_allowed_weights self.recycle = subtensor.recycle self.register = subtensor.register + self.register_limit = subtensor.register_limit self.register_subnet = subtensor.register_subnet self.set_subnet_identity = subtensor.set_subnet_identity self.start_call = subtensor.start_call From 817350e451f7d5085433a71da37b04e121351aac Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Fri, 17 Apr 2026 12:12:42 -0700 Subject: [PATCH 098/118] add unit tests --- .../extrinsics/asyncex/test_registration.py | 56 +++++++++++++++++++ .../extrinsics/test_registration.py | 55 ++++++++++++++++++ 2 files changed, 111 insertions(+) diff --git a/tests/unit_tests/extrinsics/asyncex/test_registration.py b/tests/unit_tests/extrinsics/asyncex/test_registration.py index 278d20c2c8..254e22c64c 100644 --- a/tests/unit_tests/extrinsics/asyncex/test_registration.py +++ b/tests/unit_tests/extrinsics/asyncex/test_registration.py @@ -1,5 +1,6 @@ import pytest from bittensor.core.types import ExtrinsicResponse +from bittensor.utils.balance import Balance from bittensor.core.extrinsics.asyncex import registration as async_registration @@ -432,3 +433,58 @@ async def test_set_subnet_identity_extrinsic_is_failed(subtensor, fake_wallet, m ) assert result == mocked_sign_and_send_extrinsic.return_value + + +@pytest.mark.parametrize( + "subnet_exists, neuron_is_null, recycle_success, is_registered, expected_result, test_id", + [ + # Happy paths + (True, False, None, None, True, "neuron-not-null"), + (True, True, True, True, True, "happy-path-wallet-registered"), + # Error paths + (False, True, False, None, False, "subnet-non-existence"), + (True, True, False, False, False, "error-path-recycling-failed"), + (True, True, True, False, False, "error-path-not-registered"), + ], +) +@pytest.mark.asyncio +async def test_register_limit_extrinsic( + subtensor, + fake_wallet, + subnet_exists, + neuron_is_null, + recycle_success, + is_registered, + expected_result, + test_id, + mocker, +): + # Arrange + fake_wallet.hotkey.ss58_address = "hotkey_ss58" + fake_wallet.coldkeypub.ss58_address = "coldkey_ss58" + + mocker.patch.object(subtensor, "subnet_exists", return_value=subnet_exists) + mocker.patch.object( + subtensor, + "get_neuron_for_pubkey_and_subnet", + return_value=mocker.Mock(is_null=neuron_is_null), + ) + mocker.patch.object(subtensor, "get_balance", return_value=mocker.Mock()) + mocker.patch.object(subtensor, "recycle", return_value=mocker.Mock()) + mocker.patch.object(subtensor, "compose_call") + mocker.patch.object( + subtensor, + "sign_and_send_extrinsic", + return_value=ExtrinsicResponse(recycle_success, "Mock error message"), + ) + mocker.patch.object(subtensor, "is_hotkey_registered", return_value=is_registered) + + # Act + result = await async_registration.register_limit_extrinsic( + subtensor=subtensor, + wallet=fake_wallet, + netuid=123, + limit_price=Balance.from_rao(1000000000), + ) + # Assert + assert result.success == expected_result, f"Test failed for test_id: {test_id}" diff --git a/tests/unit_tests/extrinsics/test_registration.py b/tests/unit_tests/extrinsics/test_registration.py index cefd3ca6b8..0ffb73c5fe 100644 --- a/tests/unit_tests/extrinsics/test_registration.py +++ b/tests/unit_tests/extrinsics/test_registration.py @@ -3,6 +3,7 @@ from bittensor.core.types import ExtrinsicResponse from bittensor.core.extrinsics import registration from bittensor.core.subtensor import Subtensor +from bittensor.utils.balance import Balance from bittensor.utils.registration import POWSolution @@ -364,3 +365,57 @@ def test_set_subnet_identity_extrinsic_is_failed(mock_subtensor, mock_wallet, mo ) assert result == mocked_sign_and_send_extrinsic.return_value + + +@pytest.mark.parametrize( + "subnet_exists, neuron_is_null, recycle_success, is_registered, expected_result, test_id", + [ + # Happy paths + (True, False, None, None, True, "neuron-not-null"), + (True, True, True, True, True, "happy-path-wallet-registered"), + # Error paths + (True, True, True, False, False, "error-path-not-registered"), + (False, True, False, None, False, "subnet-non-existence"), + (True, True, False, False, False, "error-path-recycling-failed"), + ], +) +def test_register_limit_extrinsic( + mock_subtensor, + mock_wallet, + subnet_exists, + neuron_is_null, + recycle_success, + is_registered, + expected_result, + test_id, + mocker, +): + # Arrange + mock_substrate_ = mocker.MagicMock( + **{"get_payment_info.return_value": {"partial_fee": 10}} + ) + mocker.patch.object(mock_subtensor, "substrate", mock_substrate_) + mocker.patch.object(mock_subtensor, "subnet_exists", return_value=subnet_exists) + mocker.patch.object( + mock_subtensor, + "get_neuron_for_pubkey_and_subnet", + return_value=mocker.MagicMock(is_null=neuron_is_null), + ) + mocker.patch.object( + mock_subtensor, + "sign_and_send_extrinsic", + return_value=ExtrinsicResponse(recycle_success, "Mock error message"), + ) + mocker.patch.object( + mock_subtensor, "is_hotkey_registered", return_value=is_registered + ) + + # Act + result = registration.register_limit_extrinsic( + subtensor=mock_subtensor, + wallet=mock_wallet, + netuid=123, + limit_price=Balance.from_rao(1000000000), + ) + # Assert + assert result.success == expected_result, f"Test failed for test_id: {test_id}" From a8c2e176047b145e9ec5ab8ae3521d161e26d02b Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Fri, 17 Apr 2026 12:12:47 -0700 Subject: [PATCH 099/118] add e2e tests --- tests/e2e_tests/test_register_limit.py | 105 +++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 tests/e2e_tests/test_register_limit.py diff --git a/tests/e2e_tests/test_register_limit.py b/tests/e2e_tests/test_register_limit.py new file mode 100644 index 0000000000..adc8ad8234 --- /dev/null +++ b/tests/e2e_tests/test_register_limit.py @@ -0,0 +1,105 @@ +import pytest + +from bittensor.utils.balance import Balance +from bittensor.utils.btlogging import logging +from tests.e2e_tests.utils import ( + TestSubnet, + ACTIVATE_SUBNET, + REGISTER_SUBNET, +) + + +def test_register_limit_success(subtensor, alice_wallet, bob_wallet): + """Tests successful registration with register_limit when limit_price is above burn.""" + alice_sn = TestSubnet(subtensor) + alice_sn.execute_steps( + [ + REGISTER_SUBNET(alice_wallet), + ACTIVATE_SUBNET(alice_wallet), + ] + ) + + recycle = subtensor.subnets.recycle(alice_sn.netuid) + assert recycle is not None, ( + "Recycle amount should not be None after subnet activation" + ) + + limit_price = recycle * 2 + logging.console.info( + f"Registering Bob with register_limit on SN #{alice_sn.netuid}, " + f"recycle={recycle}, limit_price={limit_price}" + ) + result = subtensor.subnets.register_limit(bob_wallet, alice_sn.netuid, limit_price) + assert result.success, "register_limit should succeed with limit above burn price" + + +def test_register_limit_price_exceeded(subtensor, alice_wallet, bob_wallet): + """Tests that register_limit fails when limit_price is below the current burn price.""" + alice_sn = TestSubnet(subtensor) + alice_sn.execute_steps( + [ + REGISTER_SUBNET(alice_wallet), + ACTIVATE_SUBNET(alice_wallet), + ] + ) + + logging.console.info( + f"Attempting register_limit with limit_price=1 on SN #{alice_sn.netuid}" + ) + result = subtensor.subnets.register_limit( + bob_wallet, alice_sn.netuid, limit_price=Balance.from_rao(1) + ) + assert not result.success, ( + "register_limit should fail with limit_price=1 (below burn)" + ) + + +@pytest.mark.asyncio +async def test_register_limit_success_async(async_subtensor, alice_wallet, bob_wallet): + """Tests successful async registration with register_limit when limit_price is above burn.""" + alice_sn = TestSubnet(async_subtensor) + await alice_sn.async_execute_steps( + [ + REGISTER_SUBNET(alice_wallet), + ACTIVATE_SUBNET(alice_wallet), + ] + ) + + recycle = await async_subtensor.subnets.recycle(alice_sn.netuid) + assert recycle is not None, ( + "Recycle amount should not be None after subnet activation" + ) + + limit_price = recycle * 2 + logging.console.info( + f"Registering Bob with register_limit on SN #{alice_sn.netuid}, " + f"recycle={recycle}, limit_price={limit_price}" + ) + result = await async_subtensor.subnets.register_limit( + bob_wallet, alice_sn.netuid, limit_price + ) + assert result.success, "register_limit should succeed with limit above burn price" + + +@pytest.mark.asyncio +async def test_register_limit_price_exceeded_async( + async_subtensor, alice_wallet, bob_wallet +): + """Tests that async register_limit fails when limit_price is below the current burn price.""" + alice_sn = TestSubnet(async_subtensor) + await alice_sn.async_execute_steps( + [ + REGISTER_SUBNET(alice_wallet), + ACTIVATE_SUBNET(alice_wallet), + ] + ) + + logging.console.info( + f"Attempting register_limit with limit_price=1 on SN #{alice_sn.netuid}" + ) + result = await async_subtensor.subnets.register_limit( + bob_wallet, alice_sn.netuid, limit_price=Balance.from_rao(1) + ) + assert not result.success, ( + "register_limit should fail with limit_price=1 (below burn)" + ) From 1ae73e11f6adff11bcb7bfbd7bd3cb3484dec792 Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Fri, 17 Apr 2026 12:13:46 -0700 Subject: [PATCH 100/118] update dev framework --- .../dev_framework/calls/non_sudo_calls.py | 74 +++++++++++++------ .../extras/dev_framework/calls/pallets.py | 2 +- .../extras/dev_framework/calls/sudo_calls.py | 19 +++-- 3 files changed, 67 insertions(+), 28 deletions(-) diff --git a/bittensor/extras/dev_framework/calls/non_sudo_calls.py b/bittensor/extras/dev_framework/calls/non_sudo_calls.py index 62fb1166f0..537351c5c7 100644 --- a/bittensor/extras/dev_framework/calls/non_sudo_calls.py +++ b/bittensor/extras/dev_framework/calls/non_sudo_calls.py @@ -11,7 +11,7 @@ Note: Any manual changes will be overwritten the next time the generator is run. - Subtensor spec version: 376 + Subtensor spec version: 397 """ from collections import namedtuple @@ -26,10 +26,10 @@ ) # args: [delegate: AccountIdLookupOf, proxy_type: T::ProxyType, delay: BlockNumberFor] | Pallet: Proxy ADD_STAKE = namedtuple( "ADD_STAKE", ["wallet", "pallet", "hotkey", "netuid", "amount_staked"] -) # args: [hotkey: T::AccountId, netuid: NetUid, amount_staked: TaoCurrency] | Pallet: SubtensorModule +) # args: [hotkey: T::AccountId, netuid: NetUid, amount_staked: TaoBalance] | Pallet: SubtensorModule ADD_STAKE_BURN = namedtuple( "ADD_STAKE_BURN", ["wallet", "pallet", "hotkey", "netuid", "amount", "limit"] -) # args: [hotkey: T::AccountId, netuid: NetUid, amount: TaoCurrency, limit: Option] | Pallet: SubtensorModule +) # args: [hotkey: T::AccountId, netuid: NetUid, amount: TaoBalance, limit: Option] | Pallet: SubtensorModule ADD_STAKE_LIMIT = namedtuple( "ADD_STAKE_LIMIT", [ @@ -41,7 +41,7 @@ "limit_price", "allow_partial", ], -) # args: [hotkey: T::AccountId, netuid: NetUid, amount_staked: TaoCurrency, limit_price: TaoCurrency, allow_partial: bool] | Pallet: SubtensorModule +) # args: [hotkey: T::AccountId, netuid: NetUid, amount_staked: TaoBalance, limit_price: TaoBalance, allow_partial: bool] | Pallet: SubtensorModule ANNOUNCE = namedtuple( "ANNOUNCE", ["wallet", "pallet", "real", "call_hash"] ) # args: [real: AccountIdLookupOf, call_hash: CallHashOf] | Pallet: Proxy @@ -49,8 +49,8 @@ "ANNOUNCE_COLDKEY_SWAP", ["wallet", "pallet", "new_coldkey_hash"] ) # args: [new_coldkey_hash: T::Hash] | Pallet: SubtensorModule ANNOUNCE_NEXT_KEY = namedtuple( - "ANNOUNCE_NEXT_KEY", ["wallet", "pallet", "public_key"] -) # args: [public_key: BoundedVec>] | Pallet: MevShield + "ANNOUNCE_NEXT_KEY", ["wallet", "pallet", "enc_key"] +) # args: [enc_key: Option] | Pallet: MevShield APPLY_AUTHORIZED_UPGRADE = namedtuple( "APPLY_AUTHORIZED_UPGRADE", ["wallet", "pallet", "code"] ) # args: [code: Vec] | Pallet: System @@ -126,7 +126,7 @@ ) # args: [netuid: NetUid, hotkey: T::AccountId] | Pallet: SubtensorModule BURN_ALPHA = namedtuple( "BURN_ALPHA", ["wallet", "pallet", "hotkey", "amount", "netuid"] -) # args: [hotkey: T::AccountId, amount: AlphaCurrency, netuid: NetUid] | Pallet: SubtensorModule +) # args: [hotkey: T::AccountId, amount: AlphaBalance, netuid: NetUid] | Pallet: SubtensorModule CALL = namedtuple( "CALL", ["wallet", "pallet", "dest", "value", "gas_limit", "storage_deposit_limit", "data"], @@ -171,6 +171,13 @@ CLAIM_ROOT = namedtuple( "CLAIM_ROOT", ["wallet", "pallet", "subnets"] ) # args: [subnets: BTreeSet] | Pallet: SubtensorModule +CLEAR_COLDKEY_SWAP_ANNOUNCEMENT = namedtuple( + "CLEAR_COLDKEY_SWAP_ANNOUNCEMENT", + [ + "wallet", + "pallet", + ], +) # args: [] | Pallet: SubtensorModule CLEAR_IDENTITY = namedtuple( "CLEAR_IDENTITY", ["wallet", "pallet", "identified"] ) # args: [identified: T::AccountId] | Pallet: Registry @@ -423,9 +430,6 @@ KILL_STORAGE = namedtuple( "KILL_STORAGE", ["wallet", "pallet", "keys"] ) # args: [keys: Vec] | Pallet: System -MARK_DECRYPTION_FAILED = namedtuple( - "MARK_DECRYPTION_FAILED", ["wallet", "pallet", "id", "reason"] -) # args: [id: T::Hash, reason: BoundedVec>] | Pallet: MevShield MIGRATE = namedtuple( "MIGRATE", ["wallet", "pallet", "weight_limit"] ) # args: [weight_limit: Weight] | Pallet: Contracts @@ -444,7 +448,7 @@ "destination_netuid", "alpha_amount", ], -) # args: [origin_hotkey: T::AccountId, destination_hotkey: T::AccountId, origin_netuid: NetUid, destination_netuid: NetUid, alpha_amount: AlphaCurrency] | Pallet: SubtensorModule +) # args: [origin_hotkey: T::AccountId, destination_hotkey: T::AccountId, origin_netuid: NetUid, destination_netuid: NetUid, alpha_amount: AlphaBalance] | Pallet: SubtensorModule NOTE_PREIMAGE = namedtuple( "NOTE_PREIMAGE", ["wallet", "pallet", "bytes"] ) # args: [bytes: Vec] | Pallet: Preimage @@ -470,7 +474,7 @@ ) # args: [delegate: AccountIdLookupOf, real: AccountIdLookupOf, force_proxy_type: Option, call: Box<::RuntimeCall>] | Pallet: Proxy RECYCLE_ALPHA = namedtuple( "RECYCLE_ALPHA", ["wallet", "pallet", "hotkey", "amount", "netuid"] -) # args: [hotkey: T::AccountId, amount: AlphaCurrency, netuid: NetUid] | Pallet: SubtensorModule +) # args: [hotkey: T::AccountId, amount: AlphaBalance, netuid: NetUid] | Pallet: SubtensorModule REFUND = namedtuple( "REFUND", ["wallet", "pallet", "crowdloan_id"] ) # args: [crowdloan_id: CrowdloanId] | Pallet: Crowdloan @@ -490,6 +494,9 @@ REGISTER_LEASED_NETWORK = namedtuple( "REGISTER_LEASED_NETWORK", ["wallet", "pallet", "emissions_share", "end_block"] ) # args: [emissions_share: Percent, end_block: Option>] | Pallet: SubtensorModule +REGISTER_LIMIT = namedtuple( + "REGISTER_LIMIT", ["wallet", "pallet", "netuid", "hotkey", "limit_price"] +) # args: [netuid: NetUid, hotkey: T::AccountId, limit_price: u64] | Pallet: SubtensorModule REGISTER_NETWORK = namedtuple( "REGISTER_NETWORK", ["wallet", "pallet", "hotkey"] ) # args: [hotkey: T::AccountId] | Pallet: SubtensorModule @@ -536,10 +543,10 @@ ) # args: [delegate: AccountIdLookupOf, proxy_type: T::ProxyType, delay: BlockNumberFor] | Pallet: Proxy REMOVE_STAKE = namedtuple( "REMOVE_STAKE", ["wallet", "pallet", "hotkey", "netuid", "amount_unstaked"] -) # args: [hotkey: T::AccountId, netuid: NetUid, amount_unstaked: AlphaCurrency] | Pallet: SubtensorModule +) # args: [hotkey: T::AccountId, netuid: NetUid, amount_unstaked: AlphaBalance] | Pallet: SubtensorModule REMOVE_STAKE_FULL_LIMIT = namedtuple( "REMOVE_STAKE_FULL_LIMIT", ["wallet", "pallet", "hotkey", "netuid", "limit_price"] -) # args: [hotkey: T::AccountId, netuid: NetUid, limit_price: Option] | Pallet: SubtensorModule +) # args: [hotkey: T::AccountId, netuid: NetUid, limit_price: Option] | Pallet: SubtensorModule REMOVE_STAKE_LIMIT = namedtuple( "REMOVE_STAKE_LIMIT", [ @@ -551,7 +558,7 @@ "limit_price", "allow_partial", ], -) # args: [hotkey: T::AccountId, netuid: NetUid, amount_unstaked: AlphaCurrency, limit_price: TaoCurrency, allow_partial: bool] | Pallet: SubtensorModule +) # args: [hotkey: T::AccountId, netuid: NetUid, amount_unstaked: AlphaBalance, limit_price: TaoBalance, allow_partial: bool] | Pallet: SubtensorModule REPORT_EQUIVOCATION = namedtuple( "REPORT_EQUIVOCATION", ["wallet", "pallet", "equivocation_proof", "key_owner_proof"] ) # args: [equivocation_proof: Box>>, key_owner_proof: T::KeyOwnerProof] | Pallet: Grandpa @@ -639,6 +646,9 @@ SET = namedtuple( "SET", ["wallet", "pallet", "now"] ) # args: [now: T::Moment] | Pallet: Timestamp +SET_AUTO_PARENT_DELEGATION_ENABLED = namedtuple( + "SET_AUTO_PARENT_DELEGATION_ENABLED", ["wallet", "pallet", "hotkey", "enabled"] +) # args: [hotkey: T::AccountId, enabled: bool] | Pallet: SubtensorModule SET_BASE_FEE_PER_GAS = namedtuple( "SET_BASE_FEE_PER_GAS", ["wallet", "pallet", "fee"] ) # args: [fee: U256] | Pallet: BaseFee @@ -695,6 +705,12 @@ SET_KEY = namedtuple( "SET_KEY", ["wallet", "pallet", "new"] ) # args: [new: AccountIdLookupOf] | Pallet: Sudo +SET_MAX_EXTRINSIC_WEIGHT = namedtuple( + "SET_MAX_EXTRINSIC_WEIGHT", ["wallet", "pallet", "value"] +) # args: [value: u64] | Pallet: MevShield +SET_MAX_PENDING_EXTRINSICS_NUMBER = namedtuple( + "SET_MAX_PENDING_EXTRINSICS_NUMBER", ["wallet", "pallet", "value"] +) # args: [value: u32] | Pallet: MevShield SET_MAX_SPACE = namedtuple( "SET_MAX_SPACE", ["wallet", "pallet", "new_limit"] ) # args: [new_limit: u32] | Pallet: Commitments @@ -705,9 +721,15 @@ SET_OLDEST_STORED_ROUND = namedtuple( "SET_OLDEST_STORED_ROUND", ["wallet", "pallet", "oldest_round"] ) # args: [oldest_round: u64] | Pallet: Drand +SET_ON_INITIALIZE_WEIGHT = namedtuple( + "SET_ON_INITIALIZE_WEIGHT", ["wallet", "pallet", "value"] +) # args: [value: u64] | Pallet: MevShield SET_PENDING_CHILDKEY_COOLDOWN = namedtuple( "SET_PENDING_CHILDKEY_COOLDOWN", ["wallet", "pallet", "cooldown"] ) # args: [cooldown: u64] | Pallet: SubtensorModule +SET_REAL_PAYS_FEE = namedtuple( + "SET_REAL_PAYS_FEE", ["wallet", "pallet", "delegate", "pays_fee"] +) # args: [delegate: AccountIdLookupOf, pays_fee: bool] | Pallet: Proxy SET_RETRY = namedtuple( "SET_RETRY", ["wallet", "pallet", "task", "retries", "period"] ) # args: [task: TaskAddress>, retries: u8, period: BlockNumberFor] | Pallet: Scheduler @@ -720,6 +742,9 @@ SET_STORAGE = namedtuple( "SET_STORAGE", ["wallet", "pallet", "items"] ) # args: [items: Vec] | Pallet: System +SET_STORED_EXTRINSIC_LIFETIME = namedtuple( + "SET_STORED_EXTRINSIC_LIFETIME", ["wallet", "pallet", "value"] +) # args: [value: u32] | Pallet: MevShield SET_SUBNET_IDENTITY = namedtuple( "SET_SUBNET_IDENTITY", [ @@ -745,9 +770,12 @@ START_CALL = namedtuple( "START_CALL", ["wallet", "pallet", "netuid"] ) # args: [netuid: NetUid] | Pallet: SubtensorModule +STORE_ENCRYPTED = namedtuple( + "STORE_ENCRYPTED", ["wallet", "pallet", "encrypted_call"] +) # args: [encrypted_call: BoundedVec] | Pallet: MevShield SUBMIT_ENCRYPTED = namedtuple( - "SUBMIT_ENCRYPTED", ["wallet", "pallet", "commitment", "ciphertext"] -) # args: [commitment: T::Hash, ciphertext: BoundedVec>] | Pallet: MevShield + "SUBMIT_ENCRYPTED", ["wallet", "pallet", "ciphertext"] +) # args: [ciphertext: BoundedVec>] | Pallet: MevShield SUDO = namedtuple( "SUDO", ["wallet", "pallet", "call"] ) # args: [call: Box<::RuntimeCall>] | Pallet: Sudo @@ -756,13 +784,17 @@ ) # args: [new_authorities: BoundedVec<::AuthorityId, T::MaxAuthorities>] | Pallet: AdminUtils SWAP_COLDKEY = namedtuple( "SWAP_COLDKEY", ["wallet", "pallet", "old_coldkey", "new_coldkey", "swap_cost"] -) # args: [old_coldkey: T::AccountId, new_coldkey: T::AccountId, swap_cost: TaoCurrency] | Pallet: SubtensorModule +) # args: [old_coldkey: T::AccountId, new_coldkey: T::AccountId, swap_cost: TaoBalance] | Pallet: SubtensorModule SWAP_COLDKEY_ANNOUNCED = namedtuple( "SWAP_COLDKEY_ANNOUNCED", ["wallet", "pallet", "new_coldkey"] ) # args: [new_coldkey: T::AccountId] | Pallet: SubtensorModule SWAP_HOTKEY = namedtuple( "SWAP_HOTKEY", ["wallet", "pallet", "hotkey", "new_hotkey", "netuid"] ) # args: [hotkey: T::AccountId, new_hotkey: T::AccountId, netuid: Option] | Pallet: SubtensorModule +SWAP_HOTKEY_V2 = namedtuple( + "SWAP_HOTKEY_V2", + ["wallet", "pallet", "hotkey", "new_hotkey", "netuid", "keep_stake"], +) # args: [hotkey: T::AccountId, new_hotkey: T::AccountId, netuid: Option, keep_stake: bool] | Pallet: SubtensorModule SWAP_STAKE = namedtuple( "SWAP_STAKE", [ @@ -773,7 +805,7 @@ "destination_netuid", "alpha_amount", ], -) # args: [hotkey: T::AccountId, origin_netuid: NetUid, destination_netuid: NetUid, alpha_amount: AlphaCurrency] | Pallet: SubtensorModule +) # args: [hotkey: T::AccountId, origin_netuid: NetUid, destination_netuid: NetUid, alpha_amount: AlphaBalance] | Pallet: SubtensorModule SWAP_STAKE_LIMIT = namedtuple( "SWAP_STAKE_LIMIT", [ @@ -786,7 +818,7 @@ "limit_price", "allow_partial", ], -) # args: [hotkey: T::AccountId, origin_netuid: NetUid, destination_netuid: NetUid, alpha_amount: AlphaCurrency, limit_price: TaoCurrency, allow_partial: bool] | Pallet: SubtensorModule +) # args: [hotkey: T::AccountId, origin_netuid: NetUid, destination_netuid: NetUid, alpha_amount: AlphaBalance, limit_price: TaoBalance, allow_partial: bool] | Pallet: SubtensorModule TERMINATE_LEASE = namedtuple( "TERMINATE_LEASE", ["wallet", "pallet", "lease_id", "hotkey"] ) # args: [lease_id: LeaseId, hotkey: T::AccountId] | Pallet: SubtensorModule @@ -816,7 +848,7 @@ "destination_netuid", "alpha_amount", ], -) # args: [destination_coldkey: T::AccountId, hotkey: T::AccountId, origin_netuid: NetUid, destination_netuid: NetUid, alpha_amount: AlphaCurrency] | Pallet: SubtensorModule +) # args: [destination_coldkey: T::AccountId, hotkey: T::AccountId, origin_netuid: NetUid, destination_netuid: NetUid, alpha_amount: AlphaBalance] | Pallet: SubtensorModule TRY_ASSOCIATE_HOTKEY = namedtuple( "TRY_ASSOCIATE_HOTKEY", ["wallet", "pallet", "hotkey"] ) # args: [hotkey: T::AccountId] | Pallet: SubtensorModule diff --git a/bittensor/extras/dev_framework/calls/pallets.py b/bittensor/extras/dev_framework/calls/pallets.py index 72f0dc092f..1ffef1da5d 100644 --- a/bittensor/extras/dev_framework/calls/pallets.py +++ b/bittensor/extras/dev_framework/calls/pallets.py @@ -1,5 +1,5 @@ """ " -Subtensor spec version: 376 +Subtensor spec version: 397 """ System = "System" diff --git a/bittensor/extras/dev_framework/calls/sudo_calls.py b/bittensor/extras/dev_framework/calls/sudo_calls.py index 7b29a05d99..d7b5a0cdc4 100644 --- a/bittensor/extras/dev_framework/calls/sudo_calls.py +++ b/bittensor/extras/dev_framework/calls/sudo_calls.py @@ -11,7 +11,7 @@ Note: Any manual changes will be overwritten the next time the generator is run. - Subtensor spec version: 376 + Subtensor spec version: 397 """ from collections import namedtuple @@ -53,6 +53,13 @@ SUDO_SET_BONDS_RESET_ENABLED = namedtuple( "SUDO_SET_BONDS_RESET_ENABLED", ["wallet", "pallet", "sudo", "netuid", "enabled"] ) # args: [netuid: NetUid, enabled: bool] | Pallet: AdminUtils +SUDO_SET_BURN_HALF_LIFE = namedtuple( + "SUDO_SET_BURN_HALF_LIFE", ["wallet", "pallet", "sudo", "netuid", "burn_half_life"] +) # args: [netuid: NetUid, burn_half_life: u16] | Pallet: AdminUtils +SUDO_SET_BURN_INCREASE_MULT = namedtuple( + "SUDO_SET_BURN_INCREASE_MULT", + ["wallet", "pallet", "sudo", "netuid", "burn_increase_mult"], +) # args: [netuid: NetUid, burn_increase_mult: U64F64] | Pallet: AdminUtils SUDO_SET_CK_BURN = namedtuple( "SUDO_SET_CK_BURN", ["wallet", "pallet", "sudo", "burn"] ) # args: [burn: u64] | Pallet: AdminUtils @@ -114,7 +121,7 @@ ) # args: [netuid: NetUid, max_allowed_validators: u16] | Pallet: AdminUtils SUDO_SET_MAX_BURN = namedtuple( "SUDO_SET_MAX_BURN", ["wallet", "pallet", "sudo", "netuid", "max_burn"] -) # args: [netuid: NetUid, max_burn: TaoCurrency] | Pallet: AdminUtils +) # args: [netuid: NetUid, max_burn: TaoBalance] | Pallet: AdminUtils SUDO_SET_MAX_CHILDKEY_TAKE = namedtuple( "SUDO_SET_MAX_CHILDKEY_TAKE", ["wallet", "pallet", "sudo", "take"] ) # args: [take: u16] | Pallet: SubtensorModule @@ -146,7 +153,7 @@ ) # args: [netuid: NetUid, min_allowed_weights: u16] | Pallet: AdminUtils SUDO_SET_MIN_BURN = namedtuple( "SUDO_SET_MIN_BURN", ["wallet", "pallet", "sudo", "netuid", "min_burn"] -) # args: [netuid: NetUid, min_burn: TaoCurrency] | Pallet: AdminUtils +) # args: [netuid: NetUid, min_burn: TaoBalance] | Pallet: AdminUtils SUDO_SET_MIN_CHILDKEY_TAKE = namedtuple( "SUDO_SET_MIN_CHILDKEY_TAKE", ["wallet", "pallet", "sudo", "take"] ) # args: [take: u16] | Pallet: SubtensorModule @@ -164,7 +171,7 @@ ) # args: [immunity_period: u64] | Pallet: AdminUtils SUDO_SET_NETWORK_MIN_LOCK_COST = namedtuple( "SUDO_SET_NETWORK_MIN_LOCK_COST", ["wallet", "pallet", "sudo", "lock_cost"] -) # args: [lock_cost: TaoCurrency] | Pallet: AdminUtils +) # args: [lock_cost: TaoBalance] | Pallet: AdminUtils SUDO_SET_NETWORK_POW_REGISTRATION_ALLOWED = namedtuple( "SUDO_SET_NETWORK_POW_REGISTRATION_ALLOWED", ["wallet", "pallet", "sudo", "netuid", "registration_allowed"], @@ -191,7 +198,7 @@ ) # args: [netuid: NetUid, immune_neurons: u16] | Pallet: AdminUtils SUDO_SET_RAO_RECYCLED = namedtuple( "SUDO_SET_RAO_RECYCLED", ["wallet", "pallet", "sudo", "netuid", "rao_recycled"] -) # args: [netuid: NetUid, rao_recycled: TaoCurrency] | Pallet: AdminUtils +) # args: [netuid: NetUid, rao_recycled: TaoBalance] | Pallet: AdminUtils SUDO_SET_RECYCLE_OR_BURN = namedtuple( "SUDO_SET_RECYCLE_OR_BURN", ["wallet", "pallet", "sudo", "netuid", "recycle_or_burn"], @@ -253,7 +260,7 @@ ) # args: [netuid: NetUid, toggle: bool] | Pallet: AdminUtils SUDO_SET_TOTAL_ISSUANCE = namedtuple( "SUDO_SET_TOTAL_ISSUANCE", ["wallet", "pallet", "sudo", "total_issuance"] -) # args: [total_issuance: TaoCurrency] | Pallet: AdminUtils +) # args: [total_issuance: TaoBalance] | Pallet: AdminUtils SUDO_SET_TX_CHILDKEY_TAKE_RATE_LIMIT = namedtuple( "SUDO_SET_TX_CHILDKEY_TAKE_RATE_LIMIT", ["wallet", "pallet", "sudo", "tx_rate_limit"], From e389dd9b008e787b2b4f6e68d16817be282c07e3 Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Tue, 21 Apr 2026 09:21:49 -0700 Subject: [PATCH 101/118] CHANGELOG.md --- CHANGELOG.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad73bede2c..663f9f7ba3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,29 @@ # Changelog +## 10.3.0 /2026-0-21 + +## What's Changed +* Fix logging.info and state transitions in LoggingMachine by @ionodeionode in https://github.com/latent-to/bittensor/pull/3270 +* Feat/coldkey swap clear by @ibraheem-abe in https://github.com/latent-to/bittensor/pull/3296 +* Removes munch by @thewhaleking in https://github.com/latent-to/bittensor/pull/3298 +* Ensures we don't accidentally install ASI 2.0 on this by @thewhaleking in https://github.com/latent-to/bittensor/pull/3302 +* There is not Balances.transfer by @thewhaleking in https://github.com/latent-to/bittensor/pull/3300 +* Bumps all workflows versions by @thewhaleking in https://github.com/latent-to/bittensor/pull/3301 +* Fix: Updates exception check for Commitment Pallet extension by @ibraheem-abe in https://github.com/latent-to/bittensor/pull/3303 +* Fix and extend incentive e2e test by @basfroman in https://github.com/latent-to/bittensor/pull/3304 +* Fix/e2e tests for stake lock owner alpha by @ibraheem-abe in https://github.com/latent-to/bittensor/pull/3305 +* Update/staking hotkeys limitation for coldkey swap by @ibraheem-abe in https://github.com/latent-to/bittensor/pull/3306 +* Update: Log SHA & update perms of workflow by @ibraheem-abe in https://github.com/latent-to/bittensor/pull/3307 +* Fix e2e tests (metagraph_info) by @basfroman in https://github.com/latent-to/bittensor/pull/3310 +* Removes flake8, uses ruff for linter check by @thewhaleking in https://github.com/latent-to/bittensor/pull/3313 +* Adds note to httpx in dev reqs by @thewhaleking in https://github.com/latent-to/bittensor/pull/3315 +* Add `register_limit` extrinsic by @basfroman in https://github.com/latent-to/bittensor/pull/3316 + +## New Contributors +* @ionodeionode made their first contribution in https://github.com/latent-to/bittensor/pull/3270 + +**Full Changelog**: https://github.com/latent-to/bittensor/compare/v10.2.0...v10.3.0 + ## 10.2.0 /2026-03-19 ## What's Changed From d629338d13e0d0030ac1343729bacf0d183184ed Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Tue, 21 Apr 2026 09:22:49 -0700 Subject: [PATCH 102/118] bumping versions --- pyproject.toml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2f6d7d60ed..e1afee339b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "bittensor" -version = "10.2.0" +version = "10.3.0" description = "Bittensor SDK" readme = "README.md" authors = [ @@ -33,8 +33,7 @@ dependencies = [ "uvicorn", "bittensor-drand>=1.3.0,<2.0.0", "bittensor-wallet==4.0.1", - # TODO this will need to be changed to asi 2.0-3.0 when merged - "async-substrate-interface @ git+https://github.com/opentensor/async-substrate-interface.git@release/2.0.0", + "async-substrate-interface>=2.0.0", ] [project.optional-dependencies] @@ -63,7 +62,7 @@ torch = [ "torch>=1.13.1,<3.0" ] cli = [ - "bittensor-cli>=9.16.0" + "bittensor-cli>=9.21.0" ] From 9725e9e16224930b549e65194cbd1f5af24917b4 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Tue, 21 Apr 2026 19:02:19 +0200 Subject: [PATCH 103/118] Bump ASI --- pyproject.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2f6d7d60ed..af9273619e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,8 +33,7 @@ dependencies = [ "uvicorn", "bittensor-drand>=1.3.0,<2.0.0", "bittensor-wallet==4.0.1", - # TODO this will need to be changed to asi 2.0-3.0 when merged - "async-substrate-interface @ git+https://github.com/opentensor/async-substrate-interface.git@release/2.0.0", + "async-substrate-interface==2.0.0", ] [project.optional-dependencies] From 59b55e63defffbf5ea743f1214c44f907709380c Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Tue, 21 Apr 2026 10:26:43 -0700 Subject: [PATCH 104/118] conflict --- pyproject.toml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 66d43a7cf1..e1afee339b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,11 +33,7 @@ dependencies = [ "uvicorn", "bittensor-drand>=1.3.0,<2.0.0", "bittensor-wallet==4.0.1", -<<<<<<< changelog/10.3.0 "async-substrate-interface>=2.0.0", -======= - "async-substrate-interface==2.0.0", ->>>>>>> staging ] [project.optional-dependencies] From 918c00c0c16561ce4ffc0326a767f9f8a43938d5 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Thu, 23 Apr 2026 21:31:10 +0200 Subject: [PATCH 105/118] Bump ASI req --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index af9273619e..f3b43e5718 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,7 @@ dependencies = [ "uvicorn", "bittensor-drand>=1.3.0,<2.0.0", "bittensor-wallet==4.0.1", - "async-substrate-interface==2.0.0", + "async-substrate-interface==2.0.1", ] [project.optional-dependencies] From 10fe76f75aa544a743a3967d74ff83bf69722694 Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Thu, 23 Apr 2026 15:56:06 -0700 Subject: [PATCH 106/118] get rid of deprecated logic --- bittensor/utils/registration/__init__.py | 8 +- bittensor/utils/registration/async_pow.py | 538 -------- bittensor/utils/registration/pow.py | 1181 ----------------- bittensor/utils/registration/register_cuda.py | 123 -- 4 files changed, 1 insertion(+), 1849 deletions(-) delete mode 100644 bittensor/utils/registration/async_pow.py delete mode 100644 bittensor/utils/registration/pow.py delete mode 100644 bittensor/utils/registration/register_cuda.py diff --git a/bittensor/utils/registration/__init__.py b/bittensor/utils/registration/__init__.py index ddcc7a1248..3ac18def71 100644 --- a/bittensor/utils/registration/__init__.py +++ b/bittensor/utils/registration/__init__.py @@ -1,21 +1,15 @@ -from bittensor.utils.registration.pow import ( - create_pow, +from bittensor.utils.registration.torch_utils import ( legacy_torch_api_compat, log_no_torch_error, torch, use_torch, LazyLoadedTorch, - POWSolution, ) -from bittensor.utils.registration.async_pow import create_pow_async __all__ = [ - "create_pow", - "create_pow_async", "legacy_torch_api_compat", "log_no_torch_error", "torch", "use_torch", "LazyLoadedTorch", - "POWSolution", ] diff --git a/bittensor/utils/registration/async_pow.py b/bittensor/utils/registration/async_pow.py deleted file mode 100644 index aa45cf502d..0000000000 --- a/bittensor/utils/registration/async_pow.py +++ /dev/null @@ -1,538 +0,0 @@ -"""This module provides async utilities for solving Proof-of-Work (PoW) challenges in Bittensor network.""" - -import math -import time -from multiprocessing import Event, Lock, Array, Value, Queue -from queue import Empty -from typing import Callable, Union, Optional, TYPE_CHECKING - -from bittensor.core.errors import SubstrateRequestException -from bittensor.utils.btlogging import logging -from bittensor.utils.registration.pow import ( - get_cpu_count, - update_curr_block, - terminate_workers_and_wait_for_exit, - CUDASolver, - torch, - RegistrationStatistics, - RegistrationStatisticsLogger, - Solver, - UsingSpawnStartMethod, -) - -if TYPE_CHECKING: - from bittensor.core.async_subtensor import AsyncSubtensor - from bittensor_wallet import Wallet - from bittensor.utils.registration import POWSolution - - -async def _get_block_with_retry( - subtensor: "AsyncSubtensor", netuid: int -) -> tuple[int, int, str]: - """ - Gets the current block number, difficulty, and block hash from the substrate node. - - Parameters: - subtensor: The subtensor object to use to get the block number, difficulty, and block hash. - netuid: The netuid of the network to get the block number, difficulty, and block hash from. - - Returns: - The current block number, difficulty of the subnet, block hash - - Raises: - Exception: If the block hash is None. - ValueError: If the difficulty is None. - """ - block = await subtensor.substrate.get_block() - block_hash = block["header"]["hash"] - block_number = block["header"]["number"] - try: - difficulty = ( - 1_000_000 - if netuid == -1 - else int( - await subtensor.get_hyperparameter( - param_name="Difficulty", netuid=netuid, block_hash=block_hash - ) - ) - ) - except TypeError: - raise ValueError("Chain error. Difficulty is None") - except SubstrateRequestException: - raise Exception( - "Network error. Could not connect to substrate to get block hash" - ) - return block_number, difficulty, block_hash - - -async def _check_for_newest_block_and_update( - subtensor: "AsyncSubtensor", - netuid: int, - old_block_number: int, - hotkey_bytes: bytes, - curr_diff: Array, - curr_block: Array, - curr_block_num: Value, - update_curr_block_: "Callable", - check_block: Lock, - solvers: list[Solver], - curr_stats: "RegistrationStatistics", -) -> int: - """ - Check for the newest block and update block-related information and states across solvers if a new block is detected. - - Parameters: - subtensor: The subtensor instance interface. - netuid: The network UID for the blockchain. - old_block_number: The previously known block number. - hotkey_bytes: The bytes representation of the hotkey. - curr_diff: The current difficulty level. - curr_block: The current block information. - curr_block_num: The current block number. - update_curr_block_: Function to update current block information. - check_block: Lock object for synchronizing block checking. - solvers: List of solvers to notify of new blocks. - curr_stats: Current registration statistics to update. - - Returns: - int: The updated block number which is the same as the new block - number if it was detected, otherwise the old block number. - """ - block_number = await subtensor.substrate.get_block_number(None) - if block_number != old_block_number: - old_block_number = block_number - # update block information - block_number, difficulty, block_hash = await _get_block_with_retry( - subtensor=subtensor, netuid=netuid - ) - block_bytes = bytes.fromhex(block_hash[2:]) - - update_curr_block_( - curr_diff, - curr_block, - curr_block_num, - block_number, - block_bytes, - difficulty, - hotkey_bytes, - check_block, - ) - # Set new block events for each solver - - for worker in solvers: - worker.newBlockEvent.set() - - # update stats - curr_stats.block_number = block_number - curr_stats.block_hash = block_hash - curr_stats.difficulty = difficulty - - return old_block_number - - -async def _block_solver( - subtensor: "AsyncSubtensor", - wallet: "Wallet", - num_processes: int, - netuid: int, - dev_id: list[int], - tpb: int, - update_interval: int, - curr_block, - curr_block_num, - curr_diff, - n_samples, - alpha_, - output_in_place, - log_verbose, - cuda: bool, -): - """Shared code used by the Solvers to solve the POW solution.""" - limit = int(math.pow(2, 256)) - 1 - - if cuda: - num_processes = len(dev_id) - - # Establish communication queues - # See the _Solver class for more information on the queues. - stop_event = Event() - stop_event.clear() - - solution_queue = Queue() - finished_queues = [Queue() for _ in range(num_processes)] - check_block = Lock() - - hotkey_bytes = ( - wallet.coldkeypub.public_key if netuid == -1 else wallet.hotkey.public_key - ) - - if cuda: - # Create a worker per CUDA device - solvers = [ - CUDASolver( - i, - num_processes, - update_interval, - finished_queues[i], - solution_queue, - stop_event, - curr_block, - curr_block_num, - curr_diff, - check_block, - limit, - dev_id[i], - tpb, - ) - for i in range(num_processes) - ] - else: - # Start consumers - solvers = [ - Solver( - i, - num_processes, - update_interval, - finished_queues[i], - solution_queue, - stop_event, - curr_block, - curr_block_num, - curr_diff, - check_block, - limit, - ) - for i in range(num_processes) - ] - - # Get first block - block_number, difficulty, block_hash = await _get_block_with_retry( - subtensor=subtensor, netuid=netuid - ) - - block_bytes = bytes.fromhex(block_hash[2:]) - old_block_number = block_number - # Set to current block - update_curr_block( - curr_diff, - curr_block, - curr_block_num, - block_number, - block_bytes, - difficulty, - hotkey_bytes, - check_block, - ) - - # Set new block events for each solver to start at the initial block - for worker in solvers: - worker.newBlockEvent.set() - - for worker in solvers: - worker.start() # start the solver processes - - start_time = time.time() # time that the registration started - time_last = start_time # time that the last work blocks completed - - curr_stats = RegistrationStatistics( - time_spent_total=0.0, - time_average=0.0, - rounds_total=0, - time_spent=0.0, - hash_rate_perpetual=0.0, - hash_rate=0.0, - difficulty=difficulty, - block_number=block_number, - block_hash=block_hash, - ) - - start_time_perpetual = time.time() - - logger = RegistrationStatisticsLogger(output_in_place=output_in_place) - logger.start() - - solution = None - - hash_rates = [0] * n_samples # The last n true hash_rates - weights = [alpha_**i for i in range(n_samples)] # weights decay by alpha - - timeout = 0.15 if cuda else 0.15 - while netuid == -1 or not await subtensor.is_hotkey_registered( - wallet.hotkey.ss58_address, netuid - ): - # Wait until a solver finds a solution - try: - solution = solution_queue.get(block=True, timeout=timeout) - if solution is not None: - break - except Empty: - # No solution found, try again - pass - - # check for new block - old_block_number = await _check_for_newest_block_and_update( - subtensor=subtensor, - netuid=netuid, - hotkey_bytes=hotkey_bytes, - old_block_number=old_block_number, - curr_diff=curr_diff, - curr_block=curr_block, - curr_block_num=curr_block_num, - curr_stats=curr_stats, - update_curr_block_=update_curr_block, - check_block=check_block, - solvers=solvers, - ) - - num_time = 0 - for finished_queue in finished_queues: - try: - finished_queue.get(timeout=0.1) - num_time += 1 - - except Empty: - continue - - time_now = time.time() # get current time - time_since_last = time_now - time_last # get time since last work block(s) - if num_time > 0 and time_since_last > 0.0: - # create EWMA of the hash_rate to make measure more robust - - if cuda: - hash_rate_ = (num_time * tpb * update_interval) / time_since_last - else: - hash_rate_ = (num_time * update_interval) / time_since_last - hash_rates.append(hash_rate_) - hash_rates.pop(0) # remove the 0th data point - curr_stats.hash_rate = sum( - [hash_rates[i] * weights[i] for i in range(n_samples)] - ) / (sum(weights)) - - # update time last to now - time_last = time_now - - curr_stats.time_average = ( - curr_stats.time_average * curr_stats.rounds_total - + curr_stats.time_spent - ) / (curr_stats.rounds_total + num_time) - curr_stats.rounds_total += num_time - - # Update stats - curr_stats.time_spent = time_since_last - new_time_spent_total = time_now - start_time_perpetual - if cuda: - curr_stats.hash_rate_perpetual = ( - curr_stats.rounds_total * (tpb * update_interval) - ) / new_time_spent_total - else: - curr_stats.hash_rate_perpetual = ( - curr_stats.rounds_total * update_interval - ) / new_time_spent_total - curr_stats.time_spent_total = new_time_spent_total - - # Update the logger - logger.update(curr_stats, verbose=log_verbose) - - # exited while, solution contains the nonce or wallet is registered - stop_event.set() # stop all other processes - logger.stop() - - # terminate and wait for all solvers to exit - terminate_workers_and_wait_for_exit(solvers) - - return solution - - -async def _solve_for_difficulty_fast_cuda( - subtensor: "AsyncSubtensor", - wallet: "Wallet", - netuid: int, - output_in_place: bool = True, - update_interval: int = 50_000, - tpb: int = 512, - dev_id: Union[list[int], int] = 0, - n_samples: int = 10, - alpha_: float = 0.80, - log_verbose: bool = False, -) -> Optional["POWSolution"]: - """ - Solves the registration fast using CUDA - - Parameters: - subtensor: The subtensor object to use to get the block number, difficulty, and block hash. - wallet: The wallet to register - netuid: The netuid of the subnet to register to. - output_in_place: If true, prints the output in place, otherwise prints to new lines - update_interval: The number of nonces to try before checking for more blocks - tpb: The number of threads per block. CUDA param that should match the GPU capability - dev_id: The CUDA device IDs to execute the registration on, either a single device or a list of devices - n_samples: The number of samples of the hash_rate to keep for the EWMA - alpha_: The alpha for the EWMA for the hash_rate calculation - log_verbose: If true, prints more verbose logging of the registration metrics. - - Note: - The hash rate is calculated as an exponentially weighted moving average in order to make the measure more robust. - """ - if isinstance(dev_id, int): - dev_id = [dev_id] - elif dev_id is None: - dev_id = [0] - - num_processes = min(1, get_cpu_count()) - - if update_interval is None: - update_interval = 50_000 - - if not torch.cuda.is_available(): - raise Exception("CUDA not available") - - # Set mp start to use spawn so CUDA doesn't complain - with UsingSpawnStartMethod(force=True): - curr_block, curr_block_num, curr_diff = CUDASolver.create_shared_memory() - - solution = await _block_solver( - subtensor=subtensor, - wallet=wallet, - num_processes=num_processes, - netuid=netuid, - dev_id=dev_id, - tpb=tpb, - update_interval=update_interval, - curr_block=curr_block, - curr_block_num=curr_block_num, - curr_diff=curr_diff, - n_samples=n_samples, - alpha_=alpha_, - output_in_place=output_in_place, - log_verbose=log_verbose, - cuda=True, - ) - - return solution - - -async def _solve_for_difficulty_fast( - subtensor: "AsyncSubtensor", - wallet: "Wallet", - netuid: int, - output_in_place: bool = True, - num_processes: Optional[int] = None, - update_interval: Optional[int] = None, - n_samples: int = 10, - alpha_: float = 0.80, - log_verbose: bool = False, -) -> Optional["POWSolution"]: - """ - Solves the POW for registration using multiprocessing. - - Parameters: - subtensor: The subtensor object to use to get the block number, difficulty, and block hash. - wallet: wallet to use for registration. - netuid: The netuid of the subnet to register to. - output_in_place: If true, prints the status in place. Otherwise, prints the status on a new line. - num_processes: Number of processes to use. - update_interval: Number of nonces to solve before updating block information. - n_samples: The number of samples of the hash_rate to keep for the EWMA - alpha_: The alpha for the EWMA for the hash_rate calculation - log_verbose: If true, prints more verbose logging of the registration metrics. - - Notes: - The hash rate is calculated as an exponentially weighted moving average in order to make the measure more robust. - We can also modify the update interval to do smaller blocks of work, while still updating the block information - after a different number of nonces, to increase the transparency of the process while still keeping the speed. - """ - if not num_processes: - # get the number of allowed processes for this process - num_processes = min(1, get_cpu_count()) - - if update_interval is None: - update_interval = 50_000 - - curr_block, curr_block_num, curr_diff = Solver.create_shared_memory() - - solution = await _block_solver( - subtensor=subtensor, - wallet=wallet, - num_processes=num_processes, - netuid=netuid, - dev_id=None, - tpb=None, - update_interval=update_interval, - curr_block=curr_block, - curr_block_num=curr_block_num, - curr_diff=curr_diff, - n_samples=n_samples, - alpha_=alpha_, - output_in_place=output_in_place, - log_verbose=log_verbose, - cuda=False, - ) - - return solution - - -async def create_pow_async( - subtensor: "AsyncSubtensor", - wallet: "Wallet", - netuid: int, - output_in_place: bool = True, - cuda: bool = False, - dev_id: Union[list[int], int] = 0, - tpb: int = 256, - num_processes: int = None, - update_interval: int = None, - log_verbose: bool = False, -) -> "POWSolution": - """ - Creates a proof of work for the given subtensor and wallet. - - Parameters: - subtensor: The subtensor instance. - wallet: The wallet to create a proof of work for. - netuid: The netuid for the subnet to create a proof of work for. - output_in_place: If true, prints the progress of the proof of work to the console in-place. Meaning the progress - is printed on the same lines. - cuda: If true, uses CUDA to solve the proof of work. - dev_id: The CUDA device id(s) to use. If cuda is true and dev_id is a list, then multiple CUDA devices will be - used to solve the proof of work. - tpb: The number of threads per block to use when solving the proof of work. Should be a multiple of 32. - num_processes: The number of processes to use when solving the proof of work. If None, then the number of - processes is equal to the number of CPU cores. - update_interval: The number of nonces to run before checking for a new block. - log_verbose: If true, prints the progress of the proof of work more verbosely. - - Returns: - The proof of work solution or None if the wallet is already registered or there is a different error. - - Raises: - ValueError: If the subnet does not exist. - """ - if netuid != -1: - if not await subtensor.subnet_exists(netuid=netuid): - raise ValueError(f"Subnet {netuid} does not exist") - solution: Optional[POWSolution] - if cuda: - logging.debug("Solve difficulty with CUDA.") - solution = await _solve_for_difficulty_fast_cuda( - subtensor=subtensor, - wallet=wallet, - netuid=netuid, - output_in_place=output_in_place, - dev_id=dev_id, - tpb=tpb, - update_interval=update_interval, - log_verbose=log_verbose, - ) - else: - logging.debug("Solve difficulty.") - solution = await _solve_for_difficulty_fast( - subtensor=subtensor, - wallet=wallet, - netuid=netuid, - output_in_place=output_in_place, - num_processes=num_processes, - update_interval=update_interval, - log_verbose=log_verbose, - ) - - return solution diff --git a/bittensor/utils/registration/pow.py b/bittensor/utils/registration/pow.py deleted file mode 100644 index 1fa4c23ee8..0000000000 --- a/bittensor/utils/registration/pow.py +++ /dev/null @@ -1,1181 +0,0 @@ -"""This module provides utilities for solving Proof-of-Work (PoW) challenges in Bittensor network.""" - -import binascii -import functools -import hashlib -import math -import multiprocessing as mp -import os -import random -import subprocess -import time -from dataclasses import dataclass -from datetime import timedelta -from multiprocessing.queues import Queue as QueueType -from queue import Empty, Full -from typing import Callable, Optional, Union, TYPE_CHECKING - -import numpy -from Crypto.Hash import keccak - -from bittensor.utils.btlogging import logging -from bittensor.utils.formatting import get_human_readable, millify -from bittensor.utils.registration.register_cuda import solve_cuda - - -def use_torch() -> bool: - """Force the use of torch over numpy for certain operations.""" - return True if os.getenv("USE_TORCH") == "1" else False - - -def legacy_torch_api_compat(func): - """ - Convert function operating on numpy Input&Output to legacy torch Input&Output API if `use_torch()` is True. - - Parameters: - func: Function with numpy Input/Output to be decorated. - - Returns: - decorated: Decorated function. - """ - - @functools.wraps(func) - def decorated(*args, **kwargs): - if use_torch(): - # if argument is a Torch tensor, convert it to numpy - args = [ - arg.cpu().numpy() if isinstance(arg, torch.Tensor) else arg - for arg in args - ] - kwargs = { - key: value.cpu().numpy() if isinstance(value, torch.Tensor) else value - for key, value in kwargs.items() - } - ret = func(*args, **kwargs) - if use_torch(): - # if return value is a numpy array, convert it to Torch tensor - if isinstance(ret, numpy.ndarray): - ret = torch.from_numpy(ret) - return ret - - return decorated - - -@functools.cache -def _get_real_torch(): - try: - import torch as _real_torch - except ImportError: - _real_torch = None - return _real_torch - - -def log_no_torch_error(): - logging.error( - "This command requires torch. You can install torch for bittensor" - ' with `pip install bittensor[torch]` or `pip install ".[torch]"`' - " if installing from source, and then run the command with USE_TORCH=1 {command}" - ) - - -class LazyLoadedTorch: - """A lazy-loading proxy for the torch module.""" - - def __bool__(self): - return bool(_get_real_torch()) - - def __getattr__(self, name): - if real_torch := _get_real_torch(): - return getattr(real_torch, name) - else: - log_no_torch_error() - raise ImportError("torch not installed") - - -if TYPE_CHECKING: - import torch - from bittensor.core.subtensor import Subtensor - from bittensor.core.async_subtensor import AsyncSubtensor - from bittensor_wallet import Wallet -else: - torch = LazyLoadedTorch() - - -def _hex_bytes_to_u8_list(hex_bytes: bytes) -> list[int]: - """ """ - return [int(hex_bytes[i : i + 2], 16) for i in range(0, len(hex_bytes), 2)] - - -def _create_seal_hash(block_and_hotkey_hash_bytes: bytes, nonce: int) -> bytes: - """ - Create a cryptographic seal hash from the given block and hotkey hash bytes and nonce. - - This function generates a seal hash by combining the given block and hotkey hash bytes with a nonce. - It first converts the nonce to a byte representation, then concatenates it with the first 64 hex characters of the - block and hotkey hash bytes. The result is then hashed using SHA-256 followed by the Keccak-256 algorithm to produce - the final seal hash. - - Parameters: - block_and_hotkey_hash_bytes: The combined hash bytes of the block and hotkey. - nonce: The nonce value used for hashing. - - Returns: - The resulting seal hash. - """ - nonce_bytes = binascii.hexlify(nonce.to_bytes(8, "little")) - pre_seal = nonce_bytes + binascii.hexlify(block_and_hotkey_hash_bytes)[:64] - seal_sh256 = hashlib.sha256(bytearray(_hex_bytes_to_u8_list(pre_seal))).digest() - kec = keccak.new(digest_bits=256) - seal = kec.update(seal_sh256).digest() - return seal - - -def _seal_meets_difficulty(seal: bytes, difficulty: int, limit: int) -> bool: - """Determines if a seal meets the specified difficulty.""" - seal_number = int.from_bytes(seal, "big") - product = seal_number * difficulty - return product < limit - - -@dataclass -class POWSolution: - """A solution to the registration PoW problem.""" - - nonce: int - block_number: int - difficulty: int - seal: bytes - - def is_stale(self, subtensor: "Subtensor") -> bool: - """ - Synchronous implementation. Returns True if the POW is stale. - - This means the block the POW is solved for is within 3 blocks of the current block. - """ - return self.block_number < subtensor.get_current_block() - 3 - - async def is_stale_async(self, subtensor: "AsyncSubtensor") -> bool: - """ - Asynchronous implementation. Returns True if the POW is stale. - - This means the block the POW is solved for is within 3 blocks of the current block. - """ - current_block = await subtensor.substrate.get_block_number(None) - return self.block_number < current_block - 3 - - -class UsingSpawnStartMethod: - def __init__(self, force: bool = False): - self._old_start_method = None - self._force = force - - def __enter__(self): - self._old_start_method = mp.get_start_method(allow_none=True) - if self._old_start_method is None: - self._old_start_method = "spawn" # default to spawn - - mp.set_start_method("spawn", force=self._force) - - def __exit__(self, *args): - # restore the old start method - mp.set_start_method(self._old_start_method, force=True) - - -class _SolverBase(mp.Process): - """ - A process that solves the registration PoW problem. - - Parameters: - proc_num: The number of the process being created. - num_proc: The total number of processes running. - update_interval: The number of nonces to try to solve before checking for a new block. - finished_queue: The queue to put the process number when a process finishes each update_interval. Used for - calculating the average time per update_interval across all processes. - solution_queue: The queue to put the solution the process has found during the pow solve. - stopEvent: The event to set by the main process when all the solver processes should stop. The solver process - will check for the event after each update_interval. The solver process will stop when the event is set. - Used to stop the solver processes when a solution is found. - curr_block: The array containing this process's current block hash. The main process will set the array to the - new block hash when a new block is finalized in the network. The solver process will get the new block hash - from this array when newBlockEvent is set. - curr_block_num: The value containing this process's current block number. The main process will set the value to - the new block number when a new block is finalized in the network. The solver process will get the new block - number from this value when newBlockEvent is set. - curr_diff: The array containing this process's current difficulty. The main process will set the array to the - new difficulty when a new block is finalized in the network. The solver process will get the new difficulty - from this array when newBlockEvent is set. - check_block: The lock to prevent this process from getting the new block data while the main process is updating - the data. - limit: The limit of the pow solve for a valid solution. - """ - - proc_num: int - num_proc: int - update_interval: int - finished_queue: "mp.Queue" - solution_queue: "mp.Queue" - # newBlockEvent: "mp.Event" - newBlockEvent: "mp.Event" - stopEvent: "mp.Event" - hotkey_bytes: bytes - curr_block: "mp.Array" - curr_block_num: "mp.Value" - curr_diff: "mp.Array" - check_block: "mp.Lock" - limit: int - - def __init__( - self, - proc_num, - num_proc, - update_interval, - finished_queue, - solution_queue, - stopEvent, - curr_block, - curr_block_num, - curr_diff, - check_block, - limit, - ): - mp.Process.__init__(self, daemon=True) - self.proc_num = proc_num - self.num_proc = num_proc - self.update_interval = update_interval - self.finished_queue = finished_queue - self.solution_queue = solution_queue - self.newBlockEvent = mp.Event() - self.newBlockEvent.clear() - self.curr_block = curr_block - self.curr_block_num = curr_block_num - self.curr_diff = curr_diff - self.check_block = check_block - self.stopEvent = stopEvent - self.limit = limit - - def run(self): - raise NotImplementedError("_SolverBase is an abstract class") - - @staticmethod - def create_shared_memory() -> tuple["mp.Array", "mp.Value", "mp.Array"]: - """Creates shared memory for the solver processes to use.""" - curr_block = mp.Array("h", 32, lock=True) # byte array - curr_block_num = mp.Value("i", 0, lock=True) # int - curr_diff = mp.Array("Q", [0, 0], lock=True) # [high, low] - - return curr_block, curr_block_num, curr_diff - - -class Solver(_SolverBase): - def run(self): - block_number: int - block_and_hotkey_hash_bytes: bytes - block_difficulty: int - nonce_limit = int(math.pow(2, 64)) - 1 - - # Start at random nonce - nonce_start = random.randint(0, nonce_limit) - nonce_end = nonce_start + self.update_interval - while not self.stopEvent.is_set(): - if self.newBlockEvent.is_set(): - with self.check_block: - block_number = self.curr_block_num.value - block_and_hotkey_hash_bytes = bytes(self.curr_block) - block_difficulty = _registration_diff_unpack(self.curr_diff) - - self.newBlockEvent.clear() - - # Do a block of nonces - solution = _solve_for_nonce_block( - nonce_start, - nonce_end, - block_and_hotkey_hash_bytes, - block_difficulty, - self.limit, - block_number, - ) - if solution is not None: - self.solution_queue.put(solution) - - try: - # Send time - self.finished_queue.put_nowait(self.proc_num) - except Full: - pass - - nonce_start = random.randint(0, nonce_limit) - nonce_start = nonce_start % nonce_limit - nonce_end = nonce_start + self.update_interval - - -class CUDASolver(_SolverBase): - dev_id: int - tpb: int - - def __init__( - self, - proc_num, - num_proc, - update_interval, - finished_queue, - solution_queue, - stopEvent, - curr_block, - curr_block_num, - curr_diff, - check_block, - limit, - dev_id: int, - tpb: int, - ): - super().__init__( - proc_num, - num_proc, - update_interval, - finished_queue, - solution_queue, - stopEvent, - curr_block, - curr_block_num, - curr_diff, - check_block, - limit, - ) - self.dev_id = dev_id - self.tpb = tpb - - def run(self): - block_number: int = 0 # dummy value - block_and_hotkey_hash_bytes: bytes = b"0" * 32 # dummy value - block_difficulty: int = int(math.pow(2, 64)) - 1 # dummy value - nonce_limit = int(math.pow(2, 64)) - 1 # U64MAX - - # Start at random nonce - nonce_start = random.randint(0, nonce_limit) - while not self.stopEvent.is_set(): - if self.newBlockEvent.is_set(): - with self.check_block: - block_number = self.curr_block_num.value - block_and_hotkey_hash_bytes = bytes(self.curr_block) - block_difficulty = _registration_diff_unpack(self.curr_diff) - - self.newBlockEvent.clear() - - # Do a block of nonces - solution = _solve_for_nonce_block_cuda( - nonce_start, - self.update_interval, - block_and_hotkey_hash_bytes, - block_difficulty, - self.limit, - block_number, - self.dev_id, - self.tpb, - ) - if solution is not None: - self.solution_queue.put(solution) - - try: - # Signal that a nonce_block was finished using queue - # send our proc_num - self.finished_queue.put(self.proc_num) - except Full: - pass - - # increase nonce by number of nonces processed - nonce_start += self.update_interval * self.tpb - nonce_start = nonce_start % nonce_limit - - -def _solve_for_nonce_block_cuda( - nonce_start: int, - update_interval: int, - block_and_hotkey_hash_bytes: bytes, - difficulty: int, - limit: int, - block_number: int, - dev_id: int, - tpb: int, -) -> Optional["POWSolution"]: - """Tries to solve the POW on a CUDA device for a block of nonces (nonce_start, nonce_start + update_interval * tpb""" - solution, seal = solve_cuda( - nonce_start, - update_interval, - tpb, - block_and_hotkey_hash_bytes, - difficulty, - limit, - dev_id, - ) - - if solution != -1: - # Check if solution is valid (i.e., not -1) - return POWSolution(solution, block_number, difficulty, seal) - - return None - - -def _solve_for_nonce_block( - nonce_start: int, - nonce_end: int, - block_and_hotkey_hash_bytes: bytes, - difficulty: int, - limit: int, - block_number: int, -) -> Optional["POWSolution"]: - """Tries to solve the POW for a block of nonces (nonce_start, nonce_end)""" - for nonce in range(nonce_start, nonce_end): - # Create seal. - seal = _create_seal_hash(block_and_hotkey_hash_bytes, nonce) - - # Check if seal meets difficulty - if _seal_meets_difficulty(seal, difficulty, limit): - # Found a solution, save it. - return POWSolution(nonce, block_number, difficulty, seal) - - return None - - -def _registration_diff_unpack(packed_diff: "mp.Array") -> int: - """Unpacks the packed two 32-bit integers into one 64-bit integer. Little endian.""" - return int(packed_diff[0] << 32 | packed_diff[1]) - - -def _registration_diff_pack(diff: int, packed_diff: "mp.Array"): - """Packs the difficulty into two 32-bit integers. Little endian.""" - packed_diff[0] = diff >> 32 - packed_diff[1] = diff & 0xFFFFFFFF # low 32 bits - - -def _hash_block_with_hotkey(block_bytes: bytes, hotkey_bytes: bytes) -> bytes: - """Hashes the block with the hotkey using Keccak-256 to get 32 bytes""" - kec = keccak.new(digest_bits=256) - kec = kec.update(bytearray(block_bytes + hotkey_bytes)) - block_and_hotkey_hash_bytes = kec.digest() - return block_and_hotkey_hash_bytes - - -def update_curr_block( - curr_diff: "mp.Array", - curr_block: "mp.Array", - curr_block_num: "mp.Value", - block_number: int, - block_bytes: bytes, - diff: int, - hotkey_bytes: bytes, - lock: "mp.Lock", -): - """ - Update the current block data with the provided block information and difficulty. - - This function updates the current block and its difficulty in a thread-safe manner. It sets the current block - number, hashes the block with the hotkey, updates the current block bytes, and packs the difficulty. - - Parameters: - curr_diff: Shared array to store the current difficulty. - curr_block: Shared array to store the current block data. - curr_block_num: Shared value to store the current block number. - block_number: The block number to set as the current block number. - block_bytes: The block data bytes to be hashed with the hotkey. - diff: The difficulty value to be packed into the current difficulty array. - hotkey_bytes: The hotkey bytes used for hashing the block. - lock: A lock to ensure thread-safe updates. - """ - with lock: - curr_block_num.value = block_number - # Hash the block with the hotkey - block_and_hotkey_hash_bytes = _hash_block_with_hotkey(block_bytes, hotkey_bytes) - for i in range(32): - curr_block[i] = block_and_hotkey_hash_bytes[i] - _registration_diff_pack(diff, curr_diff) - - -def get_cpu_count() -> int: - """Returns the number of CPUs in the system.""" - try: - return len(os.sched_getaffinity(0)) - except AttributeError: - # macOS does not have sched_getaffinity - return os.cpu_count() - - -@dataclass -class RegistrationStatistics: - """Statistics for a registration.""" - - time_spent_total: float - rounds_total: int - time_average: float - time_spent: float - hash_rate_perpetual: float - hash_rate: float - difficulty: int - block_number: int - block_hash: str - - -class Status: - def __init__(self, status: str): - self._status = status - - def start(self): - pass - - def stop(self): - pass - - def update(self, status: str): - self._status = status - - -class Console: - @staticmethod - def status(status: str): - return Status(status) - - @staticmethod - def log(text: str): - print(text) - - -class RegistrationStatisticsLogger: - """Logs statistics for a registration.""" - - status: Optional["Status"] - - def __init__( - self, - console: Optional["Console"] = None, - output_in_place: bool = True, - ) -> None: - if console is None: - console = Console() - - self.console = console - - if output_in_place: - self.status = self.console.status("Solving") - else: - self.status = None - - def start(self) -> None: - if self.status is not None: - self.status.start() - - def stop(self) -> None: - if self.status is not None: - self.status.stop() - - @classmethod - def get_status_message( - cls, stats: "RegistrationStatistics", verbose: bool = False - ) -> str: - """Generates the status message based on registration statistics.""" - message = ( - "Solving\n" - + f"Time Spent (total): [bold white]{timedelta(seconds=stats.time_spent_total)}[/bold white]\n" - + ( - f"Time Spent This Round: {timedelta(seconds=stats.time_spent)}\n" - + f"Time Spent Average: {timedelta(seconds=stats.time_average)}\n" - if verbose - else "" - ) - + f"Registration Difficulty: [bold white]{millify(stats.difficulty)}[/bold white]\n" - + f"Iters (Inst/Perp): [bold white]{get_human_readable(stats.hash_rate, 'H')}/s / " - + f"{get_human_readable(stats.hash_rate_perpetual, 'H')}/s[/bold white]\n" - + f"Block Number: [bold white]{stats.block_number}[/bold white]\n" - + f"Block Hash: [bold white]{stats.block_hash.encode('utf-8')}[/bold white]\n" - ) - return message - - def update(self, stats: "RegistrationStatistics", verbose: bool = False) -> None: - if self.status is not None: - self.status.update(self.get_status_message(stats, verbose=verbose)) - else: - self.console.log(self.get_status_message(stats, verbose=verbose)) - - -def _solve_for_difficulty_fast( - subtensor: "Subtensor", - wallet: "Wallet", - netuid: int, - output_in_place: bool = True, - num_processes: Optional[int] = None, - update_interval: Optional[int] = None, - n_samples: int = 10, - alpha_: float = 0.80, - log_verbose: bool = False, -) -> Optional[POWSolution]: - """ - Solves the POW for registration using multiprocessing. - - Parameters: - subtensor: Subtensor instance. - wallet: wallet to use for registration. - netuid: The netuid of the subnet to register to. - output_in_place: If true, prints the status in place. Otherwise, prints the status on a new line. - num_processes: Number of processes to use. - update_interval: Number of nonces to solve before updating block information. - n_samples: The number of samples of the hash_rate to keep for the EWMA. - alpha_: The alpha for the EWMA for the hash_rate calculation. - log_verbose: If true, prints more verbose logging of the registration metrics. - - Note: - The hash rate is calculated as an exponentially weighted moving average in order to make the measure more robust. - We can also modify the update interval to do smaller blocks of work, while still updating the block information - after a different number of nonces, to increase the transparency of the process while still keeping the speed. - """ - if num_processes is None: - # get the number of allowed processes for this process - num_processes = min(1, get_cpu_count()) - - if update_interval is None: - update_interval = 50_000 - - limit = int(math.pow(2, 256)) - 1 - - curr_block, curr_block_num, curr_diff = Solver.create_shared_memory() - - # Establish communication queues - # See the Solver class for more information on the queues. - stopEvent = mp.Event() - stopEvent.clear() - - solution_queue = mp.Queue() - finished_queues = [mp.Queue() for _ in range(num_processes)] - check_block = mp.Lock() - - hotkey_bytes = ( - wallet.coldkeypub.public_key if netuid == -1 else wallet.hotkey.public_key - ) - # Start consumers - solvers = [ - Solver( - i, - num_processes, - update_interval, - finished_queues[i], - solution_queue, - stopEvent, - curr_block, - curr_block_num, - curr_diff, - check_block, - limit, - ) - for i in range(num_processes) - ] - - # Get first block - block_number, difficulty, block_hash = _get_block_with_retry( - subtensor=subtensor, netuid=netuid - ) - - block_bytes = bytes.fromhex(block_hash[2:]) - old_block_number = block_number - # Set to current block - update_curr_block( - curr_diff, - curr_block, - curr_block_num, - block_number, - block_bytes, - difficulty, - hotkey_bytes, - check_block, - ) - - # Set new block events for each solver to start at the initial block - for worker in solvers: - worker.newBlockEvent.set() - - for worker in solvers: - worker.start() # start the solver processes - - start_time = time.time() # time that the registration started - time_last = start_time # time that the last work blocks completed - - curr_stats = RegistrationStatistics( - time_spent_total=0.0, - time_average=0.0, - rounds_total=0, - time_spent=0.0, - hash_rate_perpetual=0.0, - hash_rate=0.0, - difficulty=difficulty, - block_number=block_number, - block_hash=block_hash, - ) - - start_time_perpetual = time.time() - - logger = RegistrationStatisticsLogger(output_in_place=output_in_place) - logger.start() - - solution = None - - hash_rates = [0] * n_samples # The last n true hash_rates - weights = [alpha_**i for i in range(n_samples)] # weights decay by alpha - - while netuid == -1 or not subtensor.is_hotkey_registered( - netuid=netuid, hotkey_ss58=wallet.hotkey.ss58_address - ): - # Wait until a solver finds a solution - try: - solution = solution_queue.get(block=True, timeout=0.25) - if solution is not None: - break - except Empty: - # No solution found, try again - pass - - # check for new block - old_block_number = _check_for_newest_block_and_update( - subtensor=subtensor, - netuid=netuid, - hotkey_bytes=hotkey_bytes, - old_block_number=old_block_number, - curr_diff=curr_diff, - curr_block=curr_block, - curr_block_num=curr_block_num, - curr_stats=curr_stats, - update_curr_block_=update_curr_block, - check_block=check_block, - solvers=solvers, - ) - - num_time = 0 - for finished_queue in finished_queues: - try: - finished_queue.get(timeout=0.1) - num_time += 1 - - except Empty: - continue - - time_now = time.time() # get current time - time_since_last = time_now - time_last # get time since last work block(s) - if num_time > 0 and time_since_last > 0.0: - # create EWMA of the hash_rate to make measure more robust - - hash_rate_ = (num_time * update_interval) / time_since_last - hash_rates.append(hash_rate_) - hash_rates.pop(0) # remove the 0th data point - curr_stats.hash_rate = sum( - [hash_rates[i] * weights[i] for i in range(n_samples)] - ) / (sum(weights)) - - # update time last to now - time_last = time_now - - curr_stats.time_average = ( - curr_stats.time_average * curr_stats.rounds_total - + curr_stats.time_spent - ) / (curr_stats.rounds_total + num_time) - curr_stats.rounds_total += num_time - - # Update stats - curr_stats.time_spent = time_since_last - new_time_spent_total = time_now - start_time_perpetual - curr_stats.hash_rate_perpetual = ( - curr_stats.rounds_total * update_interval - ) / new_time_spent_total - curr_stats.time_spent_total = new_time_spent_total - - # Update the logger - logger.update(curr_stats, verbose=log_verbose) - - # exited while, solution contains the nonce or wallet is registered - stopEvent.set() # stop all other processes - logger.stop() - - # terminate and wait for all solvers to exit - terminate_workers_and_wait_for_exit(solvers) - - return solution - - -def _get_block_with_retry(subtensor: "Subtensor", netuid: int) -> tuple[int, int, str]: - """ - Gets the current block number, difficulty, and block hash from the substrate node. - - Parameters: - subtensor: The subtensor instance. - netuid: The netuid of the network to get the block number, difficulty, and block hash from. - - Returns: - tuple[int, int, bytes] - - block_number: The current block number. - - difficulty: The current difficulty of the subnet. - - block_hash: The current block hash. - - Raises: - Exception: If the block hash is None. - ValueError: If the difficulty is None. - """ - block_number = subtensor.get_current_block() - difficulty = 1_000_000 if netuid == -1 else subtensor.difficulty(netuid=netuid) - block_hash = subtensor.get_block_hash(block_number) - if block_hash is None: - raise Exception( - "Network error. Could not connect to substrate to get block hash" - ) - if difficulty is None: - raise ValueError("Chain error. Difficulty is None") - return block_number, difficulty, block_hash - - -def _check_for_newest_block_and_update( - subtensor: "Subtensor", - netuid: int, - old_block_number: int, - hotkey_bytes: bytes, - curr_diff: "mp.Array", - curr_block: "mp.Array", - curr_block_num: "mp.Value", - update_curr_block_: "Callable", - check_block: "mp.Lock", - solvers: Union[list["Solver"], list["CUDASolver"]], - curr_stats: "RegistrationStatistics", -) -> int: - """ - Checks for a new block and updates the current block information if a new block is found. - - Parameters: - subtensor: Subtensor instance. - netuid: The netuid to use for retrieving the difficulty. - old_block_number: The old block number to check against. - hotkey_bytes: The bytes of the hotkey's pubkey. - curr_diff: The current difficulty as a multiprocessing array. - curr_block: Where the current block is stored as a multiprocessing array. - curr_block_num: Where the current block number is stored as a multiprocessing value. - update_curr_block_: A function that updates the current block. - check_block: A mp lock that is used to check for a new block. - solvers: A list of solvers to update the current block for. - curr_stats: The current registration statistics to update. - - Returns: - The current block number. - """ - block_number = subtensor.get_current_block() - if block_number != old_block_number: - old_block_number = block_number - # update block information - block_number, difficulty, block_hash = _get_block_with_retry( - subtensor=subtensor, netuid=netuid - ) - block_bytes = bytes.fromhex(block_hash[2:]) - - update_curr_block_( - curr_diff, - curr_block, - curr_block_num, - block_number, - block_bytes, - difficulty, - hotkey_bytes, - check_block, - ) - # Set new block events for each solver - - for worker in solvers: - worker.newBlockEvent.set() - - # update stats - curr_stats.block_number = block_number - curr_stats.block_hash = block_hash - curr_stats.difficulty = difficulty - - return old_block_number - - -def _solve_for_difficulty_fast_cuda( - subtensor: "Subtensor", - wallet: "Wallet", - netuid: int, - output_in_place: bool = True, - update_interval: int = 50_000, - tpb: int = 512, - dev_id: Union[list[int], int] = 0, - n_samples: int = 10, - alpha_: float = 0.80, - log_verbose: bool = False, -) -> Optional["POWSolution"]: - """ - Solves the registration fast using CUDA. - - Parameters: - subtensor: Subtensor instance. - wallet: Bittensor Wallet instance. - netuid: The netuid of the subnet to register to. - output_in_place: If true, prints the output in place, otherwise prints to new lines. - update_interval: The number of nonces to try before checking for more blocks. - tpb: The number of threads per block. CUDA param that should match the GPU capability - dev_id: The CUDA device IDs to execute the registration on, either a single device or a list of devices. - n_samples: The number of samples of the hash_rate to keep for the EWMA. - alpha_: The alpha for the EWMA for the hash_rate calculation. - log_verbose: If true, prints more verbose logging of the registration metrics. - - Note: - The hash rate is calculated as an exponentially weighted moving average in order to make the measure more robust. - """ - if isinstance(dev_id, int): - dev_id = [dev_id] - elif dev_id is None: - dev_id = [0] - - if update_interval is None: - update_interval = 50_000 - - if not torch.cuda.is_available(): - raise Exception("CUDA not available") - - limit = int(math.pow(2, 256)) - 1 - - # Set mp start to use spawn so CUDA doesn't complain - with UsingSpawnStartMethod(force=True): - curr_block, curr_block_num, curr_diff = CUDASolver.create_shared_memory() - - # Create a worker per CUDA device - num_processes = len(dev_id) - - # Establish communication queues - stopEvent = mp.Event() - stopEvent.clear() - solution_queue = mp.Queue() - finished_queues = [mp.Queue() for _ in range(num_processes)] - check_block = mp.Lock() - - hotkey_bytes = wallet.hotkey.public_key - # Start workers - solvers = [ - CUDASolver( - i, - num_processes, - update_interval, - finished_queues[i], - solution_queue, - stopEvent, - curr_block, - curr_block_num, - curr_diff, - check_block, - limit, - dev_id[i], - tpb, - ) - for i in range(num_processes) - ] - - # Get first block - block_number, difficulty, block_hash = _get_block_with_retry( - subtensor=subtensor, netuid=netuid - ) - - block_bytes = bytes.fromhex(block_hash[2:]) - old_block_number = block_number - - # Set to current block - update_curr_block( - curr_diff, - curr_block, - curr_block_num, - block_number, - block_bytes, - difficulty, - hotkey_bytes, - check_block, - ) - - # Set new block events for each solver to start at the initial block - for worker in solvers: - worker.newBlockEvent.set() - - for worker in solvers: - worker.start() # start the solver processes - - start_time = time.time() # time that the registration started - time_last = start_time # time that the last work blocks completed - - curr_stats = RegistrationStatistics( - time_spent_total=0.0, - time_average=0.0, - rounds_total=0, - time_spent=0.0, - hash_rate_perpetual=0.0, - hash_rate=0.0, # EWMA hash_rate (H/s) - difficulty=difficulty, - block_number=block_number, - block_hash=block_hash, - ) - - start_time_perpetual = time.time() - - logger = RegistrationStatisticsLogger(output_in_place=output_in_place) - logger.start() - - hash_rates = [0] * n_samples # The last n true hash_rates - weights = [alpha_**i for i in range(n_samples)] # weights decay by alpha - - solution = None - while netuid == -1 or not subtensor.is_hotkey_registered( - netuid=netuid, hotkey_ss58=wallet.hotkey.ss58_address - ): - # Wait until a solver finds a solution - try: - solution = solution_queue.get(block=True, timeout=0.15) - if solution is not None: - break - except Empty: - # No solution found, try again - pass - - # check for new block - old_block_number = _check_for_newest_block_and_update( - subtensor=subtensor, - netuid=netuid, - hotkey_bytes=hotkey_bytes, - curr_diff=curr_diff, - curr_block=curr_block, - curr_block_num=curr_block_num, - old_block_number=old_block_number, - curr_stats=curr_stats, - update_curr_block_=update_curr_block, - check_block=check_block, - solvers=solvers, - ) - - num_time = 0 - # Get times for each solver - for finished_queue in finished_queues: - try: - finished_queue.get(timeout=0.1) - num_time += 1 - - except Empty: - continue - - time_now = time.time() # get current time - time_since_last = time_now - time_last # get time since last work block(s) - if num_time > 0 and time_since_last > 0.0: - # create EWMA of the hash_rate to make measure more robust - - hash_rate_ = (num_time * tpb * update_interval) / time_since_last - hash_rates.append(hash_rate_) - hash_rates.pop(0) # remove the 0th data point - curr_stats.hash_rate = sum( - [hash_rates[i] * weights[i] for i in range(n_samples)] - ) / (sum(weights)) - - # update time last to now - time_last = time_now - - curr_stats.time_average = ( - curr_stats.time_average * curr_stats.rounds_total - + curr_stats.time_spent - ) / (curr_stats.rounds_total + num_time) - curr_stats.rounds_total += num_time - - # Update stats - curr_stats.time_spent = time_since_last - new_time_spent_total = time_now - start_time_perpetual - curr_stats.hash_rate_perpetual = ( - curr_stats.rounds_total * (tpb * update_interval) - ) / new_time_spent_total - curr_stats.time_spent_total = new_time_spent_total - - # Update the logger - logger.update(curr_stats, verbose=log_verbose) - - # exited while, found_solution contains the nonce or wallet is registered - - stopEvent.set() # stop all other processes - logger.stop() - - # terminate and wait for all solvers to exit - terminate_workers_and_wait_for_exit(solvers) - - return solution - - -def terminate_workers_and_wait_for_exit( - workers: list[Union[mp.Process, QueueType]], -) -> None: - for worker in workers: - if isinstance(worker, QueueType): - worker.join_thread() - else: - try: - worker.join(3.0) - except subprocess.TimeoutExpired: - worker.terminate() - try: - worker.close() - except ValueError: - worker.terminate() - - -def create_pow( - subtensor: "Subtensor", - wallet: "Wallet", - netuid: int, - output_in_place: bool = True, - cuda: bool = False, - dev_id: Union[list[int], int] = 0, - tpb: int = 256, - num_processes: Optional[int] = None, - update_interval: Optional[int] = None, - log_verbose: bool = False, -) -> Optional["POWSolution"]: - """ - Creates a proof of work for the given subtensor and wallet. - - Parameters: - subtensor: The Subtensor instance. - wallet: The Bittensor Wallet instance. - netuid: The netuid for the subnet to create a proof of work for. - output_in_place: If true, prints the progress of the proof of work to the console in-place. Meaning the progress - is printed on the same lines. - cuda: If true, uses CUDA to solve the proof of work. - dev_id: The CUDA device id(s) to use. If cuda is true and dev_id is a list, then multiple CUDA devices will be - used to solve the proof of work. - tpb: The number of threads per block to use when solving the proof of work. Should be a multiple of 32. - num_processes: The number of processes to use when solving the proof of work. If None, then the number of - processes is equal to the number of CPU cores. - update_interval: The number of nonces to run before checking for a new block. - log_verbose: If true, prints the progress of the proof of work more verbosely. - - Returns: - The proof of work solution or None if the wallet is already registered or there is a different error. - - Raises: - ValueError: If the subnet does not exist. - """ - if netuid != -1: - if not subtensor.subnet_exists(netuid=netuid): - raise ValueError(f"Subnet {netuid} does not exist.") - - if cuda: - logging.debug("Solve difficulty with CUDA.") - solution: Optional[POWSolution] = _solve_for_difficulty_fast_cuda( - subtensor=subtensor, - wallet=wallet, - netuid=netuid, - output_in_place=output_in_place, - dev_id=dev_id, - tpb=tpb, - update_interval=update_interval, - log_verbose=log_verbose, - ) - else: - logging.debug("Solve difficulty.") - solution: Optional[POWSolution] = _solve_for_difficulty_fast( - subtensor=subtensor, - wallet=wallet, - netuid=netuid, - output_in_place=output_in_place, - num_processes=num_processes, - update_interval=update_interval, - log_verbose=log_verbose, - ) - return solution diff --git a/bittensor/utils/registration/register_cuda.py b/bittensor/utils/registration/register_cuda.py deleted file mode 100644 index 34c8e00115..0000000000 --- a/bittensor/utils/registration/register_cuda.py +++ /dev/null @@ -1,123 +0,0 @@ -"""This module provides functions for solving Proof of Work (PoW) problems using CUDA.""" - -import binascii -import hashlib -import io -from contextlib import redirect_stdout -from typing import Any, Union - -import numpy as np -from Crypto.Hash import keccak - - -def _hex_bytes_to_u8_list(hex_bytes: bytes) -> list[int]: - """ - Convert a sequence of bytes in hexadecimal format to a list of - unsigned 8-bit integers. - - Parameters: - hex_bytes: A sequence of bytes in hexadecimal format. - - Returns: - A list of unsigned 8-bit integers. - - """ - return [int(hex_bytes[i : i + 2], 16) for i in range(0, len(hex_bytes), 2)] - - -def _create_seal_hash(block_and_hotkey_hash_hex_: bytes, nonce: int) -> bytes: - """Creates a seal hash from the block and hotkey hash and nonce.""" - nonce_bytes = binascii.hexlify(nonce.to_bytes(8, "little")) - pre_seal = nonce_bytes + block_and_hotkey_hash_hex_ - seal_sh256 = hashlib.sha256(bytearray(_hex_bytes_to_u8_list(pre_seal))).digest() - kec = keccak.new(digest_bits=256) - return kec.update(seal_sh256).digest() - - -def _seal_meets_difficulty(seal_: bytes, difficulty: int, limit: int) -> bool: - """Checks if the seal meets the given difficulty.""" - seal_number = int.from_bytes(seal_, "big") - product = seal_number * difficulty - # limit = int(math.pow(2, 256)) - 1 - return product < limit - - -def solve_cuda( - nonce_start: "np.int64", - update_interval: "np.int64", - tpb: int, - block_and_hotkey_hash_bytes: bytes, - difficulty: int, - limit: int, - dev_id: int = 0, -) -> Union[tuple[Any, bytes], tuple[int, bytes], tuple[Any, None]]: - """ - Solves the PoW problem using CUDA. - - Parameters: - nonce_start: Starting nonce. - update_interval: Number of nonces to solve before updating block information. - tpb: Threads per block. - block_and_hotkey_hash_bytes: Keccak(Bytes of the block hash + bytes of the hotkey) 64 bytes. - difficulty: Difficulty of the PoW problem. - limit: Upper limit of the nonce. - dev_id: The CUDA device ID. - - Returns: - Tuple of the nonce and the seal corresponding to the solution. Returns -1 for nonce if no solution is found. - """ - - try: - import cubit - except ImportError: - raise ImportError( - "Please install cubit. See the instruction https://github.com/opentensor/cubit?tab=readme-ov-file#install." - ) - - upper = int(limit // difficulty) - - upper_bytes = upper.to_bytes(32, byteorder="little", signed=False) - - # Call cython function - # int blockSize, uint64 nonce_start, uint64 update_interval, const unsigned char[:] limit, - # const unsigned char[:] block_bytes, int dev_id - block_and_hotkey_hash_hex = binascii.hexlify(block_and_hotkey_hash_bytes)[:64] - - solution = cubit.solve_cuda( - tpb, - nonce_start, - update_interval, - upper_bytes, - block_and_hotkey_hash_hex, - dev_id, - ) # 0 is first GPU - seal = None - if solution != -1: - seal = _create_seal_hash(block_and_hotkey_hash_hex, solution) - if _seal_meets_difficulty(seal, difficulty, limit): - return solution, seal - else: - return -1, b"\x00" * 32 - return solution, seal - - -def reset_cuda(): - """Resets the CUDA environment.""" - try: - import cubit - except ImportError: - raise ImportError("Please install cubit") - cubit.reset_cuda() - - -def log_cuda_errors() -> str: - """Logs any CUDA errors.""" - try: - import cubit - except ImportError: - raise ImportError("Please install cubit") - - file = io.StringIO() - with redirect_stdout(file): - cubit.log_cuda_errors() - return file.getvalue() From 14aefa7331f033f86cef824ced6618fc9958ef43 Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Thu, 23 Apr 2026 15:56:23 -0700 Subject: [PATCH 107/118] update extrinsics --- .../core/extrinsics/asyncex/registration.py | 211 +----------------- bittensor/core/extrinsics/registration.py | 209 +---------------- 2 files changed, 4 insertions(+), 416 deletions(-) diff --git a/bittensor/core/extrinsics/asyncex/registration.py b/bittensor/core/extrinsics/asyncex/registration.py index 5b4897fbf9..3457fd915a 100644 --- a/bittensor/core/extrinsics/asyncex/registration.py +++ b/bittensor/core/extrinsics/asyncex/registration.py @@ -1,9 +1,9 @@ """ -This module provides async functionalities for registering a wallet with the subtensor network using Proof-of-Work (PoW). +This module provides async functionalities for registering a wallet with the subtensor network. """ import asyncio -from typing import Optional, Union, TYPE_CHECKING +from typing import Optional, TYPE_CHECKING from bittensor.core.errors import BalanceTypeError, RegistrationError from bittensor.core.extrinsics.asyncex.mev_shield import submit_encrypted_extrinsic @@ -12,7 +12,6 @@ from bittensor.core.types import ExtrinsicResponse from bittensor.utils.balance import Balance from bittensor.utils.btlogging import logging -from bittensor.utils.registration import create_pow_async, log_no_torch_error, torch if TYPE_CHECKING: from bittensor_wallet import Wallet @@ -392,212 +391,6 @@ async def register_subnet_extrinsic( return ExtrinsicResponse.from_exception(raise_error=raise_error, error=error) -async def register_extrinsic( - subtensor: "AsyncSubtensor", - wallet: "Wallet", - netuid: int, - max_allowed_attempts: int = 3, - output_in_place: bool = True, - cuda: bool = False, - dev_id: Union[list[int], int] = 0, - tpb: int = 256, - num_processes: Optional[int] = None, - update_interval: Optional[int] = None, - log_verbose: bool = False, - *, - mev_protection: bool = DEFAULT_MEV_PROTECTION, - period: Optional[int] = None, - raise_error: bool = False, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = True, - wait_for_revealed_execution: bool = True, -) -> ExtrinsicResponse: - """Registers a neuron on the Bittensor subnet with provided netuid using the provided wallet. - - Registration is a critical step for a neuron to become an active participant in the network, enabling it to stake, - set weights, and receive incentives. - - Parameters: - subtensor: Subtensor object to use for chain interactions - wallet: Bittensor wallet object. - netuid: The ``netuid`` of the subnet to register on. - max_allowed_attempts: Maximum number of attempts to register the wallet. - output_in_place: Whether the POW solving should be outputted to the console as it goes along. - cuda: If `True`, the wallet should be registered using CUDA device(s). - dev_id: The CUDA device id to use, or a list of device ids. - tpb: The number of threads per block (CUDA). - num_processes: The number of processes to use to register. - update_interval: The number of nonces to solve between updates. - log_verbose: If `True`, the registration process will log more information. - mev_protection: If True, encrypts and submits the transaction through the MEV Shield pallet to protect - against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators - decrypt and execute it. If False, submits the transaction directly without encryption. - period: The number of blocks during which the transaction will remain valid after it's submitted. If the - transaction is not included in a block within that number of blocks, it will expire and be rejected. You can - think of it as an expiration date for the transaction. - raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. - wait_for_inclusion: Whether to wait for the inclusion of the transaction. - wait_for_finalization: Whether to wait for the finalization of the transaction. - wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. - - Returns: - ExtrinsicResponse: The result object of the extrinsic execution. - """ - try: - if not ( - unlocked := ExtrinsicResponse.unlock_wallet( - wallet, raise_error, unlock_type="both" - ) - ).success: - return unlocked - - block_hash = await subtensor.substrate.get_chain_head() - if not await subtensor.subnet_exists(netuid, block_hash=block_hash): - return ExtrinsicResponse( - False, f"Subnet {netuid} does not exist." - ).with_log() - - neuron = await subtensor.get_neuron_for_pubkey_and_subnet( - hotkey_ss58=wallet.hotkey.ss58_address, netuid=netuid, block_hash=block_hash - ) - - if not neuron.is_null: - message = "Already registered." - logging.debug(f"[green]{message}[/green]") - logging.debug(f"\t\tuid: [blue]{neuron.uid}[/blue]") - logging.debug(f"\t\tnetuid: [blue]{neuron.netuid}[/blue]") - logging.debug(f"\t\thotkey: [blue]{neuron.hotkey}[/blue]") - logging.debug(f"\t\tcoldkey: [blue]{neuron.coldkey}[/blue]") - return ExtrinsicResponse(message=message, data={"neuron": neuron}) - - logging.debug( - f"Registration hotkey: [blue]{wallet.hotkey.ss58_address}[/blue], Public coldkey: " - f"[blue]{wallet.coldkey.ss58_address}[/blue] in the network: [blue]{subtensor.network}[/blue]." - ) - - if not torch: - log_no_torch_error() - return ExtrinsicResponse(False, "Torch is not installed.").with_log() - - # Attempt rolling registration. - attempts = 1 - - while True: - # Solve latest POW. - if cuda: - if not torch.cuda.is_available(): - return ExtrinsicResponse(False, "CUDA not available.").with_log() - - logging.debug("Creating a POW with CUDA.") - pow_result = await create_pow_async( - subtensor=subtensor, - wallet=wallet, - netuid=netuid, - output_in_place=output_in_place, - cuda=cuda, - dev_id=dev_id, - tpb=tpb, - num_processes=num_processes, - update_interval=update_interval, - log_verbose=log_verbose, - ) - else: - logging.debug("Creating a POW.") - pow_result = await create_pow_async( - subtensor=subtensor, - wallet=wallet, - netuid=netuid, - output_in_place=output_in_place, - cuda=cuda, - num_processes=num_processes, - update_interval=update_interval, - log_verbose=log_verbose, - ) - - # pow failed - if not pow_result: - # might be registered already on this subnet - is_registered = await subtensor.is_hotkey_registered( - netuid=netuid, hotkey_ss58=wallet.hotkey.ss58_address - ) - if is_registered: - message = f"Already registered in subnet {netuid}." - logging.debug(f"[green]{message}[/green]") - return ExtrinsicResponse(message=message) - - # pow successful, proceed to submit pow to chain for registration - else: - # check if a pow result is still valid - while not await pow_result.is_stale_async(subtensor=subtensor): - call = await SubtensorModule(subtensor).register( - netuid=netuid, - coldkey=wallet.coldkeypub.ss58_address, - hotkey=wallet.hotkey.ss58_address, - block_number=pow_result.block_number, - nonce=pow_result.nonce, - work=[int(byte_) for byte_ in pow_result.seal], - ) - if mev_protection: - response = await submit_encrypted_extrinsic( - subtensor=subtensor, - wallet=wallet, - call=call, - period=period, - raise_error=raise_error, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - wait_for_revealed_execution=wait_for_revealed_execution, - ) - else: - response = await subtensor.sign_and_send_extrinsic( - call=call, - wallet=wallet, - period=period, - raise_error=raise_error, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - if not response.success: - # Look error here - # https://github.com/opentensor/subtensor/blob/development/pallets/subtensor/src/errors.rs - if "HotKeyAlreadyRegisteredInSubNet" in response.message: - logging.debug( - f"[green]Already registered on subnet:[/green] [blue]{netuid}[/blue]." - ) - return response - await asyncio.sleep(0.5) - - if response.success: - is_registered = await subtensor.is_hotkey_registered( - netuid=netuid, hotkey_ss58=wallet.hotkey.ss58_address - ) - if is_registered: - logging.debug("[green]Registered.[/green]") - return response - - # neuron not found, try again - logging.warning("[red]Unknown error. Neuron not found.[/red]") - continue - else: - # Exited loop because pow is no longer valid. - logging.warning("[red]POW is stale.[/red]") - # Try again. - - if attempts < max_allowed_attempts: - # Failed registration, retry pow - attempts += 1 - logging.warning( - f"Failed registration, retrying pow ... [blue]({attempts}/{max_allowed_attempts})[/blue]" - ) - else: - # Failed to register after max attempts. - return ExtrinsicResponse(False, "No more attempts.").with_log() - - except Exception as error: - return ExtrinsicResponse.from_exception(raise_error=raise_error, error=error) - - async def set_subnet_identity_extrinsic( subtensor: "AsyncSubtensor", wallet: "Wallet", diff --git a/bittensor/core/extrinsics/registration.py b/bittensor/core/extrinsics/registration.py index cb24a6345c..3823b951e8 100644 --- a/bittensor/core/extrinsics/registration.py +++ b/bittensor/core/extrinsics/registration.py @@ -1,9 +1,9 @@ """ -This module provides sync functionalities for registering a wallet with the subtensor network using Proof-of-Work (PoW). +This module provides sync functionalities for registering a wallet with the subtensor network. """ import time -from typing import Optional, Union, TYPE_CHECKING +from typing import Optional, TYPE_CHECKING from bittensor.core.errors import BalanceTypeError, RegistrationError from bittensor.core.extrinsics.mev_shield import submit_encrypted_extrinsic @@ -12,7 +12,6 @@ from bittensor.core.types import ExtrinsicResponse from bittensor.utils.balance import Balance from bittensor.utils.btlogging import logging -from bittensor.utils.registration import create_pow, log_no_torch_error, torch if TYPE_CHECKING: from bittensor_wallet import Wallet @@ -382,210 +381,6 @@ def register_subnet_extrinsic( return ExtrinsicResponse.from_exception(raise_error=raise_error, error=error) -def register_extrinsic( - subtensor: "Subtensor", - wallet: "Wallet", - netuid: int, - max_allowed_attempts: int = 3, - output_in_place: bool = True, - cuda: bool = False, - dev_id: Union[list[int], int] = 0, - tpb: int = 256, - num_processes: Optional[int] = None, - update_interval: Optional[int] = None, - log_verbose: bool = False, - *, - mev_protection: bool = DEFAULT_MEV_PROTECTION, - period: Optional[int] = None, - raise_error: bool = False, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = True, - wait_for_revealed_execution: bool = True, -) -> ExtrinsicResponse: - """Registers a neuron on the Bittensor subnet with provided netuid using the provided wallet. - - Parameters: - subtensor: Subtensor object to use for chain interactions - wallet: Bittensor wallet object. - netuid: The ``netuid`` of the subnet to register on. - max_allowed_attempts: Maximum number of attempts to register the wallet. - output_in_place: Whether the POW solving should be outputted to the console as it goes along. - cuda: If `True`, the wallet should be registered using CUDA device(s). - dev_id: The CUDA device id to use, or a list of device ids. - tpb: The number of threads per block (CUDA). - num_processes: The number of processes to use to register. - update_interval: The number of nonces to solve between updates. - log_verbose: If `True`, the registration process will log more information. - mev_protection: If True, encrypts and submits the transaction through the MEV Shield pallet to protect - against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators - decrypt and execute it. If False, submits the transaction directly without encryption. - period: The number of blocks during which the transaction will remain valid after it's submitted. If the - transaction is not included in a block within that number of blocks, it will expire and be rejected. You can - think of it as an expiration date for the transaction. - raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. - wait_for_inclusion: Whether to wait for the inclusion of the transaction. - wait_for_finalization: Whether to wait for the finalization of the transaction. - wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. - - Returns: - ExtrinsicResponse: The result object of the extrinsic execution. - """ - try: - if not ( - unlocked := ExtrinsicResponse.unlock_wallet( - wallet, raise_error, unlock_type="both" - ) - ).success: - return unlocked - - block = subtensor.get_current_block() - if not subtensor.subnet_exists(netuid, block=block): - return ExtrinsicResponse( - False, f"Subnet {netuid} does not exist." - ).with_log() - - neuron = subtensor.get_neuron_for_pubkey_and_subnet( - hotkey_ss58=wallet.hotkey.ss58_address, netuid=netuid, block=block - ) - - if not neuron.is_null: - message = "Already registered." - logging.debug(f"[green]{message}[/green]") - logging.debug(f"\t\tuid: [blue]{neuron.uid}[/blue]") - logging.debug(f"\t\tnetuid: [blue]{neuron.netuid}[/blue]") - logging.debug(f"\t\thotkey: [blue]{neuron.hotkey}[/blue]") - logging.debug(f"\t\tcoldkey: [blue]{neuron.coldkey}[/blue]") - return ExtrinsicResponse(message=message, data={"neuron": neuron}) - - logging.debug( - f"Registration hotkey: [blue]{wallet.hotkey.ss58_address}[/blue], Public coldkey: " - f"[blue]{wallet.coldkey.ss58_address}[/blue] in the network: [blue]{subtensor.network}[/blue]." - ) - - if not torch: - log_no_torch_error() - return ExtrinsicResponse(False, "Torch is not installed.").with_log() - - # Attempt rolling registration. - attempts = 1 - - while True: - # Solve latest POW. - if cuda: - if not torch.cuda.is_available(): - return ExtrinsicResponse(False, "CUDA not available.").with_log() - - logging.debug("Creating a POW with CUDA.") - pow_result = create_pow( - subtensor=subtensor, - wallet=wallet, - netuid=netuid, - output_in_place=output_in_place, - cuda=cuda, - dev_id=dev_id, - tpb=tpb, - num_processes=num_processes, - update_interval=update_interval, - log_verbose=log_verbose, - ) - else: - logging.debug("Creating a POW.") - pow_result = create_pow( - subtensor=subtensor, - wallet=wallet, - netuid=netuid, - output_in_place=output_in_place, - cuda=cuda, - num_processes=num_processes, - update_interval=update_interval, - log_verbose=log_verbose, - ) - - # pow failed - if not pow_result: - # might be registered already on this subnet - is_registered = subtensor.is_hotkey_registered( - netuid=netuid, hotkey_ss58=wallet.hotkey.ss58_address - ) - if is_registered: - message = f"Already registered in subnet {netuid}." - logging.debug(f"[green]{message}[/green]") - return ExtrinsicResponse(message=message) - - # pow successful, proceed to submit pow to chain for registration - else: - # check if a pow result is still valid - while not pow_result.is_stale(subtensor=subtensor): - # create extrinsic call - call = SubtensorModule(subtensor).register( - netuid=netuid, - coldkey=wallet.coldkeypub.ss58_address, - hotkey=wallet.hotkey.ss58_address, - block_number=pow_result.block_number, - nonce=pow_result.nonce, - work=[int(byte_) for byte_ in pow_result.seal], - ) - if mev_protection: - response = submit_encrypted_extrinsic( - subtensor=subtensor, - wallet=wallet, - call=call, - period=period, - raise_error=raise_error, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - wait_for_revealed_execution=wait_for_revealed_execution, - ) - else: - response = subtensor.sign_and_send_extrinsic( - call=call, - wallet=wallet, - period=period, - raise_error=raise_error, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - if not response.success: - # Look error here - # https://github.com/opentensor/subtensor/blob/development/pallets/subtensor/src/errors.rs - if "HotKeyAlreadyRegisteredInSubNet" in response.message: - logging.debug( - f"[green]Already registered on subnet:[/green] [blue]{netuid}[/blue]." - ) - return response - time.sleep(0.5) - - if response.success: - is_registered = subtensor.is_hotkey_registered( - netuid=netuid, hotkey_ss58=wallet.hotkey.ss58_address - ) - if is_registered: - logging.debug("[green]Registered.[/green]") - return response - - # neuron not found, try again - logging.warning("[red]Unknown error. Neuron not found.[/red]") - continue - else: - # Exited loop because pow is no longer valid. - logging.warning("[red]POW is stale.[/red]") - # Try again. - - if attempts < max_allowed_attempts: - # Failed registration, retry pow - attempts += 1 - logging.warning( - f"Failed registration, retrying pow ... [blue]({attempts}/{max_allowed_attempts})[/blue]" - ) - else: - # Failed to register after max attempts. - return ExtrinsicResponse(False, "No more attempts.").with_log() - - except Exception as error: - return ExtrinsicResponse.from_exception(raise_error=raise_error, error=error) - - def set_subnet_identity_extrinsic( subtensor: "Subtensor", wallet: "Wallet", From 24e11582ebb77af92e7b61eeddc61b7d0004a36f Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Thu, 23 Apr 2026 15:56:30 -0700 Subject: [PATCH 108/118] update subtensors --- bittensor/core/async_subtensor.py | 100 +++++++++++++++--------------- bittensor/core/subtensor.py | 77 ++++++++++++----------- 2 files changed, 88 insertions(+), 89 deletions(-) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index ca4b9c53b9..669d2fc326 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -97,7 +97,6 @@ ) from bittensor.core.extrinsics.asyncex.registration import ( burned_register_extrinsic, - register_extrinsic, register_limit_extrinsic, register_subnet_extrinsic, set_subnet_identity_extrinsic, @@ -7818,14 +7817,7 @@ async def register( self: "AsyncSubtensor", wallet: "Wallet", netuid: int, - max_allowed_attempts: int = 3, - output_in_place: bool = False, - cuda: bool = False, - dev_id: Union[list[int], int] = 0, - tpb: int = 256, - num_processes: Optional[int] = None, - update_interval: Optional[int] = None, - log_verbose: bool = False, + limit_price: Optional[Balance] = None, *, mev_protection: bool = DEFAULT_MEV_PROTECTION, period: Optional[int] = DEFAULT_PERIOD, @@ -7834,63 +7826,71 @@ async def register( wait_for_finalization: bool = True, wait_for_revealed_execution: bool = True, ) -> ExtrinsicResponse: - """ - Registers a neuron on the Bittensor subnet with provided netuid using the provided wallet. + """Registers a neuron on the Bittensor network by recycling TAO, with automatic price protection. + + Uses ``register_limit`` under the hood. If ``limit_price`` is not provided, it is automatically + calculated as the current recycle (burn) cost plus a 0.5% tolerance to protect against price fluctuations. - Registration is a critical step for a neuron to become an active participant in the network, enabling it to - stake, set weights, and receive incentives. + For root subnet (``netuid == 0``), delegates to ``root_register_extrinsic``. Parameters: wallet: The wallet associated with the neuron to be registered. netuid: The unique identifier of the subnet. - max_allowed_attempts: Maximum number of attempts to register the wallet. - output_in_place: If `True`, prints the progress of the proof of work to the console in-place. Meaning the - progress is printed on the same lines. - cuda: If `true`, the wallet should be registered using CUDA device(s). - dev_id: The CUDA device id to use, or a list of device ids. - tpb: The number of threads per block (CUDA). - num_processes: The number of processes to use to register. - update_interval: The number of nonces to solve between updates. - log_verbose: If `true`, the registration process will log more information. - mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect + limit_price: Maximum acceptable burn price as a Balance instance. If ``None``, automatically calculated + as ``recycle * 1.005`` (0.5% tolerance). If the on-chain burn price exceeds this value, the + transaction will fail with RegistrationPriceLimitExceeded. + mev_protection: If ``True``, encrypts and submits the transaction through the MEV Shield pallet to protect against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators - decrypt and execute it. If `False`, submits the transaction directly without encryption. + decrypt and execute it. If ``False``, submits the transaction directly without encryption. period: The number of blocks during which the transaction will remain valid after it's submitted. If the transaction is not included in a block within that number of blocks, it will expire and be rejected. You can think of it as an expiration date for the transaction. - raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. - wait_for_inclusion: Whether to wait for the inclusion of the transaction. - wait_for_finalization: Whether to wait for the finalization of the transaction. + raise_error: Raises a relevant exception rather than returning ``False`` if unsuccessful. + wait_for_inclusion: Waits for the transaction to be included in a block. + wait_for_finalization: Waits for the transaction to be finalized on the blockchain. wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. Returns: ExtrinsicResponse: The result object of the extrinsic execution. - This function facilitates the entry of new neurons into the network, supporting the decentralized growth and - scalability of the Bittensor ecosystem. - Notes: - Rate Limits: """ - return await register_extrinsic( - subtensor=self, - wallet=wallet, - netuid=netuid, - max_allowed_attempts=max_allowed_attempts, - tpb=tpb, - update_interval=update_interval, - num_processes=num_processes, - cuda=cuda, - dev_id=dev_id, - output_in_place=output_in_place, - log_verbose=log_verbose, - mev_protection=mev_protection, - period=period, - raise_error=raise_error, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - wait_for_revealed_execution=wait_for_revealed_execution, - ) + async with self: + if netuid == 0: + return await root_register_extrinsic( + subtensor=self, + wallet=wallet, + mev_protection=mev_protection, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + + if limit_price is not None: + check_balance_amount(limit_price) + else: + recycle = await self.recycle(netuid=netuid) + if recycle is None: + return ExtrinsicResponse( + False, f"Subnet {netuid} does not exist." + ).with_log() + limit_price = Balance.from_rao(recycle.rao * 1005 // 1000) + + return await register_limit_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + limit_price=limit_price, + mev_protection=mev_protection, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) async def register_limit( self, @@ -7950,7 +7950,7 @@ async def register_limit( ) async def register_subnet( - self: "AsyncSubtensor", + self, wallet: "Wallet", *, mev_protection: bool = DEFAULT_MEV_PROTECTION, diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index f15319cb0d..6c634aa2e0 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -97,7 +97,6 @@ ) from bittensor.core.extrinsics.registration import ( burned_register_extrinsic, - register_extrinsic, register_limit_extrinsic, register_subnet_extrinsic, set_subnet_identity_extrinsic, @@ -6660,14 +6659,7 @@ def register( self, wallet: "Wallet", netuid: int, - max_allowed_attempts: int = 3, - output_in_place: bool = True, - cuda: bool = False, - dev_id: Union[list[int], int] = 0, - tpb: int = 256, - num_processes: Optional[int] = None, - update_interval: Optional[int] = None, - log_verbose: bool = False, + limit_price: Optional[Balance] = None, *, mev_protection: bool = DEFAULT_MEV_PROTECTION, period: Optional[int] = DEFAULT_PERIOD, @@ -6676,56 +6668,63 @@ def register( wait_for_finalization: bool = True, wait_for_revealed_execution: bool = True, ) -> ExtrinsicResponse: - """ - Registers a neuron on the Bittensor subnet with provided netuid using the provided wallet. + """Registers a neuron on the Bittensor network by recycling TAO, with automatic price protection. - Registration is a critical step for a neuron to become an active participant in the network, enabling it to - stake, set weights, and receive incentives. + Uses ``register_limit`` under the hood. If ``limit_price`` is not provided, it is automatically + calculated as the current recycle (burn) cost plus a 0.5% tolerance to protect against price fluctuations. + + For root subnet (``netuid == 0``), delegates to ``root_register_extrinsic``. Parameters: wallet: The wallet associated with the neuron to be registered. netuid: The unique identifier of the subnet. - max_allowed_attempts: Maximum number of attempts to register the wallet. - output_in_place: If `True`, prints the progress of the proof of work to the console in-place. Meaning the - progress is printed on the same lines. - cuda: If `true`, the wallet should be registered using CUDA device(s). - dev_id: The CUDA device id to use, or a list of device ids. - tpb: The number of threads per block (CUDA). - num_processes: The number of processes to use to register. - update_interval: The number of nonces to solve between updates. - log_verbose: If `true`, the registration process will log more information. - mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect + limit_price: Maximum acceptable burn price as a Balance instance. If ``None``, automatically calculated + as ``recycle * 1.005`` (0.5% tolerance). If the on-chain burn price exceeds this value, the + transaction will fail with RegistrationPriceLimitExceeded. + mev_protection: If ``True``, encrypts and submits the transaction through the MEV Shield pallet to protect against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators - decrypt and execute it. If `False`, submits the transaction directly without encryption. + decrypt and execute it. If ``False``, submits the transaction directly without encryption. period: The number of blocks during which the transaction will remain valid after it's submitted. If the transaction is not included in a block within that number of blocks, it will expire and be rejected. You can think of it as an expiration date for the transaction. - raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. - wait_for_inclusion: Whether to wait for the inclusion of the transaction. - wait_for_finalization: Whether to wait for the finalization of the transaction. + raise_error: Raises a relevant exception rather than returning ``False`` if unsuccessful. + wait_for_inclusion: Waits for the transaction to be included in a block. + wait_for_finalization: Waits for the transaction to be finalized on the blockchain. wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. Returns: ExtrinsicResponse: The result object of the extrinsic execution. - This function facilitates the entry of new neurons into the network, supporting the decentralized growth and - scalability of the Bittensor ecosystem. - Notes: - Rate Limits: """ - return register_extrinsic( + if netuid == 0: + return root_register_extrinsic( + subtensor=self, + wallet=wallet, + mev_protection=mev_protection, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + + if limit_price is not None: + check_balance_amount(limit_price) + else: + recycle = self.recycle(netuid=netuid) + if recycle is None: + return ExtrinsicResponse( + False, f"Subnet {netuid} does not exist." + ).with_log() + limit_price = Balance.from_rao(recycle.rao * 1005 // 1000) + + return register_limit_extrinsic( subtensor=self, wallet=wallet, netuid=netuid, - max_allowed_attempts=max_allowed_attempts, - tpb=tpb, - update_interval=update_interval, - num_processes=num_processes, - cuda=cuda, - dev_id=dev_id, - output_in_place=output_in_place, - log_verbose=log_verbose, + limit_price=limit_price, mev_protection=mev_protection, period=period, raise_error=raise_error, From 241fb9048ed422091e9247d448412489be98e985 Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Thu, 23 Apr 2026 15:56:39 -0700 Subject: [PATCH 109/118] opps - pallet --- .../extrinsics/pallets/subtensor_module.py | 31 ------------------- 1 file changed, 31 deletions(-) diff --git a/bittensor/core/extrinsics/pallets/subtensor_module.py b/bittensor/core/extrinsics/pallets/subtensor_module.py index 019ecc58f0..0dddc4c3e8 100644 --- a/bittensor/core/extrinsics/pallets/subtensor_module.py +++ b/bittensor/core/extrinsics/pallets/subtensor_module.py @@ -243,37 +243,6 @@ def move_stake( alpha_amount=alpha_amount, ) - def register( - self, - netuid: int, - coldkey: str, - hotkey: str, - block_number: int, - nonce: int, - work: list[int], - ) -> Call: - """Returns GenericCall instance for Subtensor function SubtensorModule.register. - - Parameters: - netuid: The netuid of the subnet to register on. - coldkey: The coldkey SS58 address associated with the neuron. - hotkey: The hotkey SS58 address associated with the neuron. - block_number: POW block number. - nonce: POW nonce. - work: List representation of POW seal. - - Returns: - GenericCall instance. - """ - return self.create_composed_call( - netuid=netuid, - coldkey=coldkey, - hotkey=hotkey, - block_number=block_number, - nonce=nonce, - work=work, - ) - def register_limit( self, netuid: int, From 2c8286e9b87b3656544757fb788bb5f23872baef Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Thu, 23 Apr 2026 15:56:50 -0700 Subject: [PATCH 110/118] unit tests --- .../extrinsics/asyncex/test_registration.py | 300 ------------------ .../extrinsics/test_registration.py | 166 ---------- tests/unit_tests/test_async_subtensor.py | 44 ++- tests/unit_tests/test_subtensor.py | 48 +++ tests/unit_tests/utils/test_registration.py | 4 +- 5 files changed, 79 insertions(+), 483 deletions(-) diff --git a/tests/unit_tests/extrinsics/asyncex/test_registration.py b/tests/unit_tests/extrinsics/asyncex/test_registration.py index 30a109024b..7e842b7173 100644 --- a/tests/unit_tests/extrinsics/asyncex/test_registration.py +++ b/tests/unit_tests/extrinsics/asyncex/test_registration.py @@ -5,306 +5,6 @@ from bittensor.core.extrinsics.asyncex import registration as async_registration -@pytest.mark.asyncio -async def test_register_extrinsic_success(subtensor, fake_wallet, mocker): - """Tests successful registration.""" - # Preps - fake_wallet.hotkey.ss58_address = "hotkey_ss58" - fake_wallet.coldkey.ss58_address = "coldkey_ss58" - - mocked_subnet_exists = mocker.patch.object( - subtensor, "subnet_exists", return_value=True - ) - mocked_get_neuron = mocker.patch.object( - subtensor, - "get_neuron_for_pubkey_and_subnet", - return_value=mocker.Mock(is_null=True), - ) - mocked_create_pow = mocker.patch.object( - async_registration, - "create_pow_async", - return_value=mocker.Mock( - is_stale_async=mocker.AsyncMock(return_value=False), seal=[] - ), - ) - mocked_compose_call = mocker.patch.object(subtensor, "compose_call") - mocked_sign_and_send_extrinsic = mocker.patch.object( - subtensor, "sign_and_send_extrinsic", return_value=ExtrinsicResponse(True, "") - ) - mocked_is_hotkey_registered = mocker.patch.object( - subtensor, "is_hotkey_registered", return_value=True - ) - - # Call - result = await async_registration.register_extrinsic( - subtensor=subtensor, - wallet=fake_wallet, - netuid=1, - wait_for_inclusion=True, - wait_for_finalization=True, - ) - - # Asserts - mocked_subnet_exists.assert_called_once_with( - 1, - block_hash=subtensor.substrate.get_chain_head.return_value, - ) - mocked_get_neuron.assert_called_once_with( - hotkey_ss58="hotkey_ss58", - netuid=1, - block_hash=subtensor.substrate.get_chain_head.return_value, - ) - mocked_create_pow.assert_called_once() - mocked_sign_and_send_extrinsic.assert_called_once_with( - call=mocked_compose_call.return_value, - wallet=fake_wallet, - wait_for_inclusion=True, - wait_for_finalization=True, - period=None, - raise_error=False, - ) - mocked_is_hotkey_registered.assert_called_once_with( - netuid=1, hotkey_ss58="hotkey_ss58" - ) - - assert result[0] - - -@pytest.mark.asyncio -async def test_register_extrinsic_success_with_cuda(subtensor, fake_wallet, mocker): - """Tests successful registration with CUDA enabled.""" - # Preps - fake_wallet.hotkey.ss58_address = "hotkey_ss58" - fake_wallet.coldkey.ss58_address = "coldkey_ss58" - - mocked_subnet_exists = mocker.patch.object( - subtensor, "subnet_exists", return_value=True - ) - mocked_get_neuron = mocker.patch.object( - subtensor, - "get_neuron_for_pubkey_and_subnet", - return_value=mocker.Mock(is_null=True), - ) - mocker.patch("torch.cuda.is_available", return_value=True) - mocked_create_pow = mocker.patch.object( - async_registration, - "create_pow_async", - return_value=mocker.Mock( - is_stale_async=mocker.AsyncMock(return_value=False), seal=[] - ), - ) - mocked_compose_call = mocker.patch.object(subtensor, "compose_call") - mocked_sign_and_send_extrinsic = mocker.patch.object( - subtensor, "sign_and_send_extrinsic", return_value=ExtrinsicResponse(True, "") - ) - mocked_is_hotkey_registered = mocker.patch.object( - subtensor, "is_hotkey_registered", return_value=True - ) - - # Call - result = await async_registration.register_extrinsic( - subtensor=subtensor, - wallet=fake_wallet, - netuid=1, - cuda=True, - wait_for_inclusion=True, - wait_for_finalization=True, - ) - - # Asserts - mocked_subnet_exists.assert_called_once_with( - 1, - block_hash=subtensor.substrate.get_chain_head.return_value, - ) - mocked_get_neuron.assert_called_once_with( - hotkey_ss58="hotkey_ss58", - netuid=1, - block_hash=subtensor.substrate.get_chain_head.return_value, - ) - mocked_create_pow.assert_called_once() - mocked_sign_and_send_extrinsic.assert_called_once_with( - call=mocked_compose_call.return_value, - wallet=fake_wallet, - wait_for_inclusion=True, - wait_for_finalization=True, - period=None, - raise_error=False, - ) - mocked_is_hotkey_registered.assert_called_once_with( - netuid=1, hotkey_ss58="hotkey_ss58" - ) - assert result[0] - - -@pytest.mark.asyncio -async def test_register_extrinsic_failed_with_cuda(subtensor, fake_wallet, mocker): - """Tests failed registration with CUDA enabled.""" - # Preps - fake_wallet.hotkey.ss58_address = "hotkey_ss58" - fake_wallet.coldkey.ss58_address = "coldkey_ss58" - - mocked_subnet_exists = mocker.patch.object( - subtensor, "subnet_exists", return_value=True - ) - mocked_get_neuron = mocker.patch.object( - subtensor, - "get_neuron_for_pubkey_and_subnet", - return_value=mocker.Mock(is_null=True), - ) - mocker.patch("torch.cuda.is_available", return_value=False) - - # Call - result = await async_registration.register_extrinsic( - subtensor=subtensor, - wallet=fake_wallet, - netuid=1, - cuda=True, - wait_for_inclusion=True, - wait_for_finalization=True, - ) - - # Asserts - mocked_subnet_exists.assert_called_once_with( - 1, - block_hash=subtensor.substrate.get_chain_head.return_value, - ) - mocked_get_neuron.assert_called_once_with( - hotkey_ss58="hotkey_ss58", - netuid=1, - block_hash=subtensor.substrate.get_chain_head.return_value, - ) - assert result == ExtrinsicResponse( - False, - "CUDA not available.", - extrinsic_function="register_extrinsic", - ) - - -@pytest.mark.asyncio -async def test_register_extrinsic_subnet_not_exists(subtensor, fake_wallet, mocker): - """Tests registration when subnet does not exist.""" - # Preps - netuid = 14 - mocked_subnet_exists = mocker.patch.object( - subtensor, "subnet_exists", return_value=False - ) - - # Call - result = await async_registration.register_extrinsic( - subtensor=subtensor, - wallet=fake_wallet, - netuid=netuid, - ) - - # Asserts - mocked_subnet_exists.assert_called_once_with( - netuid, - block_hash=subtensor.substrate.get_chain_head.return_value, - ) - assert result == ExtrinsicResponse( - False, - f"Subnet {netuid} does not exist.", - extrinsic_function="register_extrinsic", - ) - - -@pytest.mark.asyncio -async def test_register_extrinsic_already_registered(subtensor, fake_wallet, mocker): - """Tests registration when the key is already registered.""" - # Preps - netuid = 14 - mocked_get_neuron = mocker.patch.object( - subtensor, - "get_neuron_for_pubkey_and_subnet", - return_value=mocker.Mock(is_null=False), - ) - - # Call - success, message = await async_registration.register_extrinsic( - subtensor=subtensor, - wallet=fake_wallet, - netuid=netuid, - ) - - # Asserts - mocked_get_neuron.assert_called_once_with( - hotkey_ss58=fake_wallet.hotkey.ss58_address, - netuid=netuid, - block_hash=subtensor.substrate.get_chain_head.return_value, - ) - assert success is True - assert message == "Already registered." - - -@pytest.mark.asyncio -async def test_register_extrinsic_max_attempts_reached(subtensor, fake_wallet, mocker): - # Preps - fake_wallet.hotkey.ss58_address = "hotkey_ss58" - fake_wallet.coldkey.ss58_address = "coldkey_ss58" - - stale_responses = iter([False, False, False, True]) - - async def is_stale_side_effect(*_, **__): - return next(stale_responses, True) - - fake_pow_result = mocker.Mock() - fake_pow_result.is_stale_async = mocker.AsyncMock(side_effect=is_stale_side_effect) - fake_pow_result.seal = [] - - mocked_subnet_exists = mocker.patch.object( - subtensor, "subnet_exists", return_value=True - ) - mocked_get_neuron = mocker.patch.object( - subtensor, - "get_neuron_for_pubkey_and_subnet", - return_value=mocker.Mock(is_null=True), - ) - mocked_create_pow = mocker.patch.object( - async_registration, - "create_pow_async", - return_value=fake_pow_result, - ) - mocked_compose_call = mocker.patch.object(subtensor, "compose_call") - mocked_sign_and_send_extrinsic = mocker.patch.object( - subtensor, - "sign_and_send_extrinsic", - return_value=ExtrinsicResponse(False, "Test Error"), - ) - - # Call - result = await async_registration.register_extrinsic( - subtensor=subtensor, - wallet=fake_wallet, - netuid=1, - max_allowed_attempts=3, - wait_for_inclusion=True, - wait_for_finalization=True, - ) - - # Asserts - mocked_subnet_exists.assert_called_once_with( - 1, - block_hash=subtensor.substrate.get_chain_head.return_value, - ) - mocked_get_neuron.assert_called_once_with( - hotkey_ss58="hotkey_ss58", - netuid=1, - block_hash=subtensor.substrate.get_chain_head.return_value, - ) - assert mocked_create_pow.call_count == 3 - assert mocked_sign_and_send_extrinsic.call_count == 3 - mocked_sign_and_send_extrinsic.assert_called_with( - call=mocked_compose_call.return_value, - wallet=fake_wallet, - wait_for_inclusion=True, - wait_for_finalization=True, - period=None, - raise_error=False, - ) - assert result[0] is False - assert result[1] == "No more attempts." - - @pytest.mark.asyncio async def test_set_subnet_identity_extrinsic_is_success(subtensor, fake_wallet, mocker): """Verify that set_subnet_identity_extrinsic calls the correct functions and returns the correct result.""" diff --git a/tests/unit_tests/extrinsics/test_registration.py b/tests/unit_tests/extrinsics/test_registration.py index 0ffb73c5fe..631b46b6e6 100644 --- a/tests/unit_tests/extrinsics/test_registration.py +++ b/tests/unit_tests/extrinsics/test_registration.py @@ -4,7 +4,6 @@ from bittensor.core.extrinsics import registration from bittensor.core.subtensor import Subtensor from bittensor.utils.balance import Balance -from bittensor.utils.registration import POWSolution # Mocking external dependencies @@ -26,171 +25,6 @@ def mock_wallet(mocker): return mock -@pytest.fixture -def mock_pow_solution(mocker): - mock = mocker.MagicMock(spec=POWSolution) - mock.block_number = 123 - mock.nonce = 456 - mock.seal = [0, 1, 2, 3] - mock.is_stale.return_value = False - return mock - - -@pytest.fixture -def mock_new_wallet(mocker): - mock = mocker.MagicMock(spec=Wallet) - mock.coldkeypub.ss58_address = "mock_address" - mock.coldkey = mocker.MagicMock() - mock.hotkey = mocker.MagicMock() - return mock - - -@pytest.mark.parametrize( - "subnet_exists, neuron_is_null, cuda_available, expected_result, expected_message", - [ - ( - False, - True, - True, - False, - "Subnet 123 does not exist.", - ), - (True, False, True, True, "Already registered."), - (True, True, False, False, "CUDA not available."), - ], - ids=["subnet-does-not-exist", "neuron-already-registered", "cuda-unavailable"], -) -def test_register_extrinsic_without_pow( - mock_subtensor, - mock_wallet, - mocker, - subnet_exists, - neuron_is_null, - cuda_available, - expected_result, - expected_message, -): - # Arrange - mocker.patch.object(mock_subtensor, "subnet_exists", return_value=subnet_exists) - fake_neuron = mocker.patch.object( - mock_subtensor, - "get_neuron_for_pubkey_and_subnet", - return_value=mocker.MagicMock(is_null=neuron_is_null), - ) - mocker.patch("torch.cuda.is_available", return_value=cuda_available) - mocker.patch( - "bittensor.utils.registration.pow._get_block_with_retry", - return_value=(0, 0, "00ff11ee"), - ) - - # Act - result = registration.register_extrinsic( - subtensor=mock_subtensor, - wallet=mock_wallet, - netuid=123, - wait_for_inclusion=True, - wait_for_finalization=True, - max_allowed_attempts=3, - output_in_place=True, - cuda=True, - dev_id=0, - tpb=256, - num_processes=None, - update_interval=None, - log_verbose=False, - ) - - # Assert - data = ( - {"neuron": fake_neuron.return_value} - if fake_neuron.call_count > 0 and cuda_available - else None - ) - expected_result = ExtrinsicResponse( - expected_result, - expected_message, - extrinsic_function="register_extrinsic", - data=data, - ) - assert result == expected_result - - -@pytest.mark.parametrize( - "pow_success, pow_stale, registration_success, cuda, hotkey_registered, expected_result", - [ - (True, False, True, False, False, True), - (True, False, True, True, False, True), - # Pow failed but key was registered already - (False, False, False, False, True, True), - # Pow was a success but registration failed with error 'key already registered' - (True, False, False, False, False, False), - ], - ids=[ - "successful-with-valid-pow", - "successful-with-valid-cuda-pow", - "hotkey-registered", - "registration-fail-key-registered", - ], -) -def test_register_extrinsic_with_pow( - mock_subtensor, - mock_wallet, - mock_pow_solution, - pow_success, - pow_stale, - registration_success, - cuda, - hotkey_registered, - expected_result, - mocker, -): - # Arrange - mocker.patch( - "bittensor.utils.registration.pow._solve_for_difficulty_fast", - return_value=mock_pow_solution if pow_success else None, - ) - mocker.patch( - "bittensor.utils.registration.pow._solve_for_difficulty_fast_cuda", - return_value=mock_pow_solution if pow_success else None, - ) - mocker.patch.object( - mock_subtensor, - "sign_and_send_extrinsic", - return_value=ExtrinsicResponse( - registration_success, "HotKeyAlreadyRegisteredInSubNet" - ), - ) - mocker.patch("torch.cuda.is_available", return_value=cuda) - - # Act - if pow_success: - mock_pow_solution.is_stale.return_value = pow_stale - - if not pow_success and hotkey_registered: - mock_subtensor.is_hotkey_registered = mocker.MagicMock( - return_value=hotkey_registered - ) - - result = registration.register_extrinsic( - subtensor=mock_subtensor, - wallet=mock_wallet, - netuid=123, - wait_for_inclusion=True, - wait_for_finalization=True, - max_allowed_attempts=3, - output_in_place=True, - cuda=cuda, - dev_id=0, - tpb=256, - num_processes=None, - update_interval=None, - log_verbose=False, - ) - - # Assert - assert result[0] is expected_result - - @pytest.mark.parametrize( "subnet_exists, neuron_is_null, recycle_success, is_registered, expected_result, test_id", [ diff --git a/tests/unit_tests/test_async_subtensor.py b/tests/unit_tests/test_async_subtensor.py index 6ab924869d..aba3085fc2 100644 --- a/tests/unit_tests/test_async_subtensor.py +++ b/tests/unit_tests/test_async_subtensor.py @@ -2504,39 +2504,53 @@ async def test_transfer_success(subtensor, fake_wallet, mocker): @pytest.mark.asyncio async def test_register_success(subtensor, fake_wallet, mocker): - """Tests register when there is enough balance and registration succeeds.""" + """Tests register with auto-calculated limit_price from recycle.""" # Preps fake_netuid = 1 - mocked_register_extrinsic = mocker.AsyncMock() + mocked_register_limit_extrinsic = mocker.AsyncMock() mocker.patch.object( - async_subtensor, "register_extrinsic", mocked_register_extrinsic + async_subtensor, "register_limit_extrinsic", mocked_register_limit_extrinsic + ) + mocker.patch.object( + subtensor, "recycle", return_value=Balance.from_rao(1_000_000_000) ) # Call result = await subtensor.register(wallet=fake_wallet, netuid=fake_netuid) # Asserts - mocked_register_extrinsic.assert_awaited_once_with( + mocked_register_limit_extrinsic.assert_awaited_once_with( + subtensor=subtensor, wallet=fake_wallet, - cuda=False, - dev_id=0, - log_verbose=False, - max_allowed_attempts=3, netuid=1, - num_processes=None, - output_in_place=False, - subtensor=subtensor, - tpb=256, - update_interval=None, + limit_price=Balance.from_rao(1_005_000_000), mev_protection=DEFAULT_MEV_PROTECTION, period=DEFAULT_PERIOD, raise_error=False, - wait_for_finalization=True, wait_for_inclusion=True, + wait_for_finalization=True, wait_for_revealed_execution=True, ) - assert result == mocked_register_extrinsic.return_value + assert result == mocked_register_limit_extrinsic.return_value + + +@pytest.mark.asyncio +async def test_register_on_root(mock_substrate, subtensor, fake_wallet, mocker): + mock_substrate.submit_extrinsic.return_value = mocker.AsyncMock( + is_success=mocker.AsyncMock(return_value=True)(), + ) + mocked_root_register_extrinsic = mocker.patch.object( + async_subtensor, + "root_register_extrinsic", + ) + + response = await subtensor.register( + wallet=fake_wallet, + netuid=0, + ) + + assert response == mocked_root_register_extrinsic.return_value @pytest.mark.asyncio diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index d08f4c41fa..0b19bbfec5 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -6293,3 +6293,51 @@ def test_dispute_coldkey_swap(mocker, subtensor): wait_for_revealed_execution=True, ) assert response == mocked_dispute_coldkey_swap_extrinsic.return_value + + +def test_register_success(subtensor, fake_wallet, mocker): + """Tests register with auto-calculated limit_price from recycle.""" + # Preps + fake_netuid = 1 + + mocked_register_limit_extrinsic = mocker.patch.object( + subtensor_module, "register_limit_extrinsic" + ) + mocker.patch.object( + subtensor, "recycle", return_value=Balance.from_rao(1_000_000_000) + ) + + # Call + result = subtensor.register(wallet=fake_wallet, netuid=fake_netuid) + + # Asserts + mocked_register_limit_extrinsic.assert_called_once_with( + subtensor=subtensor, + wallet=fake_wallet, + netuid=1, + limit_price=Balance.from_rao(1_005_000_000), + mev_protection=DEFAULT_MEV_PROTECTION, + period=DEFAULT_PERIOD, + raise_error=False, + wait_for_inclusion=True, + wait_for_finalization=True, + wait_for_revealed_execution=True, + ) + assert result == mocked_register_limit_extrinsic.return_value + + +def test_register_on_root(mock_substrate, subtensor, fake_wallet, mocker): + mock_substrate.submit_extrinsic.return_value = MagicMock( + is_success=True, + ) + mocked_root_register_extrinsic = mocker.patch.object( + subtensor_module, + "root_register_extrinsic", + ) + + response = subtensor.register( + wallet=fake_wallet, + netuid=0, + ) + + assert response == mocked_root_register_extrinsic.return_value diff --git a/tests/unit_tests/utils/test_registration.py b/tests/unit_tests/utils/test_registration.py index a4ec066279..4b9f6ab3eb 100644 --- a/tests/unit_tests/utils/test_registration.py +++ b/tests/unit_tests/utils/test_registration.py @@ -14,7 +14,7 @@ def error(self, message): @pytest.fixture def mock_bittensor_logging(monkeypatch): mock_logger = MockBittensorLogging() - monkeypatch.setattr("bittensor.utils.registration.pow.logging", mock_logger) + monkeypatch.setattr("bittensor.utils.registration.torch_utils.logging", mock_logger) return mock_logger @@ -32,7 +32,7 @@ def test_lazy_loaded_torch__torch_installed(monkeypatch, mock_bittensor_logging) def test_lazy_loaded_torch__no_torch(monkeypatch, mock_bittensor_logging): monkeypatch.setattr( - "bittensor.utils.registration.pow._get_real_torch", lambda: None + "bittensor.utils.registration.torch_utils._get_real_torch", lambda: None ) torch = LazyLoadedTorch() From d25c6c639b6bb33fbca83ca6d027f84aedb00b5b Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Thu, 23 Apr 2026 15:56:56 -0700 Subject: [PATCH 111/118] e2e tests --- tests/e2e_tests/test_metagraph.py | 2 +- tests/e2e_tests/test_register_limit.py | 37 ++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/tests/e2e_tests/test_metagraph.py b/tests/e2e_tests/test_metagraph.py index 0247f11d7d..b5d58bb37c 100644 --- a/tests/e2e_tests/test_metagraph.py +++ b/tests/e2e_tests/test_metagraph.py @@ -13,7 +13,7 @@ ) from bittensor.utils.balance import Balance from bittensor.utils.btlogging import logging -from bittensor.utils.registration.pow import LazyLoadedTorch +from bittensor.utils.registration.torch_utils import LazyLoadedTorch from tests.e2e_tests.utils import ( AdminUtils, NETUID, diff --git a/tests/e2e_tests/test_register_limit.py b/tests/e2e_tests/test_register_limit.py index adc8ad8234..cfcb8b4090 100644 --- a/tests/e2e_tests/test_register_limit.py +++ b/tests/e2e_tests/test_register_limit.py @@ -103,3 +103,40 @@ async def test_register_limit_price_exceeded_async( assert not result.success, ( "register_limit should fail with limit_price=1 (below burn)" ) + + +def test_register_auto_limit_price(subtensor, alice_wallet, bob_wallet): + """Tests successful registration via register() with auto-calculated limit_price.""" + alice_sn = TestSubnet(subtensor) + alice_sn.execute_steps( + [ + REGISTER_SUBNET(alice_wallet), + ACTIVATE_SUBNET(alice_wallet), + ] + ) + + logging.console.info( + f"Registering Bob with register (auto limit_price) on SN #{alice_sn.netuid}" + ) + result = subtensor.subnets.register(bob_wallet, alice_sn.netuid) + assert result.success, "register should succeed with auto-calculated limit_price" + + +@pytest.mark.asyncio +async def test_register_auto_limit_price_async( + async_subtensor, alice_wallet, bob_wallet +): + """Tests successful async registration via register() with auto-calculated limit_price.""" + alice_sn = TestSubnet(async_subtensor) + await alice_sn.async_execute_steps( + [ + REGISTER_SUBNET(alice_wallet), + ACTIVATE_SUBNET(alice_wallet), + ] + ) + + logging.console.info( + f"Registering Bob with register (auto limit_price) on SN #{alice_sn.netuid}" + ) + result = await async_subtensor.subnets.register(bob_wallet, alice_sn.netuid) + assert result.success, "register should succeed with auto-calculated limit_price" From abdfd0e8a055d0162319ddd89bc9b611cc05aadb Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Thu, 23 Apr 2026 15:57:19 -0700 Subject: [PATCH 112/118] move torch stuff --- bittensor/utils/registration/torch_utils.py | 82 +++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 bittensor/utils/registration/torch_utils.py diff --git a/bittensor/utils/registration/torch_utils.py b/bittensor/utils/registration/torch_utils.py new file mode 100644 index 0000000000..38fd9131a1 --- /dev/null +++ b/bittensor/utils/registration/torch_utils.py @@ -0,0 +1,82 @@ +"""Torch compatibility utilities for Bittensor.""" + +import functools +import os +from typing import TYPE_CHECKING + +import numpy + +from bittensor.utils.btlogging import logging + + +def use_torch() -> bool: + """Force the use of torch over numpy for certain operations.""" + return True if os.getenv("USE_TORCH") == "1" else False + + +def legacy_torch_api_compat(func): + """ + Convert function operating on numpy Input&Output to legacy torch Input&Output API if `use_torch()` is True. + + Parameters: + func: Function with numpy Input/Output to be decorated. + + Returns: + decorated: Decorated function. + """ + + @functools.wraps(func) + def decorated(*args, **kwargs): + if use_torch(): + args = [ + arg.cpu().numpy() if isinstance(arg, torch.Tensor) else arg + for arg in args + ] + kwargs = { + key: value.cpu().numpy() if isinstance(value, torch.Tensor) else value + for key, value in kwargs.items() + } + ret = func(*args, **kwargs) + if use_torch(): + if isinstance(ret, numpy.ndarray): + ret = torch.from_numpy(ret) + return ret + + return decorated + + +@functools.cache +def _get_real_torch(): + try: + import torch as _real_torch + except ImportError: + _real_torch = None + return _real_torch + + +def log_no_torch_error(): + logging.error( + "This command requires torch. You can install torch for bittensor" + ' with `pip install bittensor[torch]` or `pip install ".[torch]"`' + " if installing from source, and then run the command with USE_TORCH=1 {command}" + ) + + +class LazyLoadedTorch: + """A lazy-loading proxy for the torch module.""" + + def __bool__(self): + return bool(_get_real_torch()) + + def __getattr__(self, name): + if real_torch := _get_real_torch(): + return getattr(real_torch, name) + else: + log_no_torch_error() + raise ImportError("torch not installed") + + +if TYPE_CHECKING: + import torch +else: + torch = LazyLoadedTorch() From 9ef6bae0c30d004892cb276efa7be70113f0ab70 Mon Sep 17 00:00:00 2001 From: Mirian Okradze Date: Fri, 24 Apr 2026 11:30:32 +0400 Subject: [PATCH 113/118] hoist get_required_fields out of to_headers --- bittensor/core/synapse.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/bittensor/core/synapse.py b/bittensor/core/synapse.py index c29629a144..99b7af3ae8 100644 --- a/bittensor/core/synapse.py +++ b/bittensor/core/synapse.py @@ -638,16 +638,15 @@ def to_headers(self) -> dict: # Getting the fields of the instance instance_fields = self.model_dump() + required = self.get_required_fields() # Iterating over the fields of the instance for field, value in instance_fields.items(): - # If the object is not optional, serializing it, encoding it, and adding it to the headers - required = self.get_required_fields() - # Skipping the field if it's already in the headers or its value is None if field in headers or value is None: continue + # If the object is not optional, serializing it, encoding it, and adding it to the headers elif required and field in required: try: # create an empty (dummy) instance of type(value) to pass pydantic validation on the axon side From f40dff8627d6691f492d5c93f0873e6e7b0a62c3 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Fri, 24 Apr 2026 09:43:59 +0200 Subject: [PATCH 114/118] Bump ASI + cyscale --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f3b43e5718..0bdcefb111 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,11 +29,11 @@ dependencies = [ "retry==0.9.2", "requests>=2.33.0,<3.0", "pydantic>=2.3,<3", - "cyscale==0.3.0", + "cyscale>=0.3.1,<1.0.0", "uvicorn", "bittensor-drand>=1.3.0,<2.0.0", "bittensor-wallet==4.0.1", - "async-substrate-interface==2.0.1", + "async-substrate-interface==2.0.2", ] [project.optional-dependencies] From fa252add21caa2e71499880dfe19093361c1b355 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Fri, 24 Apr 2026 09:46:35 +0200 Subject: [PATCH 115/118] Adds CODEOWNERS file --- CODEOWNERS | 1 + 1 file changed, 1 insertion(+) create mode 100644 CODEOWNERS diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000000..b752bf1c02 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1 @@ +* @basfroman @thewhaleking @ibraheem-abe \ No newline at end of file From 3235533ce8483b59cd707e783c96b33a44c3f893 Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Fri, 24 Apr 2026 11:14:00 -0700 Subject: [PATCH 116/118] bumping deps versions --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 98b510a305..dbe282c4f1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,7 +62,7 @@ torch = [ "torch>=1.13.1,<3.0" ] cli = [ - "bittensor-cli>=9.21.0" + "bittensor-cli>=9.21.0rc1" ] From 6341fe39e06266c8f2f7f48a8b9409027e11974f Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Fri, 24 Apr 2026 11:14:13 -0700 Subject: [PATCH 117/118] update changelog --- CHANGELOG.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 663f9f7ba3..28c8b9c920 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,11 +18,18 @@ * Removes flake8, uses ruff for linter check by @thewhaleking in https://github.com/latent-to/bittensor/pull/3313 * Adds note to httpx in dev reqs by @thewhaleking in https://github.com/latent-to/bittensor/pull/3315 * Add `register_limit` extrinsic by @basfroman in https://github.com/latent-to/bittensor/pull/3316 +* ASI 2.0 & cyscale by @thewhaleking in https://github.com/latent-to/bittensor/pull/3314 +* Bump ASI req by @thewhaleking in https://github.com/latent-to/bittensor/pull/3324 +* Remove PoW registration, refactor `register()` to use `register_limit` by @basfroman in https://github.com/latent-to/bittensor/pull/3325 +* hoist get_required_fields out of to_headers by @okradze in https://github.com/latent-to/bittensor/pull/3323 +* Adds CODEOWNERS file by @thewhaleking in https://github.com/latent-to/bittensor/pull/3327 +* Bump ASI + cyscale by @thewhaleking in https://github.com/latent-to/bittensor/pull/3326 ## New Contributors * @ionodeionode made their first contribution in https://github.com/latent-to/bittensor/pull/3270 +* @okradze made their first contribution in https://github.com/latent-to/bittensor/pull/3323 -**Full Changelog**: https://github.com/latent-to/bittensor/compare/v10.2.0...v10.3.0 +**Full Changelog**: https://github.com/latent-to/bittensor/compare/v10.2.1...v10.3.0 ## 10.2.0 /2026-03-19 From b9302ca5edecf09d6346de99356b74df1223dd34 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Tue, 28 Apr 2026 17:23:45 +0200 Subject: [PATCH 118/118] Bumps ruff to latest --- .github/workflows/ruff.yml | 4 ++-- pyproject.toml | 2 +- tests/unit_tests/test_subtensor.py | 7 +++---- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml index 4990b1e391..d990f61cd2 100644 --- a/.github/workflows/ruff.yml +++ b/.github/workflows/ruff.yml @@ -17,13 +17,13 @@ jobs: - name: Ruff format check uses: astral-sh/ruff-action@v4.0.0 with: - version: "0.11.5" + version: "0.15.12" args: "format --diff" src: "bittensor tests" - name: Ruff linter check uses: astral-sh/ruff-action@v4.0.0 with: - version: "0.11.5" + version: "0.15.12" args: "check" src: "bittensor" diff --git a/pyproject.toml b/pyproject.toml index dbe282c4f1..395473a898 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,7 +53,7 @@ dev = [ "typing_extensions>= 4.0.0; python_version<'3.11'", "freezegun==1.5.0", "httpx==0.27.0", # used by tests/unit_tests/test_axon: The starlette.testclient module requires the httpx package to be installed. - "ruff==0.11.5", + "ruff==0.15.12", "aioresponses==0.7.6", "factory-boy==3.3.0", "types-requests", diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index 0b19bbfec5..d2324d435d 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -3660,10 +3660,9 @@ def test_get_liquidity_list_happy_path(subtensor, fake_wallet, mocker): mocker.patch.object( subtensor.substrate, "create_storage_key", - side_effect=lambda pallet, - storage_function, - params, - block_hash=None: f"{pallet}:{storage_function}:{params}", + side_effect=lambda pallet, storage_function, params, block_hash=None: ( + f"{pallet}:{storage_function}:{params}" + ), ) # Mock query_multi for fee + sqrt_price + tick data