diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c79478b24..3819cdad38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,27 @@ # Changelog +## 8.5.0 /2024-12-12 + +## What's Changed +* add improved reveal-round params by @JohnReedV in https://github.com/opentensor/bittensor/pull/2509 +* fix: add default value to the get_block_number method in AsyncSubstrateInterface by @FLiotta in https://github.com/opentensor/bittensor/pull/2529 +* Mismatched "archive" index by @thewhaleking in https://github.com/opentensor/bittensor/pull/2530 +* Adds a factory function to create an initialised AsyncSubtensor object. by @thewhaleking in https://github.com/opentensor/bittensor/pull/2516 +* chore: fix some comments by @lvyaoting in https://github.com/opentensor/bittensor/pull/2515 +* Fixes E2E test chain buffer issues on devnet by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2531 +* Added e2e test for CRv3 + enhancements by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2532 +* Backmerge master to staging 850 by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2535 +* Enhancement/adds total stake functions by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2537 +* Fixes get_current_block by @thewhaleking in https://github.com/opentensor/bittensor/pull/2536 +* [SDK] Add `commit reveal v3` logic (python part only) by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2484 + +## New Contributors +* @JohnReedV made their first contribution in https://github.com/opentensor/bittensor/pull/2509 +* @FLiotta made their first contribution in https://github.com/opentensor/bittensor/pull/2529 +* @lvyaoting made their first contribution in https://github.com/opentensor/bittensor/pull/2515 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v8.4.5...v8.5.0 + ## 8.4.5 /2024-12-05 ## What's Changed diff --git a/VERSION b/VERSION index 007f2e63cd..5eaed3b7ca 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.4.5 \ No newline at end of file +8.5.0 \ No newline at end of file diff --git a/bittensor/__init__.py b/bittensor/__init__.py index f4d8ee906a..b6b2f08f4b 100644 --- a/bittensor/__init__.py +++ b/bittensor/__init__.py @@ -17,11 +17,20 @@ import warnings -from .core.settings import __version__, version_split, DEFAULTS +from .core.settings import __version__, version_split, DEFAULTS, DEFAULT_NETWORK +from .core.async_subtensor import AsyncSubtensor from .utils.btlogging import logging from .utils.deprecated import * +async def async_subtensor(network: str = DEFAULT_NETWORK) -> AsyncSubtensor: + """ + Creates an initialised AsyncSubtensor object. + """ + async with AsyncSubtensor(network=network) as subtensor_: + return subtensor_ + + def __getattr__(name): if name == "version_split": warnings.warn( diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index 2ad0e7b222..23404499e3 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -236,7 +236,7 @@ async def get_current_block(self) -> int: Knowing the current block number is essential for querying real-time data and performing time-sensitive operations on the blockchain. It serves as a reference point for network activities and data synchronization. """ - return await self.substrate.get_block_number() + return await self.substrate.get_block_number(None) async def get_block_hash(self, block_id: Optional[int] = None): """ @@ -1427,6 +1427,34 @@ async def blocks_since_last_update(self, netuid: int, uid: int) -> Optional[int] call = await self.get_hyperparameter(param_name="LastUpdate", netuid=netuid) return None if call is None else await self.get_current_block() - int(call[uid]) + async def commit_reveal_enabled( + self, netuid: int, block_hash: Optional[str] = None + ) -> bool: + """ + Check if commit-reveal mechanism is enabled for a given network at a specific block. + + Arguments: + netuid (int): The network identifier for which to check the commit-reveal mechanism. + block_hash (Optional[str]): The block hash of block at which to check the parameter (default is None, which implies the current block). + + Returns: + (bool): Returns the integer value of the hyperparameter if available; otherwise, returns None. + """ + call = await self.get_hyperparameter( + param_name="CommitRevealWeightsEnabled", + block_hash=block_hash, + netuid=netuid, + ) + return True if call is True else False + + async def get_subnet_reveal_period_epochs( + self, netuid: int, block_hash: Optional[str] = None + ) -> int: + """Retrieve the SubnetRevealPeriodEpochs hyperparameter.""" + return await self.get_hyperparameter( + param_name="RevealPeriodEpochs", block_hash=block_hash, netuid=netuid + ) + # Extrinsics ======================================================================================================= async def transfer( @@ -1568,35 +1596,43 @@ async def set_weights( This function is crucial in shaping the network's collective intelligence, where each neuron's learning and contribution are influenced by the weights it sets towards others【81†source】. """ - uid = await self.get_uid_for_hotkey_on_subnet( - wallet.hotkey.ss58_address, netuid - ) - retries = 0 - success = False - message = "No attempt made. Perhaps it is too soon to set weights!" - while retries < max_retries and await self.blocks_since_last_update( - netuid, uid - ) > await self.weights_rate_limit(netuid): - try: - logging.info( - f"Setting weights for subnet #[blue]{netuid}[/blue]. Attempt [blue]{retries + 1} of {max_retries}[/blue]." - ) - success, message = await set_weights_extrinsic( - subtensor=self, - wallet=wallet, - netuid=netuid, - uids=uids, - weights=weights, - version_key=version_key, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - except Exception as e: - logging.error(f"Error setting weights: {e}") - finally: - retries += 1 + if self.commit_reveal_enabled(netuid=netuid) is True: + # go with `commit reveal v3` extrinsic + raise NotImplemented("Not implemented yet for AsyncSubtensor. Coming soon.") + else: + # go with classic `set weights extrinsic` + uid = await self.get_uid_for_hotkey_on_subnet( + wallet.hotkey.ss58_address, netuid + ) + retries = 0 + success = False + message = "No attempt made. Perhaps it is too soon to set weights!" + while ( + retries < max_retries + and await self.blocks_since_last_update(netuid, uid) + > await self.weights_rate_limit(netuid) + and success is False + ): + try: + logging.info( + f"Setting weights for subnet #[blue]{netuid}[/blue]. Attempt [blue]{retries + 1} of {max_retries}[/blue]." + ) + success, message = await set_weights_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + uids=uids, + weights=weights, + version_key=version_key, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + except Exception as e: + logging.error(f"Error setting weights: {e}") + finally: + retries += 1 - return success, message + return success, message async def root_set_weights( self, diff --git a/bittensor/core/extrinsics/commit_reveal.py b/bittensor/core/extrinsics/commit_reveal.py new file mode 100644 index 0000000000..7d0331c877 --- /dev/null +++ b/bittensor/core/extrinsics/commit_reveal.py @@ -0,0 +1,165 @@ +from typing import Optional, Union, TYPE_CHECKING + +import numpy as np +from bittensor_commit_reveal import get_encrypted_commit +from numpy.typing import NDArray + +from bittensor.core.extrinsics.utils import submit_extrinsic +from bittensor.core.settings import version_as_int +from bittensor.utils import format_error_message +from bittensor.utils.btlogging import logging +from bittensor.utils.networking import ensure_connected +from bittensor.utils.registration import torch, use_torch +from bittensor.utils.weight_utils import convert_weights_and_uids_for_emit + +if TYPE_CHECKING: + from bittensor_wallet import Wallet + from bittensor.core.subtensor import Subtensor + + +@ensure_connected +def _do_commit_reveal_v3( + self: "Subtensor", + wallet: "Wallet", + netuid: int, + commit: bytes, + reveal_round: int, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = False, +) -> tuple[bool, Optional[str]]: + """ + Executes the commit-reveal phase 3 for a given netuid and commit, and optionally waits for extrinsic inclusion or finalization. + + Arguments: + wallet: Wallet An instance of the Wallet class containing the user's keypair. + netuid: int The network unique identifier. + commit bytes The commit data in bytes format. + reveal_round: int The round number for the reveal phase. + wait_for_inclusion: bool, optional Flag indicating whether to wait for the extrinsic to be included in a block. + wait_for_finalization: bool, optional Flag indicating whether to wait for the extrinsic to be finalized. + + Returns: + A tuple where the first element is a boolean indicating success or failure, and the second element is an optional string containing error message if any. + """ + logging.info( + f"Committing weights hash [blue]{commit.hex()}[/blue] for subnet #[blue]{netuid}[/blue] with " + f"reveal round [blue]{reveal_round}[/blue]..." + ) + + call = self.substrate.compose_call( + call_module="SubtensorModule", + call_function="commit_crv3_weights", + call_params={ + "netuid": netuid, + "commit": commit, + "reveal_round": reveal_round, + }, + ) + extrinsic = self.substrate.create_signed_extrinsic( + call=call, + keypair=wallet.hotkey, + ) + + response = submit_extrinsic( + subtensor=self, + extrinsic=extrinsic, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + if not wait_for_finalization and not wait_for_inclusion: + return True, "Not waiting for finalization or inclusion." + + response.process_events() + if response.is_success: + return True, None + else: + return False, format_error_message( + response.error_message, substrate=self.substrate + ) + + +def commit_reveal_v3_extrinsic( + subtensor: "Subtensor", + wallet: "Wallet", + netuid: int, + uids: Union[NDArray[np.int64], "torch.LongTensor", list], + weights: Union[NDArray[np.float32], "torch.FloatTensor", list], + version_key: int = version_as_int, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = False, +) -> tuple[bool, str]: + """ + Commits and reveals weights for given subtensor and wallet with provided uids and weights. + + Arguments: + subtensor: The Subtensor instance. + wallet: The wallet to use for committing and revealing. + netuid: The id of the network. + uids: The uids to commit. + weights: The weights associated with the uids. + version_key: The version key to use for committing and revealing. Default is version_as_int. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. Default is False. + wait_for_finalization: Whether to wait for the finalization of the transaction. Default is False. + + Returns: + tuple[bool, str]: A tuple where the first element is a boolean indicating success or failure, and the second element is a message associated with the result. + """ + try: + # Convert uids and weights + if use_torch(): + if isinstance(uids, list): + uids = torch.tensor(uids, dtype=torch.int64) + if isinstance(weights, list): + weights = torch.tensor(weights, dtype=torch.float32) + else: + if isinstance(uids, list): + uids = np.array(uids, dtype=np.int64) + if isinstance(weights, list): + weights = np.array(weights, dtype=np.float32) + + # Reformat and normalize. + uids, weights = convert_weights_and_uids_for_emit(uids, weights) + + current_block = subtensor.get_current_block() + subnet_hyperparameters = subtensor.get_subnet_hyperparameters( + netuid, block=current_block + ) + tempo = subnet_hyperparameters.tempo + subnet_reveal_period_epochs = ( + subnet_hyperparameters.commit_reveal_weights_interval + ) + + # Encrypt `commit_hash` with t-lock and `get reveal_round` + commit_for_reveal, reveal_round = get_encrypted_commit( + uids=uids, + weights=weights, + version_key=version_key, + tempo=tempo, + current_block=current_block, + netuid=netuid, + subnet_reveal_period_epochs=subnet_reveal_period_epochs, + ) + + success, message = _do_commit_reveal_v3( + self=subtensor, + wallet=wallet, + netuid=netuid, + commit=commit_for_reveal, + reveal_round=reveal_round, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + if success is True: + logging.success( + f"[green]Finalized![/green] Weights commited with reveal round [blue]{reveal_round}[/blue]." + ) + return True, f"reveal_round:{reveal_round}" + else: + logging.error(message) + return False, message + + except Exception as e: + logging.error(f":cross_mark: [red]Failed. Error:[/red] {e}") + return False, str(e) diff --git a/bittensor/core/extrinsics/set_weights.py b/bittensor/core/extrinsics/set_weights.py index 65aac03fc2..64f318f8d6 100644 --- a/bittensor/core/extrinsics/set_weights.py +++ b/bittensor/core/extrinsics/set_weights.py @@ -148,7 +148,7 @@ def set_weights_extrinsic( logging.debug(f"Weights: {[float(v / 65535) for v in weight_vals]}") try: - success, error_message = do_set_weights( + success, message = do_set_weights( self=subtensor, wallet=wallet, netuid=netuid, @@ -166,8 +166,8 @@ def set_weights_extrinsic( logging.success(f"[green]Finalized![/green] Set weights: {str(success)}") return True, "Successfully set weights and Finalized." else: - logging.error(error_message) - return False, error_message + logging.error(message) + return False, message except Exception as e: logging.error(f":cross_mark: [red]Failed.[/red]: Error: {e}") diff --git a/bittensor/core/metagraph.py b/bittensor/core/metagraph.py index 31ab3cf3d3..4da95852be 100644 --- a/bittensor/core/metagraph.py +++ b/bittensor/core/metagraph.py @@ -570,7 +570,7 @@ def sync( if ( subtensor.chain_endpoint != settings.ARCHIVE_ENTRYPOINT - or subtensor.network != settings.NETWORKS[3] + or subtensor.network != "archive" ): cur_block = subtensor.get_current_block() if block and block < (cur_block - 300): diff --git a/bittensor/core/settings.py b/bittensor/core/settings.py index 5bc596ede9..2da8ecb5a1 100644 --- a/bittensor/core/settings.py +++ b/bittensor/core/settings.py @@ -15,7 +15,7 @@ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # DEALINGS IN THE SOFTWARE. -__version__ = "8.4.5" +__version__ = "8.5.0" import os import re diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 441a4c033b..4c238feb6a 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -31,6 +31,7 @@ SubnetInfo, ) from bittensor.core.config import Config +from bittensor.core.extrinsics.commit_reveal import commit_reveal_v3_extrinsic from bittensor.core.extrinsics.commit_weights import ( commit_weights_extrinsic, reveal_weights_extrinsic, @@ -50,13 +51,13 @@ get_metadata, ) from bittensor.core.extrinsics.set_weights import set_weights_extrinsic -from bittensor.core.extrinsics.transfer import ( - transfer_extrinsic, -) from bittensor.core.extrinsics.staking import ( add_stake_extrinsic, add_stake_multiple_extrinsic, ) +from bittensor.core.extrinsics.transfer import ( + transfer_extrinsic, +) from bittensor.core.extrinsics.unstaking import ( unstake_extrinsic, unstake_multiple_extrinsic, @@ -1128,6 +1129,32 @@ def max_weight_limit( ) return None if call is None else u16_normalized_float(int(call)) + def commit_reveal_enabled( + self, netuid: int, block: Optional[int] = None + ) -> Optional[bool]: + """ + Check if commit-reveal mechanism is enabled for a given network at a specific block. + + Arguments: + netuid (int): The network identifier for which to check the commit-reveal mechanism. + block (Optional[int]): The block number at which to check the parameter (default is None, which implies the current block). + + Returns: + (Optional[bool]): Returns the integer value of the hyperparameter if available; otherwise, returns None. + """ + call = self._get_hyperparameter( + param_name="CommitRevealWeightsEnabled", block=block, netuid=netuid + ) + return True if call is True else False + + def get_subnet_reveal_period_epochs( + self, netuid: int, block: Optional[int] = None + ) -> Optional[int]: + """Retrieve the SubnetRevealPeriodEpochs hyperparameter.""" + return self._get_hyperparameter( + param_name="RevealPeriodEpochs", block=block, netuid=netuid + ) + def get_prometheus_info( self, netuid: int, hotkey_ss58: str, block: Optional[int] = None ) -> Optional["PrometheusInfo"]: @@ -1268,6 +1295,76 @@ def neurons(self, netuid: int, block: Optional[int] = None) -> list["NeuronInfo" return neurons + def last_drand_round( + self, + ) -> Optional[int]: + """ + Retrieves the last drand round emitted in bittensor. This corresponds when committed weights will be revealed. + + Returns: + int: The latest Drand round emitted in bittensor. + """ + result = self.substrate.query( + module="Drand", storage_function="LastStoredRound" + ) + return getattr(result, "value", None) + + def get_current_weight_commit_info( + self, netuid: int, block: Optional[int] = None + ) -> list: + """ + Retrieves CRV3 weight commit information for a specific subnet. + + Args: + netuid (int): The unique identifier of the subnet. + block (Optional[int]): The blockchain block number for the query. + + Returns: + list: A list of commit details, where each entry is a dictionary with keys 'who', + 'serialized_commit', and 'reveal_round', or an empty list if no data is found. + """ + result = self.query_map( + module="SubtensorModule", + name="CRV3WeightCommits", + params=[netuid], + block=block, + ) + return result.records[0][1].value if result and result.records else [] + + def get_total_stake_for_coldkey( + self, ss58_address: str, block: Optional[int] = None + ) -> Optional["Balance"]: + """Retrieves the total stake held by a coldkey across all associated hotkeys, including delegated stakes. + + Args: + ss58_address (str): The SS58 address of the coldkey account. + block (Optional[int]): The blockchain block number at which to perform the query. + + Returns: + Optional[Balance]: The total stake amount held by the coldkey, or None if the query fails. + """ + result = self.query_subtensor("TotalColdkeyStake", block, [ss58_address]) + if getattr(result, "value", None) is None: + return None + return Balance.from_rao(result.value) + + def get_total_stake_for_hotkey( + self, ss58_address: str, block: Optional[int] = None + ) -> Optional["Balance"]: + """Retrieves the total stake associated with a hotkey. + + Args: + ss58_address (str): The SS58 address of the hotkey account. + block (Optional[int]): The blockchain block number at which to perform the query. + + Returns: + Optional[Balance]: The total stake amount held by the hotkey, or None if the query fails. + """ + result = self.query_subtensor("TotalHotkeyStake", block, [ss58_address]) + if getattr(result, "value", None) is None: + return None + return Balance.from_rao(result.value) + 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. @@ -1717,35 +1814,49 @@ def set_weights( This function is crucial in shaping the network's collective intelligence, where each neuron's learning and contribution are influenced by the weights it sets towards others【81†source】. """ - uid = self.get_uid_for_hotkey_on_subnet(wallet.hotkey.ss58_address, netuid) - retries = 0 - success = False - message = "No attempt made. Perhaps it is too soon to set weights!" - while ( - self.blocks_since_last_update(netuid, uid) > self.weights_rate_limit(netuid) # type: ignore - and retries < max_retries - and success is False - ): - try: - logging.info( - f"Setting weights for subnet #{netuid}. Attempt {retries + 1} of {max_retries}." - ) - success, message = set_weights_extrinsic( - subtensor=self, - wallet=wallet, - netuid=netuid, - uids=uids, - weights=weights, - version_key=version_key, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - except Exception as e: - logging.error(f"Error setting weights: {e}") - finally: - retries += 1 - - return success, message + if self.commit_reveal_enabled(netuid=netuid) is True: + # go with `commit reveal v3` extrinsic + return commit_reveal_v3_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + uids=uids, + weights=weights, + version_key=version_key, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + else: + # go with classic `set weights` logic + uid = self.get_uid_for_hotkey_on_subnet(wallet.hotkey.ss58_address, netuid) + retries = 0 + success = False + message = "No attempt made. Perhaps it is too soon to set weights!" + while ( + self.blocks_since_last_update(netuid, uid) # type: ignore + > self.weights_rate_limit(netuid) # type: ignore + and retries < max_retries + and success is False + ): + try: + logging.info( + f"Setting weights for subnet #{netuid}. Attempt {retries + 1} of {max_retries}." + ) + success, message = set_weights_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + uids=uids, + weights=weights, + version_key=version_key, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + except Exception as e: + logging.error(f"Error setting weights: {e}") + finally: + retries += 1 + return success, message @legacy_torch_api_compat def root_set_weights( diff --git a/bittensor/utils/async_substrate_interface.py b/bittensor/utils/async_substrate_interface.py index cd50695492..05fd963212 100644 --- a/bittensor/utils/async_substrate_interface.py +++ b/bittensor/utils/async_substrate_interface.py @@ -944,7 +944,7 @@ async def get_runtime(block_hash, block_id) -> Runtime: self.last_block_hash = block_hash self.block_id = block_id - # In fact calls and storage functions are decoded against runtime of previous block, therefor retrieve + # In fact calls and storage functions are decoded against runtime of previous block, therefore retrieve # metadata and apply type registry of runtime of parent block block_header = await self.rpc_request( "chain_getHeader", [self.last_block_hash] @@ -1390,7 +1390,7 @@ async def get_block( A dict containing the extrinsic and digest logs data """ if block_hash and block_number: - raise ValueError("Either block_hash or block_number should be be set") + raise ValueError("Either block_hash or block_number should be set") if block_number is not None: block_hash = await self.get_block_hash(block_number) @@ -2758,7 +2758,7 @@ async def get_metadata_call_function( return call return None - async def get_block_number(self, block_hash: Optional[str]) -> int: + async def get_block_number(self, block_hash: Optional[str] = None) -> int: """Async version of `substrateinterface.base.get_block_number` method.""" response = await self.rpc_request("chain_getHeader", [block_hash]) diff --git a/bittensor/utils/btlogging/format.py b/bittensor/utils/btlogging/format.py index 5f76dce6d4..ebb353525f 100644 --- a/bittensor/utils/btlogging/format.py +++ b/bittensor/utils/btlogging/format.py @@ -225,7 +225,7 @@ def format(self, record: "logging.LogRecord") -> str: record (logging.LogRecord): The log record. Returns: - formated record (str): The formatted log record. + formatted record (str): The formatted log record. """ record.levelname = f"{record.levelname:^10}" return super().format(record) diff --git a/requirements/prod.txt b/requirements/prod.txt index bf39706fc7..c57ce611f9 100644 --- a/requirements/prod.txt +++ b/requirements/prod.txt @@ -25,3 +25,4 @@ substrate-interface~=1.7.9 uvicorn websockets>=14.1 bittensor-wallet>=2.1.3 +bittensor-commit-reveal>=0.1.0 diff --git a/scripts/check_pre_submit.sh b/scripts/check_pre_submit.sh index 4dbe7747f6..da2ef67eea 100755 --- a/scripts/check_pre_submit.sh +++ b/scripts/check_pre_submit.sh @@ -1,6 +1,6 @@ #!/bin/bash -# ruff checks formating +# ruff checks formatting echo ">>> Run the pre-submit format check with \`ruff format .\`." ruff format . diff --git a/tests/e2e_tests/conftest.py b/tests/e2e_tests/conftest.py index 4a7b2ccf62..5d94f3aedd 100644 --- a/tests/e2e_tests/conftest.py +++ b/tests/e2e_tests/conftest.py @@ -4,6 +4,7 @@ import signal import subprocess import time +import threading import pytest from substrateinterface import SubstrateInterface @@ -37,7 +38,11 @@ def local_chain(request): # Start new node process process = subprocess.Popen( - cmds, stdout=subprocess.PIPE, text=True, preexec_fn=os.setsid + cmds, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + preexec_fn=os.setsid, ) # Pattern match indicates node is compiled and ready @@ -52,16 +57,31 @@ def local_chain(request): timestamp = int(time.time()) def wait_for_node_start(process, pattern): - for line in process.stdout: + while True: + line = process.stdout.readline() + if not line: + break + print(line.strip()) # 10 min as timeout if int(time.time()) - timestamp > 10 * 60: print("Subtensor not started in time") - break + return if pattern.search(line): print("Node started!") break + # Start a background reader after pattern is found + # To prevent the buffer filling up + def read_output(): + while True: + line = process.stdout.readline() + if not line: + break + + reader_thread = threading.Thread(target=read_output, daemon=True) + reader_thread.start() + wait_for_node_start(process, pattern) # Run the test, passing in substrate interface diff --git a/tests/e2e_tests/test_commit_reveal_v3.py b/tests/e2e_tests/test_commit_reveal_v3.py new file mode 100644 index 0000000000..4c038c3764 --- /dev/null +++ b/tests/e2e_tests/test_commit_reveal_v3.py @@ -0,0 +1,209 @@ +import re + +import numpy as np +import pytest +from bittensor.utils.btlogging import logging +from bittensor.core.subtensor import Subtensor +from bittensor.utils.balance import Balance +from bittensor.utils.weight_utils import convert_weights_and_uids_for_emit +from tests.e2e_tests.utils.chain_interactions import ( + add_stake, + register_subnet, + sudo_set_hyperparameter_bool, + sudo_set_hyperparameter_values, + wait_interval, + sudo_set_admin_utils, + next_tempo, +) +from tests.e2e_tests.utils.e2e_test_utils import setup_wallet + + +@pytest.mark.parametrize("local_chain", [False], indirect=True) +@pytest.mark.asyncio +async def test_commit_and_reveal_weights_cr3(local_chain): + """ + Tests the commit/reveal weights mechanism (CR3) + + Steps: + 1. Register a subnet through Alice + 2. Register Alice's neuron and add stake + 3. Enable commit-reveal mechanism on the subnet + 4. Lower weights rate limit + 5. Change the tempo for subnet 1 + 5. Commit weights and ensure they are committed. + 6. Wait interval & reveal weights and verify + Raises: + AssertionError: If any of the checks or verifications fail + """ + netuid = 1 + logging.console.info("Testing test_commit_and_reveal_weights") + + # Register root as Alice + keypair, alice_wallet = setup_wallet("//Alice") + assert register_subnet(local_chain, alice_wallet), "Unable to register the subnet" + + # Verify subnet 1 created successfully + assert local_chain.query( + "SubtensorModule", "NetworksAdded", [1] + ).serialize(), "Subnet wasn't created successfully" + + logging.console.info("Subnet 1 is registered") + + subtensor = Subtensor(network="ws://localhost:9945") + + # Register Alice to the subnet + assert subtensor.burned_register( + alice_wallet, netuid + ), "Unable to register Alice as a neuron" + logging.console.info("Registered Alice to subnet 1") + + # Stake to become to top neuron after the first epoch + add_stake(local_chain, alice_wallet, Balance.from_tao(100_000)) + logging.console.info("Stake added by Alice") + + # Enable commit_reveal on the subnet + assert sudo_set_hyperparameter_bool( + local_chain, + alice_wallet, + "sudo_set_commit_reveal_weights_enabled", + True, + netuid, + ), "Unable to enable commit reveal on the subnet" + + # Verify commit_reveal was enabled + assert subtensor.get_subnet_hyperparameters( + netuid=netuid, + ).commit_reveal_weights_enabled, "Failed to enable commit/reveal" + logging.console.info("Commit reveal enabled") + + # Change the weights rate limit on the subnet + assert sudo_set_hyperparameter_values( + local_chain, + alice_wallet, + call_function="sudo_set_weights_set_rate_limit", + call_params={"netuid": netuid, "weights_set_rate_limit": "0"}, + return_error_message=True, + ) + + # Verify weights rate limit was changed + assert ( + subtensor.get_subnet_hyperparameters(netuid=netuid).weights_rate_limit == 0 + ), "Failed to set weights_rate_limit" + assert subtensor.weights_rate_limit(netuid=netuid) == 0 + logging.console.info("sudo_set_weights_set_rate_limit executed: set to 0") + + # Change the tempo of the subnet from default 360 + # Since this is in normal blocks, this is necessary + tempo_set = 10 + assert sudo_set_admin_utils( + local_chain, + alice_wallet, + call_function="sudo_set_tempo", + call_params={"netuid": netuid, "tempo": tempo_set}, + return_error_message=True, + ) + tempo = subtensor.get_subnet_hyperparameters(netuid=netuid).tempo + assert tempo_set == tempo + logging.console.info(f"sudo_set_tempo executed: set to {tempo_set}") + + # Commit-reveal values - setting weights to self + uids = np.array([0], dtype=np.int64) + weights = np.array([0.1], dtype=np.float32) + weight_uids, weight_vals = convert_weights_and_uids_for_emit( + uids=uids, weights=weights + ) + + # Fetch current block and calculate next tempo for the subnet + current_block = subtensor.get_current_block() + upcoming_tempo = next_tempo(current_block, tempo, netuid) + logging.console.info( + f"Checking if window is too low with Current block: {current_block}, next tempo: {upcoming_tempo}" + ) + # Lower than this might mean weights will get revealed before we can check them + if upcoming_tempo - current_block < 3: + await wait_interval( + tempo, + subtensor, + netuid=netuid, + reporting_interval=1, + ) + current_block = subtensor.get_current_block() + latest_drand_round = subtensor.last_drand_round() + upcoming_tempo = next_tempo(current_block, tempo, netuid) + logging.console.info( + f"Post first wait_interval (to ensure window isnt too low): {current_block}, next tempo: {upcoming_tempo}, drand: {latest_drand_round}" + ) + + # Commit weights + success, message = subtensor.set_weights( + alice_wallet, + netuid, + uids=weight_uids, + weights=weight_vals, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + # Assert committing was a success + assert success is True + assert bool(re.match(r"reveal_round:\d+", message)) + + # Parse expected reveal_round + expected_reveal_round = int(message.split(":")[1]) + logging.console.info( + f"Successfully set weights: uids {weight_uids}, weights {weight_vals}, reveal_round: {expected_reveal_round}" + ) + + current_block = subtensor.get_current_block() + latest_drand_round = subtensor.last_drand_round() + upcoming_tempo = next_tempo(current_block, tempo, netuid) + logging.console.info( + f"After setting weights: Current block: {current_block}, next tempo: {upcoming_tempo}, drand: {latest_drand_round}" + ) + + # Ensure the expected drand round is well in the future + assert ( + expected_reveal_round > latest_drand_round + ), "Revealed drand pulse is older than the drand pulse right after setting weights" + + # Fetch current commits pending on the chain + commits_on_chain = subtensor.get_current_weight_commit_info(netuid=netuid) + address, commit, reveal_round = commits_on_chain[0] + + # Assert correct values are committed on the chain + assert expected_reveal_round == reveal_round + assert address == alice_wallet.hotkey.ss58_address + + # Ensure no weights are available as of now + assert subtensor.weights(netuid=netuid) == [] + + # Wait for the next tempo so weights can be revealed + await wait_interval( + subtensor.get_subnet_hyperparameters(netuid=netuid).tempo, + subtensor, + netuid=netuid, + reporting_interval=1, + ) + + # Fetch the latest drand pulse + latest_drand_round = subtensor.last_drand_round() + logging.console.info( + f"Latest drand round after waiting for tempo: {latest_drand_round}" + ) + + # Fetch weights on the chain as they should be revealed now + revealed_weights = subtensor.weights(netuid=netuid)[0][1] + + # Assert correct weights were revealed + assert weight_uids[0] == revealed_weights[0][0] + assert weight_vals[0] == revealed_weights[0][1] + + # Now that the commit has been revealed, there shouldn't be any pending commits + assert subtensor.get_current_weight_commit_info(netuid=netuid) == [] + + # Ensure the drand_round is always in the positive w.r.t expected when revealed + assert ( + latest_drand_round - expected_reveal_round >= 0 + ), f"latest_drand_round ({latest_drand_round}) is less than expected_reveal_round ({expected_reveal_round})" + + logging.console.info("✅ Passed commit_reveal v3") diff --git a/tests/e2e_tests/test_commit_weights.py b/tests/e2e_tests/test_commit_weights.py index 3e29dc56ec..c6737e01ae 100644 --- a/tests/e2e_tests/test_commit_weights.py +++ b/tests/e2e_tests/test_commit_weights.py @@ -2,7 +2,6 @@ import numpy as np import pytest - from bittensor.core.subtensor import Subtensor from bittensor.utils.balance import Balance from bittensor.utils.weight_utils import convert_weights_and_uids_for_emit @@ -18,7 +17,7 @@ @pytest.mark.asyncio -async def test_commit_and_reveal_weights(local_chain): +async def test_commit_and_reveal_weights_legacy(local_chain): """ Tests the commit/reveal weights mechanism with subprocess disabled (CR1.0) diff --git a/tests/e2e_tests/utils/chain_interactions.py b/tests/e2e_tests/utils/chain_interactions.py index 9c0d9100e8..bae60c5443 100644 --- a/tests/e2e_tests/utils/chain_interactions.py +++ b/tests/e2e_tests/utils/chain_interactions.py @@ -133,7 +133,27 @@ async def wait_epoch(subtensor: "Subtensor", netuid: int = 1): await wait_interval(tempo, subtensor, netuid) -async def wait_interval(tempo: int, subtensor: "Subtensor", netuid: int = 1): +def next_tempo(current_block: int, tempo: int, netuid: int) -> int: + """ + Calculates the next tempo block for a specific subnet. + + Args: + current_block (int): The current block number. + tempo (int): The tempo value for the subnet. + netuid (int): The unique identifier of the subnet. + + Returns: + int: The next tempo block number. + """ + interval = tempo + 1 + last_epoch = current_block - 1 - (current_block + netuid + 1) % interval + next_tempo = last_epoch + interval + return next_tempo + + +async def wait_interval( + tempo: int, subtensor: "Subtensor", netuid: int = 1, reporting_interval: int = 10 +): """ Waits until the next tempo interval starts for a specific subnet. @@ -141,10 +161,8 @@ async def wait_interval(tempo: int, subtensor: "Subtensor", netuid: int = 1): and the provided tempo, then enters a loop where it periodically checks the current block number until the next tempo interval starts. """ - interval = tempo + 1 current_block = subtensor.get_current_block() - last_epoch = current_block - 1 - (current_block + netuid + 1) % interval - next_tempo_block_start = last_epoch + interval + next_tempo_block_start = next_tempo(current_block, tempo, netuid) last_reported = None while current_block < next_tempo_block_start: @@ -152,7 +170,7 @@ async def wait_interval(tempo: int, subtensor: "Subtensor", netuid: int = 1): 1 ) # Wait for 1 second before checking the block number again current_block = subtensor.get_current_block() - if last_reported is None or current_block - last_reported >= 10: + if last_reported is None or current_block - last_reported >= reporting_interval: last_reported = current_block print( f"Current Block: {current_block} Next tempo for netuid {netuid} at: {next_tempo_block_start}" @@ -160,3 +178,52 @@ async def wait_interval(tempo: int, subtensor: "Subtensor", netuid: int = 1): logging.info( f"Current Block: {current_block} Next tempo for netuid {netuid} at: {next_tempo_block_start}" ) + + +# Helper to execute sudo wrapped calls on the chain +def sudo_set_admin_utils( + substrate: "SubstrateInterface", + wallet: "Wallet", + call_function: str, + call_params: dict, + return_error_message: bool = False, +) -> Union[bool, tuple[bool, Optional[str]]]: + """ + Wraps the call in sudo to set hyperparameter values using AdminUtils. + + Args: + substrate (SubstrateInterface): Substrate connection. + wallet (Wallet): Wallet object with the keypair for signing. + call_function (str): The AdminUtils function to call. + call_params (dict): Parameters for the AdminUtils function. + return_error_message (bool): If True, returns the error message along with the success status. + + Returns: + Union[bool, tuple[bool, Optional[str]]]: Success status or (success status, error message). + """ + inner_call = substrate.compose_call( + call_module="AdminUtils", + call_function=call_function, + call_params=call_params, + ) + + sudo_call = substrate.compose_call( + call_module="Sudo", + call_function="sudo", + call_params={"call": inner_call}, + ) + extrinsic = substrate.create_signed_extrinsic( + call=sudo_call, keypair=wallet.coldkey + ) + + response = substrate.submit_extrinsic( + extrinsic, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + response.process_events() + + if return_error_message: + return response.is_success, response.error_message + + return response.is_success diff --git a/tests/unit_tests/extrinsics/test_commit_reveal.py b/tests/unit_tests/extrinsics/test_commit_reveal.py new file mode 100644 index 0000000000..6d2f4549a6 --- /dev/null +++ b/tests/unit_tests/extrinsics/test_commit_reveal.py @@ -0,0 +1,358 @@ +from bittensor.core import subtensor as subtensor_module +from bittensor.core.chain_data import SubnetHyperparameters +from bittensor.core.subtensor import Subtensor +from bittensor.core.extrinsics import commit_reveal +import pytest +import torch +import numpy as np + + +@pytest.fixture +def subtensor(mocker): + fake_substrate = mocker.MagicMock() + fake_substrate.websocket.sock.getsockopt.return_value = 0 + mocker.patch.object( + subtensor_module, "SubstrateInterface", return_value=fake_substrate + ) + yield Subtensor() + + +@pytest.fixture +def hyperparams(): + yield SubnetHyperparameters( + rho=0, + kappa=0, + immunity_period=0, + min_allowed_weights=0, + max_weight_limit=0.0, + tempo=0, + min_difficulty=0, + max_difficulty=0, + weights_version=0, + weights_rate_limit=0, + adjustment_interval=0, + activity_cutoff=0, + registration_allowed=False, + target_regs_per_interval=0, + min_burn=0, + max_burn=0, + bonds_moving_avg=0, + max_regs_per_block=0, + serving_rate_limit=0, + max_validators=0, + adjustment_alpha=0, + difficulty=0, + commit_reveal_weights_interval=0, + commit_reveal_weights_enabled=True, + alpha_high=0, + alpha_low=0, + liquid_alpha_enabled=False, + ) + + +def test_do_commit_reveal_v3_success(mocker, subtensor): + """Test successful commit-reveal with wait for finalization.""" + # Preps + fake_wallet = mocker.Mock(autospec=subtensor_module.Wallet) + fake_netuid = 1 + fake_commit = b"fake_commit" + fake_reveal_round = 1 + + mocked_compose_call = mocker.patch.object(subtensor.substrate, "compose_call") + mocked_create_signed_extrinsic = mocker.patch.object( + subtensor.substrate, "create_signed_extrinsic" + ) + mocked_submit_extrinsic = mocker.patch.object(commit_reveal, "submit_extrinsic") + + # Call + result = commit_reveal._do_commit_reveal_v3( + self=subtensor, + wallet=fake_wallet, + netuid=fake_netuid, + commit=fake_commit, + reveal_round=fake_reveal_round, + ) + + # Asserts + mocked_compose_call.assert_called_once_with( + call_module="SubtensorModule", + call_function="commit_crv3_weights", + call_params={ + "netuid": fake_netuid, + "commit": fake_commit, + "reveal_round": fake_reveal_round, + }, + ) + mocked_create_signed_extrinsic.assert_called_once_with( + call=mocked_compose_call.return_value, keypair=fake_wallet.hotkey + ) + mocked_submit_extrinsic.assert_called_once_with( + subtensor=subtensor, + extrinsic=mocked_create_signed_extrinsic.return_value, + wait_for_inclusion=False, + wait_for_finalization=False, + ) + assert result == (True, "Not waiting for finalization or inclusion.") + + +def test_do_commit_reveal_v3_failure_due_to_error(mocker, subtensor): + """Test commit-reveal fails due to an error in submission.""" + # Preps + fake_wallet = mocker.Mock(autospec=subtensor_module.Wallet) + fake_netuid = 1 + fake_commit = b"fake_commit" + fake_reveal_round = 1 + + mocked_compose_call = mocker.patch.object(subtensor.substrate, "compose_call") + mocked_create_signed_extrinsic = mocker.patch.object( + subtensor.substrate, "create_signed_extrinsic" + ) + mocked_submit_extrinsic = mocker.patch.object( + commit_reveal, + "submit_extrinsic", + return_value=mocker.Mock(is_success=False, error_message="Mocked error"), + ) + mocked_format_error_message = mocker.patch.object( + commit_reveal, "format_error_message", return_value="Formatted error" + ) + + # Call + result = commit_reveal._do_commit_reveal_v3( + self=subtensor, + wallet=fake_wallet, + netuid=fake_netuid, + commit=fake_commit, + reveal_round=fake_reveal_round, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + # Asserts + mocked_compose_call.assert_called_once_with( + call_module="SubtensorModule", + call_function="commit_crv3_weights", + call_params={ + "netuid": fake_netuid, + "commit": fake_commit, + "reveal_round": fake_reveal_round, + }, + ) + mocked_create_signed_extrinsic.assert_called_once_with( + call=mocked_compose_call.return_value, keypair=fake_wallet.hotkey + ) + mocked_submit_extrinsic.assert_called_once_with( + subtensor=subtensor, + extrinsic=mocked_create_signed_extrinsic.return_value, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + mocked_format_error_message.assert_called_once_with( + "Mocked error", substrate=subtensor.substrate + ) + assert result == (False, "Formatted error") + + +def test_commit_reveal_v3_extrinsic_success_with_torch(mocker, subtensor, hyperparams): + """Test successful commit-reveal with torch tensors.""" + # Preps + fake_wallet = mocker.Mock(autospec=subtensor_module.Wallet) + fake_netuid = 1 + fake_uids = torch.tensor([1, 2, 3], dtype=torch.int64) + fake_weights = torch.tensor([0.1, 0.2, 0.7], dtype=torch.float32) + fake_commit_for_reveal = b"mock_commit_for_reveal" + fake_reveal_round = 1 + + # Mocks + mocker.patch.object(commit_reveal, "use_torch", return_value=True) + + mocked_uids = mocker.Mock() + mocked_weights = mocker.Mock() + mocked_convert_weights_and_uids_for_emit = mocker.patch.object( + commit_reveal, + "convert_weights_and_uids_for_emit", + return_value=(mocked_uids, mocked_weights), + ) + mocked_get_subnet_reveal_period_epochs = mocker.patch.object( + subtensor, "get_subnet_reveal_period_epochs" + ) + mocked_get_encrypted_commit = mocker.patch.object( + commit_reveal, + "get_encrypted_commit", + return_value=(fake_commit_for_reveal, fake_reveal_round), + ) + mock_do_commit_reveal_v3 = mocker.patch.object( + commit_reveal, "_do_commit_reveal_v3", return_value=(True, "Success") + ) + mock_block = mocker.patch.object(subtensor, "get_current_block", return_value=1) + mock_hyperparams = mocker.patch.object( + subtensor, + "get_subnet_hyperparameters", + return_value=hyperparams, + ) + + # Call + success, message = commit_reveal.commit_reveal_v3_extrinsic( + subtensor=subtensor, + wallet=fake_wallet, + netuid=fake_netuid, + uids=fake_uids, + weights=fake_weights, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + # Asserts + assert success is True + assert message == "reveal_round:1" + mocked_convert_weights_and_uids_for_emit.assert_called_once_with( + fake_uids, fake_weights + ) + mocked_get_encrypted_commit.assert_called_once_with( + uids=mocked_uids, + weights=mocked_weights, + subnet_reveal_period_epochs=mock_hyperparams.return_value.commit_reveal_weights_interval, + version_key=commit_reveal.version_as_int, + tempo=mock_hyperparams.return_value.tempo, + netuid=fake_netuid, + current_block=mock_block.return_value, + ) + mock_do_commit_reveal_v3.assert_called_once_with( + self=subtensor, + wallet=fake_wallet, + netuid=fake_netuid, + commit=fake_commit_for_reveal, + reveal_round=fake_reveal_round, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + +def test_commit_reveal_v3_extrinsic_success_with_numpy(mocker, subtensor, hyperparams): + """Test successful commit-reveal with numpy arrays.""" + # Preps + fake_wallet = mocker.Mock(autospec=subtensor_module.Wallet) + fake_netuid = 1 + fake_uids = np.array([1, 2, 3], dtype=np.int64) + fake_weights = np.array([0.1, 0.2, 0.7], dtype=np.float32) + + mocker.patch.object(commit_reveal, "use_torch", return_value=False) + mock_convert = mocker.patch.object( + commit_reveal, + "convert_weights_and_uids_for_emit", + return_value=(fake_uids, fake_weights), + ) + mock_encode_drand = mocker.patch.object( + commit_reveal, "get_encrypted_commit", return_value=(b"commit", 0) + ) + mock_do_commit = mocker.patch.object( + commit_reveal, "_do_commit_reveal_v3", return_value=(True, "Committed!") + ) + mocker.patch.object(subtensor, "get_current_block", return_value=1) + mocker.patch.object( + subtensor, + "get_subnet_hyperparameters", + return_value=hyperparams, + ) + + # Call + success, message = commit_reveal.commit_reveal_v3_extrinsic( + subtensor=subtensor, + wallet=fake_wallet, + netuid=fake_netuid, + uids=fake_uids, + weights=fake_weights, + wait_for_inclusion=False, + wait_for_finalization=False, + ) + + # Asserts + assert success is True + assert message == "reveal_round:0" + mock_convert.assert_called_once_with(fake_uids, fake_weights) + mock_encode_drand.assert_called_once() + mock_do_commit.assert_called_once() + + +def test_commit_reveal_v3_extrinsic_response_false(mocker, subtensor, hyperparams): + """Test unsuccessful commit-reveal with torch.""" + # Preps + fake_wallet = mocker.Mock(autospec=subtensor_module.Wallet) + fake_netuid = 1 + fake_uids = torch.tensor([1, 2, 3], dtype=torch.int64) + fake_weights = torch.tensor([0.1, 0.2, 0.7], dtype=torch.float32) + fake_commit_for_reveal = b"mock_commit_for_reveal" + fake_reveal_round = 1 + + # Mocks + mocker.patch.object(commit_reveal, "use_torch", return_value=True) + mocker.patch.object( + commit_reveal, + "convert_weights_and_uids_for_emit", + return_value=(fake_uids, fake_weights), + ) + mocker.patch.object( + commit_reveal, + "get_encrypted_commit", + return_value=(fake_commit_for_reveal, fake_reveal_round), + ) + mock_do_commit_reveal_v3 = mocker.patch.object( + commit_reveal, "_do_commit_reveal_v3", return_value=(False, "Failed") + ) + mocker.patch.object(subtensor, "get_current_block", return_value=1) + mocker.patch.object( + subtensor, + "get_subnet_hyperparameters", + return_value=hyperparams, + ) + + # Call + success, message = commit_reveal.commit_reveal_v3_extrinsic( + subtensor=subtensor, + wallet=fake_wallet, + netuid=fake_netuid, + uids=fake_uids, + weights=fake_weights, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + # Asserts + assert success is False + assert message == "Failed" + mock_do_commit_reveal_v3.assert_called_once_with( + self=subtensor, + wallet=fake_wallet, + netuid=fake_netuid, + commit=fake_commit_for_reveal, + reveal_round=fake_reveal_round, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + +def test_commit_reveal_v3_extrinsic_exception(mocker, subtensor): + """Test exception handling in commit-reveal.""" + # Preps + fake_wallet = mocker.Mock(autospec=subtensor_module.Wallet) + fake_netuid = 1 + fake_uids = [1, 2, 3] + fake_weights = [0.1, 0.2, 0.7] + + mocker.patch.object( + commit_reveal, + "convert_weights_and_uids_for_emit", + side_effect=Exception("Test Error"), + ) + + # Call + success, message = commit_reveal.commit_reveal_v3_extrinsic( + subtensor=subtensor, + wallet=fake_wallet, + netuid=fake_netuid, + uids=fake_uids, + weights=fake_weights, + ) + + # Asserts + assert success is False + assert "Test Error" in message diff --git a/tests/unit_tests/test_async_subtensor.py b/tests/unit_tests/test_async_subtensor.py index eb259649c7..d5b89c8d99 100644 --- a/tests/unit_tests/test_async_subtensor.py +++ b/tests/unit_tests/test_async_subtensor.py @@ -2365,6 +2365,46 @@ async def test_blocks_since_last_update_no_last_update(subtensor, mocker): assert result is None +@pytest.mark.asyncio +async def test_commit_reveal_enabled(subtensor, mocker): + """Test commit_reveal_enabled.""" + # Preps + netuid = 1 + block_hash = "block_hash" + mocked_get_hyperparameter = mocker.patch.object( + subtensor, "get_hyperparameter", return_value=mocker.AsyncMock() + ) + + # Call + result = await subtensor.commit_reveal_enabled(netuid, block_hash) + + # Assertions + mocked_get_hyperparameter.assert_awaited_once_with( + param_name="CommitRevealWeightsEnabled", block_hash=block_hash, netuid=netuid + ) + assert result is False + + +@pytest.mark.asyncio +async def test_get_subnet_reveal_period_epochs(subtensor, mocker): + """Test get_subnet_reveal_period_epochs.""" + # Preps + netuid = 1 + block_hash = "block_hash" + mocked_get_hyperparameter = mocker.patch.object( + subtensor, "get_hyperparameter", return_value=mocker.AsyncMock() + ) + + # Call + result = await subtensor.get_subnet_reveal_period_epochs(netuid, block_hash) + + # Assertions + mocked_get_hyperparameter.assert_awaited_once_with( + param_name="RevealPeriodEpochs", block_hash=block_hash, netuid=netuid + ) + assert result == mocked_get_hyperparameter.return_value + + @pytest.mark.asyncio async def test_transfer_success(subtensor, mocker): """Tests transfer when the transfer is successful.""" diff --git a/tests/unit_tests/test_dendrite.py b/tests/unit_tests/test_dendrite.py index 7bb97950c1..a23f959a31 100644 --- a/tests/unit_tests/test_dendrite.py +++ b/tests/unit_tests/test_dendrite.py @@ -22,6 +22,7 @@ from unittest.mock import MagicMock, Mock import aiohttp +from bittensor_wallet.mock import get_mock_wallet import pytest from bittensor.core.axon import Axon diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index 7d92488d31..dea9d66ed0 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -489,6 +489,40 @@ def test_hyperparameter_normalization( ########################### +def test_commit_reveal_enabled(subtensor, mocker): + """Test commit_reveal_enabled.""" + # Preps + netuid = 1 + block = 123 + mocked_get_hyperparameter = mocker.patch.object(subtensor, "_get_hyperparameter") + + # Call + result = subtensor.commit_reveal_enabled(netuid, block) + + # Assertions + mocked_get_hyperparameter.assert_called_once_with( + param_name="CommitRevealWeightsEnabled", block=block, netuid=netuid + ) + assert result is False + + +def test_get_subnet_reveal_period_epochs(subtensor, mocker): + """Test get_subnet_reveal_period_epochs.""" + # Preps + netuid = 1 + block = 123 + mocked_get_hyperparameter = mocker.patch.object(subtensor, "_get_hyperparameter") + + # Call + result = subtensor.get_subnet_reveal_period_epochs(netuid, block) + + # Assertions + mocked_get_hyperparameter.assert_called_once_with( + param_name="RevealPeriodEpochs", block=block, netuid=netuid + ) + assert result == mocked_get_hyperparameter.return_value + + # get_prometheus_info tests def test_get_prometheus_info_success(mocker, subtensor): """Test get_prometheus_info returns correct data when information is found.""" @@ -2798,3 +2832,45 @@ def test_unstake_multiple_success(mocker, subtensor): wait_for_finalization=False, ) assert result == mock_unstake_multiple_extrinsic.return_value + + +def test_set_weights_with_commit_reveal_enabled(subtensor, mocker): + """Test set_weights with commit_reveal_enabled is True.""" + # Preps + fake_wallet = mocker.Mock() + fake_netuid = 1 + fake_uids = [1, 5] + fake_weights = [0.1, 0.9] + fake_wait_for_inclusion = True + fake_wait_for_finalization = False + + mocked_commit_reveal_enabled = mocker.patch.object( + subtensor, "commit_reveal_enabled", return_value=True + ) + mocked_commit_reveal_v3_extrinsic = mocker.patch.object( + subtensor_module, "commit_reveal_v3_extrinsic" + ) + + # Call + result = subtensor.set_weights( + wallet=fake_wallet, + netuid=fake_netuid, + uids=fake_uids, + weights=fake_weights, + wait_for_inclusion=fake_wait_for_inclusion, + wait_for_finalization=fake_wait_for_finalization, + ) + + # Asserts + mocked_commit_reveal_enabled.assert_called_once_with(netuid=fake_netuid) + mocked_commit_reveal_v3_extrinsic.assert_called_once_with( + subtensor=subtensor, + wallet=fake_wallet, + netuid=fake_netuid, + uids=fake_uids, + weights=fake_weights, + version_key=subtensor_module.settings.version_as_int, + wait_for_inclusion=fake_wait_for_inclusion, + wait_for_finalization=fake_wait_for_finalization, + ) + assert result == mocked_commit_reveal_v3_extrinsic.return_value