diff --git a/.github/workflows/e2e-subtensor-tests.yaml b/.github/workflows/e2e-subtensor-tests.yaml index 0bc467a94d..157f2ad955 100644 --- a/.github/workflows/e2e-subtensor-tests.yaml +++ b/.github/workflows/e2e-subtensor-tests.yaml @@ -90,7 +90,7 @@ jobs: - name: Setup subtensor repo working-directory: ${{ github.workspace }}/subtensor - run: git checkout testnet + run: git checkout main - name: Run tests run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 0976792d33..b961f0bbc1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ # Changelog +## 8.5.2 /2025-01-17 + +## What's Changed +* Feat/use tx pool for set weights by @camfairchild in https://github.com/opentensor/bittensor/pull/2534 +* fix get_delegates result decoding by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2551 +* [SDK] Handle server connection limit by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2553 +* Backmerge master to staging post 851 by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2557 +* [SDK] Improve InvalidStatus handler by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2558 +* [SDK] Add async version of commit reveal v3 by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2560 +* Use apt-get instead of apt for scripts by @camfairchild in https://github.com/opentensor/bittensor/pull/2571 +* fix _do_stake incorrect arguments error in staking.py by @Assh-codes in https://github.com/opentensor/bittensor/pull/2574 +* Updates tests for btwallet 3.0.0 by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2540 +* Bumps cr3 FFI by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2583 + +## New Contributors +* @Assh-codes made their first contribution in https://github.com/opentensor/bittensor/pull/2574 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v8.5.1...v8.5.2 + ## 8.5.1 /2024-12-16 ## What's Changed diff --git a/Dockerfile b/Dockerfile index 5a355b5cf6..03c0533746 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,10 +12,10 @@ LABEL bittensor.image.authors="bittensor.com" \ ARG DEBIAN_FRONTEND=noninteractive # Update the base image -RUN apt update && apt upgrade -y +RUN apt-get update && apt-get upgrade -y # Install bittensor ## Install dependencies -RUN apt install -y curl sudo nano git htop netcat-openbsd wget unzip tmux apt-utils cmake build-essential +RUN apt-get install -y curl sudo nano git htop netcat-openbsd wget unzip tmux apt-utils cmake build-essential ## Upgrade pip RUN pip3 install --upgrade pip diff --git a/VERSION b/VERSION index e0741a834a..bd0b85a9b1 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.5.1 \ No newline at end of file +8.5.2 \ No newline at end of file diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index 2141055cca..43a0ff9252 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -22,6 +22,7 @@ SubnetHyperparameters, decode_account_id, ) +from bittensor.core.extrinsics.async_commit_reveal import commit_reveal_v3_extrinsic from bittensor.core.extrinsics.async_registration import register_extrinsic from bittensor.core.extrinsics.async_root import ( set_root_weights_extrinsic, @@ -1596,18 +1597,44 @@ 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】. """ + retries = 0 + success = False + if ( + uid := await self.get_uid_for_hotkey_on_subnet( + wallet.hotkey.ss58_address, netuid + ) + ) is None: + return ( + False, + f"Hotkey {wallet.hotkey.ss58_address} not registered in subnet {netuid}", + ) + if (await self.commit_reveal_enabled(netuid=netuid)) is True: # go with `commit reveal v3` extrinsic - raise NotImplementedError( - "Not implemented yet for AsyncSubtensor. Coming soon." - ) + message = "No attempt made. Perhaps it is too soon to commit weights!" + while ( + await self.blocks_since_last_update(netuid, uid) + > await self.weights_rate_limit(netuid) + and retries < max_retries + and success is False + ): + logging.info( + f"Committing weights for subnet #{netuid}. Attempt {retries + 1} of {max_retries}." + ) + success, message = await 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, + ) + retries += 1 + return success, message 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 diff --git a/bittensor/core/extrinsics/async_commit_reveal.py b/bittensor/core/extrinsics/async_commit_reveal.py new file mode 100644 index 0000000000..b1c2bea094 --- /dev/null +++ b/bittensor/core/extrinsics/async_commit_reveal.py @@ -0,0 +1,152 @@ +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.settings import version_as_int +from bittensor.utils import format_error_message +from bittensor.utils.btlogging import logging +from bittensor.utils.weight_utils import convert_weights_and_uids_for_emit + +if TYPE_CHECKING: + from bittensor_wallet import Wallet + from bittensor.core.async_subtensor import AsyncSubtensor + from bittensor.utils.registration import torch + + +async def _do_commit_reveal_v3( + subtensor: "AsyncSubtensor", + 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: + subtensor: An instance of the Subtensor class. + 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 = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="commit_crv3_weights", + call_params={ + "netuid": netuid, + "commit": commit, + "reveal_round": reveal_round, + }, + ) + extrinsic = await subtensor.substrate.create_signed_extrinsic( + call=call, + keypair=wallet.hotkey, + ) + + response = await subtensor.substrate.submit_extrinsic( + subtensor=subtensor, + 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." + + if await response.is_success: + return True, None + + return False, format_error_message(await response.error_message) + + +async def commit_reveal_v3_extrinsic( + subtensor: "AsyncSubtensor", + 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 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 = await subtensor.substrate.get_block_number(None) + subnet_hyperparameters = await subtensor.get_subnet_hyperparameters(netuid) + 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 = await _do_commit_reveal_v3( + subtensor=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 not True: + logging.error(message) + return False, message + + logging.success( + f"[green]Finalized![/green] Weights commited with reveal round [blue]{reveal_round}[/blue]." + ) + return True, f"reveal_round:{reveal_round}" + + except Exception as e: + logging.error(f":cross_mark: [red]Failed. Error:[/red] {e}") + return False, str(e) diff --git a/bittensor/core/extrinsics/async_weights.py b/bittensor/core/extrinsics/async_weights.py index 572266c3f6..82934edfc5 100644 --- a/bittensor/core/extrinsics/async_weights.py +++ b/bittensor/core/extrinsics/async_weights.py @@ -58,11 +58,17 @@ async def _do_set_weights( "version_key": version_key, }, ) + + next_nonce = await subtensor.substrate.get_account_next_index( + wallet.hotkey.ss58_address + ) + # Period dictates how long the extrinsic will stay as part of waiting pool extrinsic = await subtensor.substrate.create_signed_extrinsic( call=call, keypair=wallet.hotkey, era={"period": 5}, + nonce=next_nonce, ) response = await subtensor.substrate.submit_extrinsic( extrinsic, @@ -180,9 +186,15 @@ async def _do_commit_weights( "commit_hash": commit_hash, }, ) + + next_nonce = await subtensor.substrate.get_account_next_index( + wallet.hotkey.ss58_address + ) + extrinsic = await subtensor.substrate.create_signed_extrinsic( call=call, keypair=wallet.hotkey, + nonce=next_nonce, ) response = await subtensor.substrate.submit_extrinsic( substrate=subtensor.substrate, diff --git a/bittensor/core/extrinsics/commit_weights.py b/bittensor/core/extrinsics/commit_weights.py index 0ad6ad5add..4136e0c348 100644 --- a/bittensor/core/extrinsics/commit_weights.py +++ b/bittensor/core/extrinsics/commit_weights.py @@ -66,9 +66,11 @@ def do_commit_weights( "commit_hash": commit_hash, }, ) + next_nonce = self.get_account_next_index(wallet.hotkey.ss58_address) extrinsic = self.substrate.create_signed_extrinsic( call=call, keypair=wallet.hotkey, + nonce=next_nonce, ) response = submit_extrinsic( subtensor=self, diff --git a/bittensor/core/extrinsics/set_weights.py b/bittensor/core/extrinsics/set_weights.py index 64f318f8d6..9cb291a299 100644 --- a/bittensor/core/extrinsics/set_weights.py +++ b/bittensor/core/extrinsics/set_weights.py @@ -76,11 +76,13 @@ def do_set_weights( "version_key": version_key, }, ) + next_nonce = self.get_account_next_index(wallet.hotkey.ss58_address) # Period dictates how long the extrinsic will stay as part of waiting pool extrinsic = self.substrate.create_signed_extrinsic( call=call, keypair=wallet.hotkey, era={"period": period}, + nonce=next_nonce, ) response = submit_extrinsic( self, diff --git a/bittensor/core/extrinsics/staking.py b/bittensor/core/extrinsics/staking.py index 81bbc39745..771d73dc76 100644 --- a/bittensor/core/extrinsics/staking.py +++ b/bittensor/core/extrinsics/staking.py @@ -122,6 +122,7 @@ def __do_add_stake_single( raise NotDelegateError("Hotkey: {} is not a delegate.".format(hotkey_ss58)) success = _do_stake( + self=subtensor, wallet=wallet, hotkey_ss58=hotkey_ss58, amount=amount, diff --git a/bittensor/core/settings.py b/bittensor/core/settings.py index 04d94436ef..c5e9182678 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.5.1" +__version__ = "8.5.2" import os import re diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index ff17c8e896..1a29694b99 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -16,7 +16,9 @@ from scalecodec.exceptions import RemainingScaleBytesNotEmptyException from scalecodec.type_registry import load_type_registry_preset from scalecodec.types import ScaleType +from substrateinterface import Keypair from substrateinterface.base import QueryMapResult, SubstrateInterface +from websockets.exceptions import InvalidStatus from websockets.sync import client as ws_client from bittensor.core import settings @@ -233,6 +235,7 @@ def _get_substrate(self, force: bool = False): open_timeout=self._connection_timeout, max_size=2**32, ) + self.substrate = SubstrateInterface( ss58_format=settings.SS58_FORMAT, use_remote_preset=True, @@ -244,19 +247,27 @@ def _get_substrate(self, force: bool = False): f"Connected to {self.network} network and {self.chain_endpoint}." ) - except (ConnectionRefusedError, ssl.SSLError) as error: - logging.error( - f"Could not connect to {self.network} network with {self.chain_endpoint} chain endpoint.", + except ConnectionRefusedError as error: + logging.critical( + f"[red]Could not connect to[/red] [blue]{self.network}[/blue] [red]network with[/red] [blue]{self.chain_endpoint}[/blue] [red]chain endpoint.[/red]", ) raise ConnectionRefusedError(error.args) - except ssl.SSLError as e: + + except ssl.SSLError as error: logging.critical( "SSL error occurred. To resolve this issue, run the following command in your terminal:" ) logging.critical("[blue]sudo python -m bittensor certifi[/blue]") raise RuntimeError( "SSL configuration issue, please follow the instructions above." - ) from e + ) from error + + except InvalidStatus as error: + logging.critical( + f"Error [red]'{error.response.reason_phrase}'[/red] with status code [red]{error.response.status_code}[/red]." + ) + logging.debug(f"Server response is '{error.response}'.") + raise @staticmethod def config() -> "Config": @@ -661,6 +672,16 @@ def query_module( ), ) + @networking.ensure_connected + def get_account_next_index(self, address: str) -> int: + """ + Returns the next nonce for an account, taking into account the transaction pool. + """ + if not self.substrate.supports_rpc_method("account_nextIndex"): + raise Exception("account_nextIndex not supported") + + return self.substrate.rpc_request("account_nextIndex", [address])["result"] + # Common subtensor methods ========================================================================================= def metagraph( self, netuid: int, lite: bool = True, block: Optional[int] = None @@ -1512,9 +1533,11 @@ def get_transfer_fee( call_params={"dest": dest, "value": value.rao}, ) + temp_keypair = Keypair(ss58_address=wallet.coldkeypub.ss58_address) + try: payment_info = self.substrate.get_payment_info( - call=call, keypair=wallet.coldkeypub + call=call, keypair=temp_keypair ) except Exception as e: logging.error(f"[red]Failed to get payment info.[/red] {e}") @@ -1764,7 +1787,7 @@ def get_delegates(self, block: Optional[int] = None) -> list[DelegateInfo]: if not (result := json_body.get("result", None)): return [] - return DelegateInfo.list_from_vec_u8(result) + return DelegateInfo.list_from_vec_u8(bytes(result)) def is_hotkey_delegate(self, hotkey_ss58: str, block: Optional[int] = None) -> bool: """ @@ -1816,7 +1839,13 @@ def set_weights( """ retries = 0 success = False - uid = self.get_uid_for_hotkey_on_subnet(wallet.hotkey.ss58_address, netuid) + if ( + uid := self.get_uid_for_hotkey_on_subnet(wallet.hotkey.ss58_address, netuid) + ) is None: + return ( + False, + f"Hotkey {wallet.hotkey.ss58_address} not registered in subnet {netuid}", + ) if self.commit_reveal_enabled(netuid=netuid) is True: # go with `commit reveal v3` extrinsic diff --git a/bittensor/utils/async_substrate_interface.py b/bittensor/utils/async_substrate_interface.py index 05fd963212..eeb5eb1068 100644 --- a/bittensor/utils/async_substrate_interface.py +++ b/bittensor/utils/async_substrate_interface.py @@ -13,6 +13,7 @@ from hashlib import blake2b from typing import Optional, Any, Union, Callable, Awaitable, cast, TYPE_CHECKING +import asyncstdlib as a from async_property import async_property from bittensor_wallet import Keypair from bt_decode import PortableRegistry, decode as decode_by_type_string, MetadataV15 @@ -1704,6 +1705,24 @@ def make_payload(id_: str, method: str, params: list) -> dict: "payload": {"jsonrpc": "2.0", "method": method, "params": params}, } + @a.lru_cache(maxsize=512) # RPC methods are unlikely to change often + async def supports_rpc_method(self, name: str) -> bool: + """ + Check if substrate RPC supports given method + Parameters + ---------- + name: name of method to check + + Returns + ------- + bool + """ + result = await self.rpc_request("rpc_methods", []).get("result") + if result: + self.config["rpc_methods"] = result.get("methods", []) + + return name in self.config["rpc_methods"] + async def rpc_request( self, method: str, @@ -2296,10 +2315,33 @@ async def get_account_nonce(self, account_address: str) -> int: Returns: Nonce for given account address """ - nonce_obj = await self.runtime_call( - "AccountNonceApi", "account_nonce", [account_address] - ) - return nonce_obj.value + if await self.supports_rpc_method("state_call"): + nonce_obj = await self.runtime_call( + "AccountNonceApi", "account_nonce", [account_address] + ) + return nonce_obj + else: + response = await self.query( + module="System", storage_function="Account", params=[account_address] + ) + return response["nonce"] + + async def get_account_next_index(self, account_address: str) -> int: + """ + Returns next index for the given account address, taking into account the transaction pool. + + Args: + account_address: SS58 formatted address + + Returns: + Next index for the given account address + """ + if not await self.supports_rpc_method("account_nextIndex"): + # Unlikely to happen, this is a common RPC method + raise Exception("account_nextIndex not supported") + + nonce_obj = await self.rpc_request("account_nextIndex", [account_address]) + return nonce_obj["result"] async def get_metadata_constant(self, module_name, constant_name, block_hash=None): """ diff --git a/requirements/prod.txt b/requirements/prod.txt index c57ce611f9..c447d8093f 100644 --- a/requirements/prod.txt +++ b/requirements/prod.txt @@ -1,6 +1,7 @@ wheel setuptools~=70.0.0 aiohttp~=3.9 +asyncstdlib~=3.13.0 async-property==0.2.2 bittensor-cli bt-decode==0.4.0 @@ -24,5 +25,5 @@ scalecodec==1.2.11 substrate-interface~=1.7.9 uvicorn websockets>=14.1 -bittensor-wallet>=2.1.3 -bittensor-commit-reveal>=0.1.0 +bittensor-commit-reveal>=0.2.0 +bittensor-wallet>=3.0.0 diff --git a/scripts/install.sh b/scripts/install.sh index 5111d75afb..9021fb499c 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -175,9 +175,9 @@ mac_install_bittensor() { OS="$(uname)" if [[ "$OS" == "Linux" ]]; then - which -s apt + which -s apt-get if [[ $? == 0 ]] ; then - abort "This linux based install requires apt. To run with other distros (centos, arch, etc), you will need to manually install the requirements" + abort "This linux based install requires apt-get. To run with other distros (centos, arch, etc), you will need to manually install the requirements" fi echo """ diff --git a/tests/e2e_tests/test_commit_weights.py b/tests/e2e_tests/test_commit_weights.py index c6737e01ae..8a5371283e 100644 --- a/tests/e2e_tests/test_commit_weights.py +++ b/tests/e2e_tests/test_commit_weights.py @@ -2,6 +2,9 @@ import numpy as np import pytest + +import asyncio + from bittensor.core.subtensor import Subtensor from bittensor.utils.balance import Balance from bittensor.utils.weight_utils import convert_weights_and_uids_for_emit @@ -171,3 +174,158 @@ async def test_commit_and_reveal_weights_legacy(local_chain): weight_vals[0] == revealed_weights.value[0][1] ), f"Incorrect revealed weights. Expected: {weights[0]}, Actual: {revealed_weights.value[0][1]}" print("✅ Passed test_commit_and_reveal_weights") + + +@pytest.mark.asyncio +async def test_commit_weights_uses_next_nonce(local_chain): + """ + Tests that commiting weights doesn't re-use a nonce in the transaction pool. + + 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 the commit_reveal interval and rate limit + 5. Commit weights three times + 6. Assert that all commits succeeded + Raises: + AssertionError: If any of the checks or verifications fail + """ + netuid = 1 + utils.EXTRINSIC_SUBMISSION_TIMEOUT = 12 # handle fast blocks + print("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" + + 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" + + # Stake to become to top neuron after the first epoch + add_stake(local_chain, alice_wallet, Balance.from_tao(100_000)) + + # 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" + + assert subtensor.get_subnet_hyperparameters( + netuid=netuid, + ).commit_reveal_weights_enabled, "Failed to enable commit/reveal" + + # Lower the commit_reveal interval + assert sudo_set_hyperparameter_values( + local_chain, + alice_wallet, + call_function="sudo_set_commit_reveal_weights_interval", + call_params={"netuid": netuid, "interval": "1"}, + return_error_message=True, + ) + + assert ( + subtensor.get_subnet_hyperparameters( + netuid=netuid + ).commit_reveal_weights_interval + == 1 + ), "Failed to set commit/reveal periods" + + assert ( + subtensor.weights_rate_limit(netuid=netuid) > 0 + ), "Weights rate limit is below 0" + # Lower the rate limit + 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, + ) + + 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 + + # Commit-reveal values + uids = np.array([0], dtype=np.int64) + weights = np.array([0.1], dtype=np.float32) + salt = [18, 179, 107, 0, 165, 211, 141, 197] + weight_uids, weight_vals = convert_weights_and_uids_for_emit( + uids=uids, weights=weights + ) + + # Make a second salt + salt2 = salt.copy() + salt2[0] += 1 # Increment the first byte to produce a different commit hash + + # Make a third salt + salt3 = salt.copy() + salt3[0] += 2 # Increment the first byte to produce a different commit hash + + # Commit all three salts + success, message = subtensor.commit_weights( + alice_wallet, + netuid, + salt=salt, + uids=weight_uids, + weights=weight_vals, + wait_for_inclusion=False, # Don't wait for inclusion, we are testing the nonce when there is a tx in the pool + wait_for_finalization=False, + ) + + assert success is True + + success, message = subtensor.commit_weights( + alice_wallet, + netuid, + salt=salt2, + uids=weight_uids, + weights=weight_vals, + wait_for_inclusion=False, + wait_for_finalization=False, + ) + + assert success is True + + # Commit the third salt + success, message = subtensor.commit_weights( + alice_wallet, + netuid, + salt=salt3, + uids=weight_uids, + weights=weight_vals, + wait_for_inclusion=False, + wait_for_finalization=False, + ) + + assert success is True + + # Wait a few blocks + await asyncio.sleep(2) # Wait for the txs to be included in the chain + + # Query the WeightCommits storage map for all three salts + weight_commits = subtensor.query_module( + module="SubtensorModule", + name="WeightCommits", + params=[netuid, alice_wallet.hotkey.ss58_address], + ) + # Assert that the committed weights are set correctly + assert weight_commits.value is not None, "Weight commit not found in storage" + commit_hash, commit_block, reveal_block, expire_block = weight_commits.value[0] + assert commit_block > 0, f"Invalid block number: {commit_block}" + + # Check for three commits in the WeightCommits storage map + assert len(weight_commits.value) == 3, "Expected 3 weight commits" diff --git a/tests/e2e_tests/test_incentive.py b/tests/e2e_tests/test_incentive.py index ab557a56fd..cfafef42b5 100644 --- a/tests/e2e_tests/test_incentive.py +++ b/tests/e2e_tests/test_incentive.py @@ -15,6 +15,7 @@ templates_repo, ) from bittensor.utils.balance import Balance +from bittensor.core.extrinsics import utils from bittensor.core.extrinsics.set_weights import do_set_weights from bittensor.core.metagraph import Metagraph @@ -40,6 +41,8 @@ async def test_incentive(local_chain): print("Testing test_incentive") netuid = 1 + utils.EXTRINSIC_SUBMISSION_TIMEOUT = 12 # handle fast blocks + # Register root as Alice - the subnet owner and validator alice_keypair, alice_wallet = setup_wallet("//Alice") register_subnet(local_chain, alice_wallet) diff --git a/tests/e2e_tests/test_set_weights.py b/tests/e2e_tests/test_set_weights.py new file mode 100644 index 0000000000..edf208ba8d --- /dev/null +++ b/tests/e2e_tests/test_set_weights.py @@ -0,0 +1,157 @@ +import numpy as np +import pytest + +import asyncio + +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 bittensor.core.extrinsics import utils +from tests.e2e_tests.utils.chain_interactions import ( + add_stake, + register_subnet, + sudo_set_hyperparameter_bool, + sudo_set_hyperparameter_values, + sudo_set_admin_utils, +) +from tests.e2e_tests.utils.e2e_test_utils import setup_wallet + + +@pytest.mark.asyncio +async def test_set_weights_uses_next_nonce(local_chain): + """ + Tests that setting weights doesn't re-use a nonce in the transaction pool. + + Steps: + 1. Register three subnets through Alice + 2. Register Alice's neuron on each subnet and add stake + 3. Verify Alice has a vpermit on each subnet + 4. Lower the set weights rate limit on each subnet + 5. Set weights on each subnet + 6. Assert that all the set weights succeeded + Raises: + AssertionError: If any of the checks or verifications fail + """ + netuids = [1, 2] + utils.EXTRINSIC_SUBMISSION_TIMEOUT = 12 # handle fast blocks + print("Testing test_set_weights_uses_next_nonce") + # Register root as Alice + keypair, alice_wallet = setup_wallet("//Alice") + + # Lower the network registration rate limit and cost + sudo_set_admin_utils( + local_chain, + alice_wallet, + call_function="sudo_set_network_rate_limit", + call_params={"rate_limit": "0"}, # No limit + return_error_message=True, + ) + # Set lock reduction interval + sudo_set_admin_utils( + local_chain, + alice_wallet, + call_function="sudo_set_lock_reduction_interval", + call_params={"interval": "1"}, # 1 block # reduce lock every block + return_error_message=True, + ) + # Try to register the subnets + for _ in netuids: + assert register_subnet( + local_chain, alice_wallet + ), "Unable to register the subnet" + + # Verify all subnets created successfully + assert local_chain.query( + "SubtensorModule", "NetworksAdded", [3] + ).serialize(), "Subnet wasn't created successfully" + + subtensor = Subtensor(network="ws://localhost:9945") + + for netuid in netuids: + # Allow registration on the subnet + assert sudo_set_hyperparameter_values( + local_chain, + alice_wallet, + "sudo_set_network_registration_allowed", + {"netuid": netuid, "registration_allowed": True}, + return_error_message=True, + ) + + # This should give a gap for the calls above to be included in the chain + await asyncio.sleep(2) + + for netuid in netuids: + # Register Alice to the subnet + assert subtensor.burned_register( + alice_wallet, netuid + ), f"Unable to register Alice as a neuron on SN{netuid}" + + # Stake to become to top neuron after the first epoch + add_stake(local_chain, alice_wallet, Balance.from_tao(100_000)) + + # Set weight hyperparameters per subnet + for netuid in netuids: + assert sudo_set_hyperparameter_bool( + local_chain, + alice_wallet, + "sudo_set_commit_reveal_weights_enabled", + False, + netuid, + ), "Unable to enable commit reveal on the subnet" + + assert not subtensor.get_subnet_hyperparameters( + netuid=netuid, + ).commit_reveal_weights_enabled, "Failed to enable commit/reveal" + + assert ( + subtensor.weights_rate_limit(netuid=netuid) > 0 + ), "Weights rate limit is below 0" + # Lower the rate limit + 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, + ) + + 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 + + # Weights values + 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 + ) + + # Set weights for each subnet + for netuid in netuids: + success, message = subtensor.set_weights( + alice_wallet, + netuid, + uids=weight_uids, + weights=weight_vals, + wait_for_inclusion=False, # Don't wait for inclusion, we are testing the nonce when there is a tx in the pool + wait_for_finalization=False, + ) + + assert success is True, f"Failed to set weights for subnet {netuid}" + + # Wait for the txs to be included in the chain + await asyncio.sleep(4) + + for netuid in netuids: + # Query the Weights storage map for all three subnets + weights = subtensor.query_module( + module="SubtensorModule", + name="Weights", + params=[netuid, 0], # Alice should be the only UID + ) + + assert weights is not None, f"Weights not found for subnet {netuid}" + assert weights == list( + zip(weight_uids, weight_vals) + ), f"Weights do not match for subnet {netuid}" diff --git a/tests/e2e_tests/test_transfer.py b/tests/e2e_tests/test_transfer.py index 5a18db386e..7b95ae75bd 100644 --- a/tests/e2e_tests/test_transfer.py +++ b/tests/e2e_tests/test_transfer.py @@ -1,6 +1,9 @@ from bittensor.core.subtensor import Subtensor from bittensor.utils.balance import Balance from tests.e2e_tests.utils.e2e_test_utils import setup_wallet +from bittensor import logging + +logging.set_trace() def test_transfer(local_chain): diff --git a/tests/e2e_tests/utils/chain_interactions.py b/tests/e2e_tests/utils/chain_interactions.py index bae60c5443..bd5829e219 100644 --- a/tests/e2e_tests/utils/chain_interactions.py +++ b/tests/e2e_tests/utils/chain_interactions.py @@ -215,6 +215,35 @@ def sudo_set_admin_utils( 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 + + +async def root_set_subtensor_hyperparameter_values( + substrate: "SubstrateInterface", + wallet: "Wallet", + call_function: str, + call_params: dict, + return_error_message: bool = False, +) -> Union[bool, tuple[bool, Optional[str]]]: + """ + Sets liquid alpha values using AdminUtils. Mimics setting hyperparams + """ + call = substrate.compose_call( + call_module="SubtensorModule", + call_function=call_function, + call_params=call_params, + ) + extrinsic = substrate.create_signed_extrinsic(call=call, keypair=wallet.coldkey) response = substrate.submit_extrinsic( extrinsic, diff --git a/tests/integration_tests/utils/test_init.py b/tests/integration_tests/utils/test_init.py index 000e94b3d8..59d613ca89 100644 --- a/tests/integration_tests/utils/test_init.py +++ b/tests/integration_tests/utils/test_init.py @@ -7,33 +7,51 @@ from bittensor import utils -def test_unlock_key(monkeypatch): +def test_unlock_through_env(): # Ensure path is clean before we run the tests - if os.path.exists("/tmp/bittensor-tests-wallets"): + if os.path.exists("/tmp/bittensor-tests-wallets/"): shutil.rmtree("/tmp/bittensor-tests-wallets") wallet = Wallet(path="/tmp/bittensor-tests-wallets") + + # Set up the coldkey cold_kf = Keyfile("/tmp/bittensor-tests-wallets/default/coldkey", name="default") kp = Keypair.create_from_mnemonic( "stool feel open east woman high can denial forget screen trust salt" ) cold_kf.set_keypair(kp, False, False) cold_kf.encrypt("1234password1234") - hot_kf = Keyfile("/tmp/bittensor-tests-wallets/default/hotkey", name="default") + + # Set up the hotkey + hot_kf = Keyfile( + "/tmp/bittensor-tests-wallets/default/hotkeys/default", name="default" + ) hkp = Keypair.create_from_mnemonic( "stool feel open east woman high can denial forget screen trust salt" ) hot_kf.set_keypair(hkp, False, False) hot_kf.encrypt("1234hotkey1234") - monkeypatch.setattr("getpass.getpass", lambda _: "badpassword1234") + + # Save a wrong password to the environment for CK + cold_kf.save_password_to_env("badpassword") result = utils.unlock_key(wallet) assert result.success is False - monkeypatch.setattr("getpass.getpass", lambda _: "1234password1234") + + # Save correct password to the environment for CK + cold_kf.save_password_to_env("1234password1234") result = utils.unlock_key(wallet) assert result.success is True - monkeypatch.setattr("getpass.getpass", lambda _: "badpassword1234") + + # Save a wrong password to the environment for HK + hot_kf.save_password_to_env("badpassword") result = utils.unlock_key(wallet, "hotkey") assert result.success is False + + # Save correct password to the environment for HK + hot_kf.save_password_to_env("1234hotkey1234") + result = utils.unlock_key(wallet, "hotkey") + assert result.success is True + with pytest.raises(ValueError): utils.unlock_key(wallet, "mycoldkey") diff --git a/tests/unit_tests/extrinsics/test_async_commit_reveal.py b/tests/unit_tests/extrinsics/test_async_commit_reveal.py new file mode 100644 index 0000000000..ce4a5ccbfa --- /dev/null +++ b/tests/unit_tests/extrinsics/test_async_commit_reveal.py @@ -0,0 +1,373 @@ +from bittensor.core import async_subtensor as subtensor_module +from bittensor.core.chain_data import SubnetHyperparameters +from bittensor.core.async_subtensor import AsyncSubtensor +from bittensor.core.extrinsics import async_commit_reveal +import pytest +import torch +import numpy as np + + +@pytest.fixture +def subtensor(mocker): + fake_substrate = mocker.AsyncMock() + fake_substrate.websocket.sock.getsockopt.return_value = 0 + mocker.patch.object( + subtensor_module, "AsyncSubstrateInterface", return_value=fake_substrate + ) + yield AsyncSubtensor() + + +@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, + ) + + +@pytest.mark.asyncio +async 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( + subtensor.substrate, "submit_extrinsic" + ) + + # Call + result = await async_commit_reveal._do_commit_reveal_v3( + subtensor=subtensor, + wallet=fake_wallet, + netuid=fake_netuid, + commit=fake_commit, + reveal_round=fake_reveal_round, + ) + + # Asserts + mocked_compose_call.assert_awaited_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_awaited_once_with( + call=mocked_compose_call.return_value, keypair=fake_wallet.hotkey + ) + mocked_submit_extrinsic.assert_awaited_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.") + + +@pytest.mark.asyncio +async 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( + subtensor.substrate, + "submit_extrinsic", + return_value=mocker.Mock( + is_success=mocker.AsyncMock(return_value=False)(), + error_message=mocker.AsyncMock(return_value="Mocked error")(), + ), + ) + + mocked_format_error_message = mocker.patch.object( + async_commit_reveal, "format_error_message", return_value="Formatted error" + ) + + # Call + result = await async_commit_reveal._do_commit_reveal_v3( + subtensor=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_awaited_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_awaited_once_with( + call=mocked_compose_call.return_value, keypair=fake_wallet.hotkey + ) + mocked_submit_extrinsic.assert_awaited_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") + assert result == (False, "Formatted error") + + +@pytest.mark.asyncio +async 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 + + mocked_uids = mocker.Mock() + mocked_weights = mocker.Mock() + mocked_convert_weights_and_uids_for_emit = mocker.patch.object( + async_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( + async_commit_reveal, + "get_encrypted_commit", + return_value=(fake_commit_for_reveal, fake_reveal_round), + ) + mock_do_commit_reveal_v3 = mocker.patch.object( + async_commit_reveal, "_do_commit_reveal_v3", return_value=(True, "Success") + ) + mock_block = mocker.patch.object( + subtensor.substrate, "get_block_number", return_value=1 + ) + mock_hyperparams = mocker.patch.object( + subtensor, + "get_subnet_hyperparameters", + return_value=hyperparams, + ) + + # Call + success, message = await async_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=async_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_awaited_once_with( + subtensor=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, + ) + + +@pytest.mark.asyncio +async 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) + + mock_convert = mocker.patch.object( + async_commit_reveal, + "convert_weights_and_uids_for_emit", + return_value=(fake_uids, fake_weights), + ) + mock_encode_drand = mocker.patch.object( + async_commit_reveal, "get_encrypted_commit", return_value=(b"commit", 0) + ) + mock_do_commit = mocker.patch.object( + async_commit_reveal, "_do_commit_reveal_v3", return_value=(True, "Committed!") + ) + mocker.patch.object(subtensor.substrate, "get_block_number", return_value=1) + mocker.patch.object( + subtensor, + "get_subnet_hyperparameters", + return_value=hyperparams, + ) + + # Call + success, message = await async_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_awaited_once() + + +@pytest.mark.asyncio +async 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( + async_commit_reveal, + "convert_weights_and_uids_for_emit", + return_value=(fake_uids, fake_weights), + ) + mocker.patch.object( + async_commit_reveal, + "get_encrypted_commit", + return_value=(fake_commit_for_reveal, fake_reveal_round), + ) + mock_do_commit_reveal_v3 = mocker.patch.object( + async_commit_reveal, "_do_commit_reveal_v3", return_value=(False, "Failed") + ) + mocker.patch.object(subtensor.substrate, "get_block_number", return_value=1) + mocker.patch.object( + subtensor, + "get_subnet_hyperparameters", + return_value=hyperparams, + ) + + # Call + success, message = await async_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_awaited_once_with( + subtensor=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, + ) + + +@pytest.mark.asyncio +async 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( + async_commit_reveal, + "convert_weights_and_uids_for_emit", + side_effect=Exception("Test Error"), + ) + + # Call + success, message = await async_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/extrinsics/test_commit_weights.py b/tests/unit_tests/extrinsics/test_commit_weights.py index 35a1d4d426..57d78a8013 100644 --- a/tests/unit_tests/extrinsics/test_commit_weights.py +++ b/tests/unit_tests/extrinsics/test_commit_weights.py @@ -53,9 +53,10 @@ def test_do_commit_weights(subtensor, mocker): }, ) - subtensor.substrate.create_signed_extrinsic.assert_called_once_with( - call=subtensor.substrate.compose_call.return_value, keypair=fake_wallet.hotkey - ) + subtensor.substrate.create_signed_extrinsic.assert_called_once() + _, kwargs = subtensor.substrate.create_signed_extrinsic.call_args + assert kwargs["call"] == subtensor.substrate.compose_call.return_value + assert kwargs["keypair"] == fake_wallet.hotkey subtensor.substrate.submit_extrinsic.assert_called_once_with( subtensor.substrate.create_signed_extrinsic.return_value, diff --git a/tests/unit_tests/extrinsics/test_set_weights.py b/tests/unit_tests/extrinsics/test_set_weights.py index f447915d2f..6c070bf5c4 100644 --- a/tests/unit_tests/extrinsics/test_set_weights.py +++ b/tests/unit_tests/extrinsics/test_set_weights.py @@ -135,17 +135,11 @@ def test_do_set_weights_is_success(mock_subtensor, mocker): }, ) - mock_subtensor.substrate.create_signed_extrinsic.assert_called_once_with( - call=mock_subtensor.substrate.compose_call.return_value, - keypair=fake_wallet.hotkey, - era={"period": 5}, - ) - - mock_subtensor.substrate.submit_extrinsic.assert_called_once_with( - mock_subtensor.substrate.create_signed_extrinsic.return_value, - wait_for_inclusion=fake_wait_for_inclusion, - wait_for_finalization=fake_wait_for_finalization, - ) + mock_subtensor.substrate.create_signed_extrinsic.assert_called_once() + _, kwargs = mock_subtensor.substrate.create_signed_extrinsic.call_args + assert kwargs["call"] == mock_subtensor.substrate.compose_call.return_value + assert kwargs["keypair"] == fake_wallet.hotkey + assert kwargs["era"] == {"period": 5} mock_subtensor.substrate.submit_extrinsic.return_value.process_events.assert_called_once() assert result == (True, "Successfully set weights.") @@ -189,11 +183,11 @@ def test_do_set_weights_is_not_success(mock_subtensor, mocker): }, ) - mock_subtensor.substrate.create_signed_extrinsic.assert_called_once_with( - call=mock_subtensor.substrate.compose_call.return_value, - keypair=fake_wallet.hotkey, - era={"period": 5}, - ) + mock_subtensor.substrate.create_signed_extrinsic.assert_called_once() + _, kwargs = mock_subtensor.substrate.create_signed_extrinsic.call_args + assert kwargs["call"] == mock_subtensor.substrate.compose_call.return_value + assert kwargs["keypair"] == fake_wallet.hotkey + assert kwargs["era"] == {"period": 5} mock_subtensor.substrate.submit_extrinsic.assert_called_once_with( mock_subtensor.substrate.create_signed_extrinsic.return_value, @@ -242,11 +236,11 @@ def test_do_set_weights_no_waits(mock_subtensor, mocker): }, ) - mock_subtensor.substrate.create_signed_extrinsic.assert_called_once_with( - call=mock_subtensor.substrate.compose_call.return_value, - keypair=fake_wallet.hotkey, - era={"period": 5}, - ) + mock_subtensor.substrate.create_signed_extrinsic.assert_called_once() + _, kwargs = mock_subtensor.substrate.create_signed_extrinsic.call_args + assert kwargs["call"] == mock_subtensor.substrate.compose_call.return_value + assert kwargs["keypair"] == fake_wallet.hotkey + assert kwargs["era"] == {"period": 5} mock_subtensor.substrate.submit_extrinsic.assert_called_once_with( mock_subtensor.substrate.create_signed_extrinsic.return_value, diff --git a/tests/unit_tests/test_async_subtensor.py b/tests/unit_tests/test_async_subtensor.py index d5b89c8d99..bf672beb56 100644 --- a/tests/unit_tests/test_async_subtensor.py +++ b/tests/unit_tests/test_async_subtensor.py @@ -2614,7 +2614,9 @@ async def test_set_weights_success(subtensor, mocker): fake_weights = [0.3, 0.5, 0.2] max_retries = 1 - mocked_get_uid_for_hotkey_on_subnet = mocker.AsyncMock(return_value=fake_netuid) + mocked_get_uid_for_hotkey_on_subnet = mocker.patch.object( + subtensor, "get_uid_for_hotkey_on_subnet" + ) subtensor.get_uid_for_hotkey_on_subnet = mocked_get_uid_for_hotkey_on_subnet mocked_blocks_since_last_update = mocker.AsyncMock(return_value=2) diff --git a/tests/unit_tests/test_config.py b/tests/unit_tests/test_config.py index 53754764e1..fc0ad7c6f7 100644 --- a/tests/unit_tests/test_config.py +++ b/tests/unit_tests/test_config.py @@ -19,9 +19,14 @@ def test_py_config_parsed_successfully_rust_wallet(): config.wallet.hotkey = "new_hotkey" config.wallet.path = "/some/not_default/path" + # Pass in the whole bittensor config wallet = bittensor.wallet(config=config) - - # Asserts assert wallet.name == config.wallet.name assert wallet.hotkey_str == config.wallet.hotkey assert wallet.path == config.wallet.path + + # Pass in only the btwallet's config + wallet_two = bittensor.wallet(config=config.wallet) + assert wallet_two.name == config.wallet.name + assert wallet_two.hotkey_str == config.wallet.hotkey + assert wallet_two.path == config.wallet.path diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index bf156e2122..7cd0ed19f5 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -1739,6 +1739,8 @@ def test_get_transfer_fee(subtensor, mocker): fake_payment_info = {"partialFee": int(2e10)} subtensor.substrate.get_payment_info.return_value = fake_payment_info + mocker.patch.object(subtensor_module, "Keypair", return_value=mocker.MagicMock()) + # Call result = subtensor.get_transfer_fee(wallet=fake_wallet, dest=fake_dest, value=value) @@ -1751,7 +1753,7 @@ def test_get_transfer_fee(subtensor, mocker): subtensor.substrate.get_payment_info.assert_called_once_with( call=subtensor.substrate.compose_call.return_value, - keypair=fake_wallet.coldkeypub, + keypair=subtensor_module.Keypair.return_value, ) assert result == 2e10 @@ -2548,7 +2550,7 @@ def test_get_delegates_success(mocker, subtensor): fake_block = 123 fake_block_hash = "0xabc123" fake_json_body = { - "result": "mock_encoded_delegates", + "result": b"mock_encoded_delegates", } # Mocks @@ -2616,7 +2618,7 @@ def test_get_delegates_latest_block(mocker, subtensor): """Test when no block is provided (latest block).""" # Mock data fake_json_body = { - "result": "mock_encoded_delegates", + "result": b"mock_encoded_delegates", } # Mocks @@ -2880,3 +2882,27 @@ def test_set_weights_with_commit_reveal_enabled(subtensor, mocker): wait_for_finalization=fake_wait_for_finalization, ) assert result == mocked_commit_reveal_v3_extrinsic.return_value + + +def test_connection_limit(mocker): + """Test connection limit is not exceeded.""" + # Technically speaking, this test should exist in integration tests. But to reduce server costs we will leave this + # test here. + + # Preps + mocker.patch.object( + subtensor_module.ws_client, + "connect", + side_effect=subtensor_module.InvalidStatus( + response=mocker.Mock( + response=mocker.Mock( + status_code=429, message="test connection limit error" + ) + ) + ), + ) + # Call with assertions + + with pytest.raises(subtensor_module.InvalidStatus): + for i in range(2): + Subtensor("test")