From cfea584ee1770ad775f349196d8ea44540ecfd7c Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Mon, 20 Oct 2025 15:43:57 -0700 Subject: [PATCH 1/3] rename `max_retries` to `max_attempts` --- bittensor/core/async_subtensor.py | 51 ++++++++++++++++++------------- bittensor/core/subtensor.py | 51 ++++++++++++++++++------------- tests/e2e_tests/utils/__init__.py | 10 +++--- 3 files changed, 66 insertions(+), 46 deletions(-) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index c690dbee43..350fc01d52 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -122,6 +122,7 @@ is_valid_ss58_address, u16_normalized_float, u64_normalized_float, + validate_max_attempts, ) from bittensor.utils.balance import ( Balance, @@ -5098,7 +5099,7 @@ async def commit_weights( weights: Weights, mechid: int = 0, version_key: int = version_as_int, - max_retries: int = 5, + max_attempts: int = 5, period: Optional[int] = 16, raise_error: bool = True, wait_for_inclusion: bool = False, @@ -5116,7 +5117,7 @@ async def commit_weights( weights: NumPy array of weight values corresponding to each UID. mechid: The subnet mechanism unique identifier. version_key: Version key for compatibility with the network. - max_retries: The number of maximum attempts to commit weights. + max_attempts: The number of maximum attempts to commit weights. 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. @@ -5133,16 +5134,19 @@ async def commit_weights( Notes: See also: , """ - retries = 0 + attempt = 0 response = ExtrinsicResponse(False) + if attempt_check := validate_max_attempts(max_attempts, response): + return attempt_check + logging.debug( f"Committing weights with params: " f"netuid=[blue]{netuid}[/blue], uids=[blue]{uids}[/blue], weights=[blue]{weights}[/blue], " f"version_key=[blue]{version_key}[/blue]" ) - while retries < max_retries and response.success is False: + while attempt < max_attempts and response.success is False: try: response = await commit_weights_extrinsic( subtensor=self, @@ -5161,7 +5165,7 @@ async def commit_weights( return ExtrinsicResponse.from_exception( raise_error=raise_error, error=error ) - retries += 1 + attempt += 1 if not response.success: logging.debug( @@ -5655,7 +5659,7 @@ async def reveal_weights( weights: Weights, salt: Salt, mechid: int = 0, - max_retries: int = 5, + max_attempts: int = 5, version_key: int = version_as_int, period: Optional[int] = 16, raise_error: bool = False, @@ -5673,7 +5677,7 @@ async def reveal_weights( weights: NumPy array of weight values corresponding to each UID. salt: NumPy array of salt values corresponding to the hash function. mechid: The subnet mechanism unique identifier. - max_retries: The number of maximum attempts to reveal weights. + max_attempts: The number of maximum attempts to reveal weights. version_key: Version key for compatibility with the network. 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 @@ -5690,10 +5694,13 @@ async def reveal_weights( See also: , """ - retries = 0 + attempt = 0 response = ExtrinsicResponse(False) - while retries < max_retries and response.success is False: + if attempt_check := validate_max_attempts(max_attempts, response): + return attempt_check + + while attempt < max_attempts and response.success is False: try: response = await reveal_weights_extrinsic( subtensor=self, @@ -5713,7 +5720,7 @@ async def reveal_weights( return ExtrinsicResponse.from_exception( raise_error=raise_error, error=error ) - retries += 1 + attempt += 1 if not response.success: logging.debug("No attempt made. Perhaps it is too soon to reveal weights!") @@ -6010,7 +6017,7 @@ async def set_weights( mechid: int = 0, block_time: float = 12.0, commit_reveal_version: int = 4, - max_retries: int = 5, + max_attempts: int = 5, version_key: int = version_as_int, period: Optional[int] = 8, raise_error: bool = False, @@ -6034,7 +6041,7 @@ async def set_weights( mechid: The subnet mechanism unique identifier. block_time: The number of seconds for block duration. commit_reveal_version: The version of the chain commit-reveal protocol to use. - max_retries: The number of maximum attempts to set weights. + max_attempts: The number of maximum attempts to set weights. version_key: Version key for compatibility with the network. 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 @@ -6052,6 +6059,11 @@ async def set_weights( Notes: See """ + attempt = 0 + response = ExtrinsicResponse(False) + + if attempt_check := validate_max_attempts(max_attempts, response): + return attempt_check async def _blocks_weight_limit() -> bool: bslu, wrl = await asyncio.gather( @@ -6060,9 +6072,6 @@ async def _blocks_weight_limit() -> bool: ) return bslu > wrl - retries = 0 - response = ExtrinsicResponse(False) - if ( uid := await self.get_uid_for_hotkey_on_subnet( wallet.hotkey.ss58_address, netuid @@ -6077,13 +6086,13 @@ async def _blocks_weight_limit() -> bool: # go with `commit_timelocked_mechanism_weights_extrinsic` extrinsic while ( - retries < max_retries + attempt < max_attempts and response.success is False and await _blocks_weight_limit() ): logging.debug( f"Committing weights {weights} for subnet [blue]{netuid}[/blue]. " - f"Attempt [blue]{retries + 1}[blue] of [green]{max_retries}[/green]." + f"Attempt [blue]{attempt + 1}[blue] of [green]{max_attempts}[/green]." ) try: response = await commit_timelocked_weights_extrinsic( @@ -6105,19 +6114,19 @@ async def _blocks_weight_limit() -> bool: return ExtrinsicResponse.from_exception( raise_error=raise_error, error=error ) - retries += 1 + attempt += 1 else: # go with `set_mechanism_weights_extrinsic` while ( - retries < max_retries + attempt < max_attempts and response.success is False and await _blocks_weight_limit() ): try: logging.debug( f"Setting weights for subnet #[blue]{netuid}[/blue]. " - f"Attempt [blue]{retries + 1}[/blue] of [green]{max_retries}[/green]." + f"Attempt [blue]{attempt + 1}[/blue] of [green]{max_attempts}[/green]." ) response = await set_weights_extrinsic( subtensor=self, @@ -6136,7 +6145,7 @@ async def _blocks_weight_limit() -> bool: return ExtrinsicResponse.from_exception( raise_error=raise_error, error=error ) - retries += 1 + attempt += 1 if not response.success: logging.debug( diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index b43a045149..2a1b4d317d 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -122,6 +122,7 @@ is_valid_ss58_address, u16_normalized_float, u64_normalized_float, + validate_max_attempts, ) from bittensor.utils.balance import ( Balance, @@ -3883,7 +3884,7 @@ def commit_weights( weights: Weights, mechid: int = 0, version_key: int = version_as_int, - max_retries: int = 5, + max_attempts: int = 5, period: Optional[int] = 16, raise_error: bool = True, wait_for_inclusion: bool = False, @@ -3901,7 +3902,7 @@ def commit_weights( weights: NumPy array of weight values corresponding to each UID. mechid: Subnet mechanism unique identifier. version_key: Version key for compatibility with the network. - max_retries: The number of maximum attempts to commit weights. + max_attempts: The number of maximum attempts to commit weights. 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. @@ -3915,16 +3916,19 @@ def commit_weights( This function allows neurons to create a tamper-proof record of their weight distribution at a specific point in time, enhancing transparency and accountability within the Bittensor network. """ - retries = 0 + attempt = 0 response = ExtrinsicResponse(False) + if attempt_check := validate_max_attempts(max_attempts, response): + return attempt_check + logging.debug( f"Committing weights with params: " f"netuid=[blue]{netuid}[/blue], uids=[blue]{uids}[/blue], weights=[blue]{weights}[/blue], " f"version_key=[blue]{version_key}[/blue]" ) - while retries < max_retries and response.success is False: + while attempt < max_attempts and response.success is False: try: response = commit_weights_extrinsic( subtensor=self, @@ -3943,7 +3947,7 @@ def commit_weights( return ExtrinsicResponse.from_exception( raise_error=raise_error, error=error ) - retries += 1 + attempt += 1 if not response.success: logging.debug( @@ -4437,7 +4441,7 @@ def reveal_weights( weights: Weights, salt: Salt, mechid: int = 0, - max_retries: int = 5, + max_attempts: int = 5, version_key: int = version_as_int, period: Optional[int] = 16, raise_error: bool = False, @@ -4455,7 +4459,7 @@ def reveal_weights( weights: NumPy array of weight values corresponding to each UID. salt: NumPy array of salt values corresponding to the hash function. mechid: The subnet mechanism unique identifier. - max_retries: The number of maximum attempts to reveal weights. + max_attempts: The number of maximum attempts to reveal weights. version_key: Version key for compatibility with the network. 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 @@ -4472,10 +4476,13 @@ def reveal_weights( See also: , """ - retries = 0 + attempt = 0 response = ExtrinsicResponse(False) - while retries < max_retries and response.success is False: + if attempt_check := validate_max_attempts(max_attempts, response): + return attempt_check + + while attempt < max_attempts and response.success is False: try: response = reveal_weights_extrinsic( subtensor=self, @@ -4495,7 +4502,7 @@ def reveal_weights( return ExtrinsicResponse.from_exception( raise_error=raise_error, error=error ) - retries += 1 + attempt += 1 if not response.success: logging.debug("No attempt made. Perhaps it is too soon to reveal weights!") @@ -4779,7 +4786,7 @@ def set_weights( mechid: int = 0, block_time: float = 12.0, commit_reveal_version: int = 4, - max_retries: int = 5, + max_attempts: int = 5, version_key: int = version_as_int, period: Optional[int] = 8, raise_error: bool = False, @@ -4800,7 +4807,7 @@ def set_weights( mechid: The subnet mechanism unique identifier. block_time: The number of seconds for block duration. commit_reveal_version: The version of the chain commit-reveal protocol to use. - max_retries: The number of maximum attempts to set weights. + max_attempts: The number of maximum attempts to set weights. version_key: Version key for compatibility with the network. 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 @@ -4818,15 +4825,17 @@ def set_weights( Notes: See """ + attempt = 0 + response = ExtrinsicResponse(False) + + if attempt_check := validate_max_attempts(max_attempts, response): + return attempt_check def _blocks_weight_limit() -> bool: bslu = cast(int, self.blocks_since_last_update(netuid, cast(int, uid))) wrl = cast(int, self.weights_rate_limit(netuid)) return bslu > wrl - retries = 0 - response = ExtrinsicResponse(False) - if ( uid := self.get_uid_for_hotkey_on_subnet(wallet.hotkey.ss58_address, netuid) ) is None: @@ -4839,13 +4848,13 @@ def _blocks_weight_limit() -> bool: # go with `commit_reveal_weights_extrinsic` extrinsic while ( - retries < max_retries + attempt < max_attempts and response.success is False and _blocks_weight_limit() ): logging.debug( f"Committing weights {weights} for subnet [blue]{netuid}[/blue]. " - f"Attempt [blue]{retries + 1}[blue] of [green]{max_retries}[/green]." + f"Attempt [blue]{attempt + 1}[blue] of [green]{max_attempts}[/green]." ) try: response = commit_timelocked_weights_extrinsic( @@ -4867,19 +4876,19 @@ def _blocks_weight_limit() -> bool: return ExtrinsicResponse.from_exception( raise_error=raise_error, error=error ) - retries += 1 + attempt += 1 else: # go with `set_mechanism_weights_extrinsic` while ( - retries < max_retries + attempt < max_attempts and response.success is False and _blocks_weight_limit() ): try: logging.debug( f"Setting weights for subnet [blue]{netuid}[/blue]. " - f"Attempt [blue]{retries + 1}[/blue] of [green]{max_retries}[/green]." + f"Attempt [blue]{attempt + 1}[/blue] of [green]{max_attempts}[/green]." ) response = set_weights_extrinsic( subtensor=self, @@ -4898,7 +4907,7 @@ def _blocks_weight_limit() -> bool: return ExtrinsicResponse.from_exception( raise_error=raise_error, error=error ) - retries += 1 + attempt += 1 if not response.success: logging.debug( diff --git a/tests/e2e_tests/utils/__init__.py b/tests/e2e_tests/utils/__init__.py index 7e6ba3ec3b..7ebb460fc7 100644 --- a/tests/e2e_tests/utils/__init__.py +++ b/tests/e2e_tests/utils/__init__.py @@ -19,14 +19,14 @@ def get_dynamic_balance(rao: int, netuid: int = 0): def execute_and_wait_for_next_nonce( - subtensor: "SubtensorApi", wallet, sleep=0.25, timeout=60.0, max_retries=3 + subtensor: "SubtensorApi", wallet, sleep=0.25, timeout=60.0, max_attempts=3 ): """Decorator that ensures the nonce has been consumed after a blockchain extrinsic call.""" def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): - for attempt in range(max_retries): + for attempt in range(max_attempts): start_nonce = subtensor.substrate.get_account_next_index( wallet.hotkey.ss58_address ) @@ -52,9 +52,11 @@ def wrapper(*args, **kwargs): time.sleep(sleep) logging.warning( - f"⚠️ Attempt {attempt + 1}/{max_retries}: Nonce did not increment." + f"⚠️ Attempt {attempt + 1}/{max_attempts}: Nonce did not increment." ) - raise TimeoutError(f"❌ Nonce did not change after {max_retries} attempts.") + raise TimeoutError( + f"❌ Nonce did not change after {max_attempts} attempts." + ) return wrapper From fd262dfcffc696f139505fac728ba461343b4bb0 Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Mon, 20 Oct 2025 15:44:09 -0700 Subject: [PATCH 2/3] add `validate_max_attempts` --- bittensor/utils/__init__.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/bittensor/utils/__init__.py b/bittensor/utils/__init__.py index c4ce11e924..0cc4b7114b 100644 --- a/bittensor/utils/__init__.py +++ b/bittensor/utils/__init__.py @@ -13,6 +13,7 @@ ) from bittensor_wallet import Keypair from bittensor_wallet.errors import KeyFileError, PasswordError +from bittensor_wallet.utils import SS58_FORMAT from scalecodec import ( ss58_decode, ss58_encode, @@ -20,13 +21,13 @@ ) from bittensor.core import settings -from bittensor_wallet.utils import SS58_FORMAT from bittensor.utils.btlogging import logging 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.utils.balance import Balance # keep save from import analyzer as obvious aliases @@ -498,3 +499,16 @@ def get_caller_name(depth: int = 2) -> str: if frame is not None: frame = frame.f_back return frame.f_code.co_name if frame else "unknown" + + +def validate_max_attempts( + max_attempts: int, response: "ExtrinsicResponse" +) -> Optional["ExtrinsicResponse"]: + """Common guard for all subtensor methods with max_attempts parameter.""" + if max_attempts <= 0: + response.message = ( + f"`max_attempts` parameter must be greater than 0, not {max_attempts}." + ) + response.error = ValueError(response.message) + return response.with_log("warning") + return None From 48daa4c67049d9b03a303b692b277953d4624bc6 Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Mon, 20 Oct 2025 15:44:19 -0700 Subject: [PATCH 3/3] update tests and add new one --- tests/e2e_tests/test_commit_weights.py | 6 +-- tests/e2e_tests/test_set_weights.py | 6 +-- tests/unit_tests/test_async_subtensor.py | 47 ++++++++++++++++++++++++ tests/unit_tests/test_subtensor.py | 46 +++++++++++++++++++++++ 4 files changed, 99 insertions(+), 6 deletions(-) diff --git a/tests/e2e_tests/test_commit_weights.py b/tests/e2e_tests/test_commit_weights.py index 48e79383fb..0d488eedf0 100644 --- a/tests/e2e_tests/test_commit_weights.py +++ b/tests/e2e_tests/test_commit_weights.py @@ -489,11 +489,11 @@ async def send_commit_(): ) return success_, message_ - max_retries = 3 + max_attempts = 3 timeout = 60.0 sleep = 0.25 if async_subtensor.chain.is_fast_blocks() else 12.0 - for attempt in range(1, max_retries + 1): + for attempt in range(1, max_attempts + 1): try: start_nonce = await async_subtensor.substrate.get_account_nonce( alice_wallet.hotkey.ss58_address @@ -518,7 +518,7 @@ async def send_commit_(): time.sleep(sleep) except Exception as e: raise e - raise Exception(f"Failed to commit weights after {max_retries} attempts.") + raise Exception(f"Failed to commit weights after {max_attempts} attempts.") # Send some number of commit weights AMOUNT_OF_COMMIT_WEIGHTS = 3 diff --git a/tests/e2e_tests/test_set_weights.py b/tests/e2e_tests/test_set_weights.py index 93cb3ed86b..0f6aa9240d 100644 --- a/tests/e2e_tests/test_set_weights.py +++ b/tests/e2e_tests/test_set_weights.py @@ -285,11 +285,11 @@ async def set_weights_(): ) assert success_ is True, message_ - max_retries = 3 + max_attempts = 3 timeout = 60.0 sleep = 0.25 if async_subtensor.chain.is_fast_blocks() else 12.0 - for attempt in range(1, max_retries + 1): + for attempt in range(1, max_attempts + 1): try: start_nonce = await async_subtensor.substrate.get_account_nonce( alice_wallet.hotkey.ss58_address @@ -314,7 +314,7 @@ async def set_weights_(): time.sleep(sleep) except Exception as e: raise e - raise Exception(f"Failed to commit weights after {max_retries} attempts.") + raise Exception(f"Failed to commit weights after {max_attempts} attempts.") for mechid in range(TESTED_MECHANISMS): # Set weights for each subnet diff --git a/tests/unit_tests/test_async_subtensor.py b/tests/unit_tests/test_async_subtensor.py index 5b78eca540..295222f0ca 100644 --- a/tests/unit_tests/test_async_subtensor.py +++ b/tests/unit_tests/test_async_subtensor.py @@ -4610,3 +4610,50 @@ async def test_get_crowdloans(mocker, subtensor): block_hash=mocked_determine_block_hash.return_value, ) assert result == [mocked_decode_crowdloan_entry.return_value] + + +@pytest.mark.parametrize( + "method, add_salt", + [ + ("commit_weights", True), + ("reveal_weights", True), + ("set_weights", False), + ], + ids=["commit_weights", "reveal_weights", "set_weights"], +) +@pytest.mark.asyncio +async def test_commit_weights_with_zero_max_attempts( + mocker, subtensor, caplog, method, add_salt +): + """Verify that commit_weights returns response with proper error message.""" + # Preps + wallet = mocker.Mock(spec=Wallet) + netuid = mocker.Mock(spec=int) + salt = mocker.Mock(spec=list) + uids = mocker.Mock(spec=list) + weights = mocker.Mock(spec=list) + max_attempts = 0 + expected_message = ( + f"`max_attempts` parameter must be greater than 0, not {max_attempts}." + ) + + params = { + "wallet": wallet, + "netuid": netuid, + "uids": uids, + "weights": weights, + "max_attempts": max_attempts, + } + if add_salt: + params["salt"] = salt + + # Call + # with caplog.at_level(logging.WARNING): + response = await getattr(subtensor, method)(**params) + + # Asserts + assert response.success is False + assert response.message == expected_message + assert isinstance(response.error, ValueError) + assert expected_message in str(response.error) + assert expected_message in caplog.text diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index b5f228f925..5b7d7a626d 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -4707,3 +4707,49 @@ def test_get_crowdloans(mocker, subtensor): block_hash=mocked_determine_block_hash.return_value, ) assert result == [mocked_decode_crowdloan_entry.return_value] + + +@pytest.mark.parametrize( + "method, add_salt", + [ + ("commit_weights", True), + ("reveal_weights", True), + ("set_weights", False), + ], + ids=["commit_weights", "reveal_weights", "set_weights"], +) +def test_commit_weights_with_zero_max_attempts( + mocker, subtensor, caplog, method, add_salt +): + """Verify that commit_weights returns response with proper error message.""" + # Preps + wallet = mocker.Mock(spec=Wallet) + netuid = mocker.Mock(spec=int) + salt = mocker.Mock(spec=list) + uids = mocker.Mock(spec=list) + weights = mocker.Mock(spec=list) + max_attempts = 0 + expected_message = ( + f"`max_attempts` parameter must be greater than 0, not {max_attempts}." + ) + + params = { + "wallet": wallet, + "netuid": netuid, + "uids": uids, + "weights": weights, + "max_attempts": max_attempts, + } + if add_salt: + params["salt"] = salt + + # Call + # with caplog.at_level(logging.WARNING): + response = getattr(subtensor, method)(**params) + + # Asserts + assert response.success is False + assert response.message == expected_message + assert isinstance(response.error, ValueError) + assert expected_message in str(response.error) + assert expected_message in caplog.text