diff --git a/.circleci/config.yml b/.circleci/config.yml index c610dd7b6c..fbe39a6645 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -290,6 +290,9 @@ workflows: - check_compatibility: python_version: "3.12" name: check-compatibility-3.12 + - check_compatibility: + python_version: "3.13" + name: check-compatibility-3.13 pr-requirements: @@ -302,7 +305,7 @@ workflows: - build-and-test: matrix: parameters: - python-version: [ "3.9.13", "3.10.6", "3.11.4", "3.12.7" ] + python-version: [ "3.9.13", "3.10.6", "3.11.4", "3.12.7"] requires: - check-if-pr-is-draft - unit-tests-all-python-versions: @@ -311,7 +314,7 @@ workflows: - lint-and-type-check: matrix: parameters: - python-version: [ "3.9.13", "3.10.6", "3.11.4", "3.12.7" ] + python-version: [ "3.9.13", "3.10.6", "3.11.4", "3.12.7"] requires: - check-if-pr-is-draft #- coveralls: diff --git a/.github/workflows/e2e-subtensor-tests.yaml b/.github/workflows/e2e-subtensor-tests.yaml index 97df3354be..c74c5fadfe 100644 --- a/.github/workflows/e2e-subtensor-tests.yaml +++ b/.github/workflows/e2e-subtensor-tests.yaml @@ -49,10 +49,10 @@ jobs: run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u $GITHUB_ACTOR --password-stdin - name: Pull Docker Image - run: docker pull ghcr.io/opentensor/subtensor-localnet:latest + run: docker pull ghcr.io/opentensor/subtensor-localnet:devnet-ready - name: Save Docker Image to Cache - run: docker save -o subtensor-localnet.tar ghcr.io/opentensor/subtensor-localnet:latest + run: docker save -o subtensor-localnet.tar ghcr.io/opentensor/subtensor-localnet:devnet-ready - name: Upload Docker Image as Artifact uses: actions/upload-artifact@v4 @@ -62,6 +62,7 @@ jobs: # Job to run tests in parallel run: + name: ${{ matrix.test-file }} / Python ${{ matrix.python-version }} needs: - find-tests - pull-docker-image @@ -74,10 +75,16 @@ jobs: os: - ubuntu-latest test-file: ${{ fromJson(needs.find-tests.outputs.test-files) }} + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] steps: - name: Check-out repository uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install uv uses: astral-sh/setup-uv@v4 diff --git a/CHANGELOG.md b/CHANGELOG.md index 15214fcf88..84d7c10774 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,35 @@ # Changelog +## 9.3.0 /2025-04-09 + +## What's Changed +* More E2E tests by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2754 +* Fix E2E: fix wait_epoch and next_tempo by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2753 +* Add all supported python versions to e2e tests workflow by @basfroman in https://github.com/opentensor/bittensor/pull/2761 +* update docker image name by @basfroman in https://github.com/opentensor/bittensor/pull/2760 +* Add pypi package version checker for `python -m bittensor` by @basfroman in https://github.com/opentensor/bittensor/pull/2762 +* Feat: set_children and get_pending_children methods by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2752 +* Add logic for keep docker image up to date by @basfroman in https://github.com/opentensor/bittensor/pull/2765 +* Fix: CI/CD Set up Python version for E2E tests by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2767 +* Fix E2E Tests: wait for new nonce by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2768 +* Fix e2e `conftest.py` for legacy runner by @basfroman in https://github.com/opentensor/bittensor/pull/2769 +* Fix E2E with devnet-ready by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2776 +* Add compatibility check for 3.13 by @thewhaleking in https://github.com/opentensor/bittensor/pull/2779 +* Fix E2E test_dendrite by making sure Alice is Top validator in Subnet by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2780 +* Add get_owned_hotkeys to subtensor and async one + tests by @basfroman in https://github.com/opentensor/bittensor/pull/2766 +* Add drand-commitments by @basfroman in https://github.com/opentensor/bittensor/pull/2781 +* Missing f-string format by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2785 +* bump version by @basfroman in https://github.com/opentensor/bittensor/pull/2786 +* Improvement and fix for https://github.com/opentensor/bittensor/pull/2781 by @basfroman in https://github.com/opentensor/bittensor/pull/2787 +* Add `stop_existing_test_containers` logic before run e2e test/s by @basfroman in https://github.com/opentensor/bittensor/pull/2790 +* Bump async substrate interface by @thewhaleking in https://github.com/opentensor/bittensor/pull/2788 +* Improve CRv3 functionality by @basfroman in https://github.com/opentensor/bittensor/pull/2791 +* Improve logic in Balance magic methods by @basfroman in https://github.com/opentensor/bittensor/pull/2764 +* Requirements update by @thewhaleking in https://github.com/opentensor/bittensor/pull/2789 +* remove Levenshtein requirement by @thewhaleking in https://github.com/opentensor/bittensor/pull/2802 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v9.2.0...v9.3.0 + ## 9.2.0 /2025-03-18 ## What's Changed diff --git a/README.md b/README.md index 8de9667c7d..7dbfcbc260 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ # **Bittensor SDK** [![Discord Chat](https://img.shields.io/discord/308323056592486420.svg)](https://discord.gg/bittensor) +[![CodeQL](https://github.com/opentensor/bittensor/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/opentensor/bittensor/actions) [![PyPI version](https://badge.fury.io/py/bittensor.svg)](https://badge.fury.io/py/bittensor) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) diff --git a/VERSION b/VERSION index 85f864fe85..4d0ffae7b5 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -9.2.0 \ No newline at end of file +9.3.0 \ No newline at end of file diff --git a/bittensor/__main__.py b/bittensor/__main__.py index 65734ad99f..f3cc1e6487 100644 --- a/bittensor/__main__.py +++ b/bittensor/__main__.py @@ -3,6 +3,7 @@ import sys from bittensor import __version__ +from bittensor.utils.version import check_latest_version_in_pypi if __name__ == "__main__": if len(sys.argv) > 1 and sys.argv[1] == "certifi": @@ -18,4 +19,5 @@ # Run the script subprocess.run([certifi_script], check=True) else: - print(f"Bittensor SDK version: {__version__}") + print(f"Installed Bittensor SDK version: {__version__}") + check_latest_version_in_pypi() diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index 7eb4f04eb2..9a132f4258 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -1,7 +1,7 @@ import asyncio import copy -from datetime import datetime, timezone import ssl +from datetime import datetime, timezone from functools import partial from typing import Optional, Any, Union, Iterable, TYPE_CHECKING @@ -9,6 +9,7 @@ import numpy as np import scalecodec from async_substrate_interface import AsyncSubstrateInterface +from bittensor_commit_reveal import get_encrypted_commitment from bittensor_wallet.utils import SS58_FORMAT from numpy.typing import NDArray from scalecodec import GenericCall @@ -29,21 +30,25 @@ ) from bittensor.core.chain_data.chain_identity import ChainIdentity from bittensor.core.chain_data.delegate_info import DelegatedInfo -from bittensor.core.chain_data.utils import decode_metadata +from bittensor.core.chain_data.utils import ( + decode_metadata, + decode_revealed_commitment, + decode_revealed_commitment_with_hotkey, +) from bittensor.core.config import Config from bittensor.core.errors import ChainError, SubstrateRequestException from bittensor.core.extrinsics.asyncex.commit_reveal import commit_reveal_v3_extrinsic +from bittensor.core.extrinsics.asyncex.move_stake import ( + transfer_stake_extrinsic, + swap_stake_extrinsic, + move_stake_extrinsic, +) from bittensor.core.extrinsics.asyncex.registration import ( burned_register_extrinsic, register_extrinsic, register_subnet_extrinsic, set_subnet_identity_extrinsic, ) -from bittensor.core.extrinsics.asyncex.move_stake import ( - transfer_stake_extrinsic, - swap_stake_extrinsic, - move_stake_extrinsic, -) from bittensor.core.extrinsics.asyncex.root import ( set_root_weights_extrinsic, root_register_extrinsic, @@ -77,10 +82,13 @@ from bittensor.utils import ( Certificate, decode_hex_identity_dict, + float_to_u64, format_error_message, + is_valid_ss58_address, torch, u16_normalized_float, u64_normalized_float, + unlock_key, ) from bittensor.utils.balance import ( Balance, @@ -934,6 +942,57 @@ async def get_children( except SubstrateRequestException as e: return False, [], format_error_message(e) + async def get_children_pending( + self, + hotkey: str, + netuid: int, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> tuple[ + list[tuple[float, str]], + int, + ]: + """ + This method retrieves the pending children of a given hotkey and netuid. + It queries the SubtensorModule's PendingChildKeys storage function. + + Arguments: + hotkey (str): The hotkey value. + netuid (int): The netuid value. + block (Optional[int]): The block number for which the children are to be retrieved. + block_hash (Optional[str]): The hash of the block to retrieve the subnet unique identifiers from. + reuse_block (bool): Whether to reuse the last-used block hash. + + Returns: + list[tuple[float, str]]: A list of children with their proportions. + int: The cool-down block number. + """ + + response = await self.substrate.query( + module="SubtensorModule", + storage_function="PendingChildKeys", + params=[netuid, hotkey], + block_hash=await self.determine_block_hash( + block, + block_hash, + reuse_block, + ), + reuse_block_hash=reuse_block, + ) + children, cooldown = response.value + + return ( + [ + ( + u64_normalized_float(proportion), + decode_account_id(child[0]), + ) + for proportion, child in children + ], + cooldown, + ) + async def get_commitment( self, netuid: int, @@ -1006,6 +1065,115 @@ async def get_all_commitments( result[decode_account_id(id_[0])] = decode_metadata(value) return result + async def get_revealed_commitment_by_hotkey( + self, + netuid: int, + hotkey_ss58_address: Optional[str] = None, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> Optional[tuple[tuple[int, str], ...]]: + """Returns hotkey related revealed commitment for a given netuid. + + Arguments: + netuid (int): The unique identifier of the subnetwork. + block (Optional[int]): The block number to retrieve the commitment from. Default is ``None``. + hotkey_ss58_address (str): The ss58 address of the committee member. + block_hash (Optional[str]): The hash of the block to retrieve the subnet unique identifiers from. + reuse_block (bool): Whether to reuse the last-used block hash. + + Returns: + result (tuple[int, str): A tuple of reveal block and commitment message. + """ + if not is_valid_ss58_address(address=hotkey_ss58_address): + raise ValueError(f"Invalid ss58 address {hotkey_ss58_address} provided.") + + query = await self.query_module( + module="Commitments", + name="RevealedCommitments", + params=[netuid, hotkey_ss58_address], + block=block, + block_hash=block_hash, + reuse_block=reuse_block, + ) + if query is None: + return None + return tuple(decode_revealed_commitment(pair) for pair in query) + + async def get_revealed_commitment( + self, + netuid: int, + uid: int, + block: Optional[int] = None, + ) -> Optional[tuple[tuple[int, str], ...]]: + """Returns uid related revealed commitment for a given netuid. + + Arguments: + netuid (int): The unique identifier of the subnetwork. + uid (int): The neuron uid to retrieve the commitment from. + block (Optional[int]): The block number to retrieve the commitment from. Default is ``None``. + + Returns: + result (Optional[tuple[int, str]]: A tuple of reveal block and commitment message. + + Example of result: + ( (12, "Alice message 1"), (152, "Alice message 2") ) + ( (12, "Bob message 1"), (147, "Bob message 2") ) + """ + try: + meta_info = await self.get_metagraph_info(netuid, block=block) + if meta_info: + hotkey_ss58_address = meta_info.hotkeys[uid] + else: + raise ValueError(f"Subnet with netuid {netuid} does not exist.") + except IndexError: + raise ValueError(f"Subnet {netuid} does not have a neuron with uid {uid}.") + + return await self.get_revealed_commitment_by_hotkey( + netuid=netuid, hotkey_ss58_address=hotkey_ss58_address, block=block + ) + + async def get_all_revealed_commitments( + self, + netuid: int, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> dict[str, tuple[tuple[int, str], ...]]: + """Returns all revealed commitments for a given netuid. + + Arguments: + netuid (int): The unique identifier of the subnetwork. + block (Optional[int]): The block number to retrieve the commitment from. Default is ``None``. + block_hash (Optional[str]): The hash of the block to retrieve the subnet unique identifiers from. + reuse_block (bool): Whether to reuse the last-used block hash. + + Returns: + result (dict): A dictionary of all revealed commitments in view {ss58_address: (reveal block, commitment message)}. + + Example of result: + { + "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY": ( (12, "Alice message 1"), (152, "Alice message 2") ), + "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty": ( (12, "Bob message 1"), (147, "Bob message 2") ), + } + """ + query = await self.query_map( + module="Commitments", + name="RevealedCommitments", + params=[netuid], + block=block, + block_hash=block_hash, + reuse_block=reuse_block, + ) + + result = {} + async for pair in query: + hotkey_ss58_address, commitment_message = ( + decode_revealed_commitment_with_hotkey(pair) + ) + result[hotkey_ss58_address] = commitment_message + return result + async def get_current_weight_commit_info( self, netuid: int, @@ -1519,6 +1687,36 @@ async def get_neuron_for_pubkey_and_subnet( reuse_block=reuse_block, ) + async def get_owned_hotkeys( + self, + coldkey_ss58: str, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> list[str]: + """ + Retrieves all hotkeys owned by a specific coldkey address. + + Args: + coldkey_ss58 (str): The SS58 address of the coldkey to query. + block (int): The blockchain block number for the query. + block_hash (str): The hash of the blockchain block number for the query. + reuse_block (bool): Whether to reuse the last-used blockchain block hash. + + Returns: + list[str]: A list of hotkey SS58 addresses owned by the coldkey. + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + owned_hotkeys = await self.substrate.query( + module="SubtensorModule", + storage_function="OwnedHotkeys", + params=[coldkey_ss58], + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) + + return [decode_account_id(hotkey[0]) for hotkey in owned_hotkeys or []] + async def get_stake( self, coldkey_ss58: str, @@ -2020,10 +2218,11 @@ async def get_vote_data( block_hash=block_hash, reuse_block_hash=reuse_block, ) + if vote_data is None: return None - else: - return ProposalVoteData(vote_data) + + return ProposalVoteData.from_dict(vote_data) async def get_uid_for_hotkey_on_subnet( self, @@ -2566,6 +2765,46 @@ async def recycle( ) return None if call is None else Balance.from_rao(int(call)) + async def set_reveal_commitment( + self, + wallet, + netuid: int, + data: str, + blocks_until_reveal: int = 360, + block_time: Union[int, float] = 12, + ) -> tuple[bool, int]: + """ + Commits arbitrary data to the Bittensor network by publishing metadata. + + Arguments: + wallet (bittensor_wallet.Wallet): The wallet associated with the neuron committing the data. + netuid (int): The unique identifier of the subnetwork. + data (str): The data to be committed to the network. + blocks_until_reveal (int): The number of blocks from now after which the data will be revealed. Defaults to `360`. + Then amount of blocks in one epoch. + block_time (Union[int, float]): The number of seconds between each block. Defaults to `12`. + + Returns: + bool: `True` if the commitment was successful, `False` otherwise. + + Note: A commitment can be set once per subnet epoch and is reset at the next epoch in the chain automatically. + """ + + encrypted, reveal_round = get_encrypted_commitment( + data, blocks_until_reveal, block_time + ) + + # increase reveal_round in return + 1 because we want to fetch data from the chain after that round was revealed + # and stored. + data_ = {"encrypted": encrypted, "reveal_round": reveal_round} + return await publish_metadata( + subtensor=self, + wallet=wallet, + netuid=netuid, + data_type=f"TimelockEncrypted", + data=data_, + ), reveal_round + async def subnet( self, netuid: int, @@ -3380,6 +3619,75 @@ async def root_set_weights( wait_for_inclusion=wait_for_inclusion, ) + async def set_children( + self, + wallet: "Wallet", + hotkey: str, + netuid: int, + children: list[tuple[float, str]], + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + raise_error: bool = False, + ) -> tuple[bool, str]: + """ + Allows a coldkey to set children keys. + + Arguments: + wallet (bittensor_wallet.Wallet): bittensor wallet instance. + hotkey (str): The ``SS58`` address of the neuron's hotkey. + netuid (int): The netuid value. + children (list[tuple[float, str]]): A list of children with their proportions. + wait_for_inclusion (bool): Waits for the transaction to be included in a block. + wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. + raise_error: Raises relevant exception rather than returning `False` if unsuccessful. + + Returns: + tuple[bool, str]: A tuple where the first element is a boolean indicating success or failure of the + operation, and the second element is a message providing additional information. + + Raises: + DuplicateChild: There are duplicates in the list of children. + InvalidChild: Child is the hotkey. + NonAssociatedColdKey: The coldkey does not own the hotkey or the child is the same as the hotkey. + NotEnoughStakeToSetChildkeys: Parent key doesn't have minimum own stake. + ProportionOverflow: The sum of the proportions does exceed uint64. + RegistrationNotPermittedOnRootSubnet: Attempting to register a child on the root network. + SubNetworkDoesNotExist: Attempting to register to a non-existent network. + TooManyChildren: Too many children in request. + TxRateLimitExceeded: Hotkey hit the rate limit. + bittensor_wallet.errors.KeyFileError: Failed to decode keyfile data. + bittensor_wallet.errors.PasswordError: Decryption failed or wrong password for decryption provided. + """ + + unlock = unlock_key(wallet, raise_error=raise_error) + + if not unlock.success: + return False, unlock.message + + call = await self.substrate.compose_call( + call_module="SubtensorModule", + call_function="set_children", + call_params={ + "children": [ + ( + float_to_u64(proportion), + child_hotkey, + ) + for proportion, child_hotkey in children + ], + "hotkey": hotkey, + "netuid": netuid, + }, + ) + + return await self.sign_and_send_extrinsic( + call, + wallet, + wait_for_inclusion, + wait_for_finalization, + raise_error=raise_error, + ) + async def set_delegate_take( self, wallet: "Wallet", @@ -3504,6 +3812,7 @@ async def set_weights( wait_for_inclusion: bool = False, wait_for_finalization: bool = False, max_retries: int = 5, + block_time: float = 12.0, ): """ Sets the inter-neuronal weights for the specified neuron. This process involves specifying the influence or @@ -3523,6 +3832,7 @@ async def set_weights( wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. Default is ``False``. max_retries (int): The number of maximum attempts to set weights. Default is ``5``. + block_time (float): The amount of seconds for block duration. Default is 12.0 seconds. Returns: tuple[bool, str]: ``True`` if the setting of weights is successful, False otherwise. And `msg`, a string @@ -3571,6 +3881,7 @@ async def _blocks_weight_limit() -> bool: version_key=version_key, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + block_time=block_time, ) retries += 1 return success, message diff --git a/bittensor/core/axon.py b/bittensor/core/axon.py index a51f1c3e2d..54817ccdfd 100644 --- a/bittensor/core/axon.py +++ b/bittensor/core/axon.py @@ -976,7 +976,9 @@ async def default_verify(self, synapse: "Synapse"): ): raise Exception("Nonce is too old, a newer one was last processed") - if not keypair.verify(message, synapse.dendrite.signature): + if synapse.dendrite.signature and not keypair.verify( + message, synapse.dendrite.signature + ): raise Exception( f"Signature mismatch with {message} and {synapse.dendrite.signature}" ) diff --git a/bittensor/core/chain_data/proposal_vote_data.py b/bittensor/core/chain_data/proposal_vote_data.py index ea785a7a90..3cf5439955 100644 --- a/bittensor/core/chain_data/proposal_vote_data.py +++ b/bittensor/core/chain_data/proposal_vote_data.py @@ -1,22 +1,27 @@ +from dataclasses import dataclass + +from bittensor.core.chain_data.info_base import InfoBase from bittensor.core.chain_data.utils import decode_account_id -# Senate / Proposal data -class ProposalVoteData: +@dataclass +class ProposalVoteData(InfoBase): + """ + Senate / Proposal data + """ + index: int threshold: int ayes: list[str] nays: list[str] end: int - def __init__(self, proposal_dict: dict) -> None: - self.index = proposal_dict["index"] - self.threshold = proposal_dict["threshold"] - self.ayes = self.decode_ss58_tuples(proposal_dict["ayes"]) - self.nays = self.decode_ss58_tuples(proposal_dict["nays"]) - self.end = proposal_dict["end"] - - @staticmethod - def decode_ss58_tuples(line: tuple): - """Decodes a tuple of ss58 addresses formatted as bytes tuples.""" - return [decode_account_id(line[x][0]) for x in range(len(line))] + @classmethod + def from_dict(cls, proposal_dict: dict) -> "ProposalVoteData": + return cls( + ayes=[decode_account_id(key) for key in proposal_dict["ayes"]], + end=proposal_dict["end"], + index=proposal_dict["index"], + nays=[decode_account_id(key) for key in proposal_dict["nays"]], + threshold=proposal_dict["threshold"], + ) diff --git a/bittensor/core/chain_data/utils.py b/bittensor/core/chain_data/utils.py index c7d6986038..9915b51c1f 100644 --- a/bittensor/core/chain_data/utils.py +++ b/bittensor/core/chain_data/utils.py @@ -1,7 +1,7 @@ """Chain data helper functions and data.""" from enum import Enum -from typing import Optional, Union +from typing import Optional, Union, TYPE_CHECKING from scalecodec.base import RuntimeConfiguration, ScaleBytes from scalecodec.type_registry import load_type_registry_preset @@ -10,6 +10,9 @@ from bittensor.core.settings import SS58_FORMAT from bittensor.utils.balance import Balance +if TYPE_CHECKING: + from async_substrate_interface.sync_substrate import QueryMapResult + class ChainDataType(Enum): NeuronInfo = 1 @@ -135,3 +138,49 @@ def decode_metadata(metadata: dict) -> str: commitment = metadata["info"]["fields"][0][0] bytes_tuple = commitment[next(iter(commitment.keys()))][0] return bytes(bytes_tuple).decode() + + +def decode_revealed_commitment(encoded_data) -> tuple[int, str]: + """ + Decode the revealed commitment data from the given input if it is not None. + + Arguments: + encoded_data (tuple[bytes, int]): A tuple containing the revealed message and the block number. + + Returns: + tuple[int, str]: A tuple containing the revealed block number and decoded commitment message. + """ + + def scale_decode_offset(data: bytes) -> int: + """Decodes the scale offset from a given byte data sequence.""" + first_byte = data[0] + mode = first_byte & 0b11 + if mode == 0: + return 1 + elif mode == 1: + return 2 + else: + return 4 + + com_bytes, revealed_block = encoded_data + offset = scale_decode_offset(com_bytes) + + revealed_commitment = bytes(com_bytes[offset:]).decode("utf-8", errors="ignore") + return revealed_block, revealed_commitment + + +def decode_revealed_commitment_with_hotkey( + encoded_data: "QueryMapResult", +) -> tuple[str, tuple[tuple[int, str], ...]]: + """ + Decode revealed commitment using a hotkey. + + Returns: + tuple[str, tuple[tuple[int, str], ...]]: A tuple containing the hotkey (ss58 address) and a tuple of block + numbers and their corresponding revealed commitments. + """ + key, data = encoded_data + + ss58_address = decode_account_id(next(iter(key))) + block_data = tuple(decode_revealed_commitment(p) for p in data.value) + return ss58_address, block_data diff --git a/bittensor/core/errors.py b/bittensor/core/errors.py index 0625a098f9..d4b438bd2f 100644 --- a/bittensor/core/errors.py +++ b/bittensor/core/errors.py @@ -63,10 +63,6 @@ class ChainTransactionError(ChainError): """Error for any chain transaction related errors.""" -class ChainQueryError(ChainError): - """Error for any chain query related errors.""" - - class DelegateTakeTooHigh(ChainTransactionError): """ Delegate take is too high. @@ -79,9 +75,9 @@ class DelegateTakeTooLow(ChainTransactionError): """ -class DelegateTxRateLimitExceeded(ChainTransactionError): +class DuplicateChild(ChainTransactionError): """ - A transactor exceeded the rate limit for delegate transaction. + Duplicate child when setting children. """ @@ -91,50 +87,124 @@ class HotKeyAccountNotExists(ChainTransactionError): """ +class IdentityError(ChainTransactionError): + """ + Error raised when an identity transaction fails. + """ + + +class InvalidChild(ChainTransactionError): + """ + Attempting to set an invalid child for a hotkey on a network. + """ + + +class MetadataError(ChainTransactionError): + """ + Error raised when metadata commitment transaction fails. + """ + + +class NominationError(ChainTransactionError): + """ + Error raised when a nomination transaction fails. + """ + + class NonAssociatedColdKey(ChainTransactionError): """ Request to stake, unstake or subscribe is made by a coldkey that is not associated with the hotkey account. """ -class StakeError(ChainTransactionError): - """Error raised when a stake transaction fails.""" +class NotEnoughStakeToSetChildkeys(ChainTransactionError): + """ + The parent hotkey doesn't have enough own stake to set childkeys. + """ -class UnstakeError(ChainTransactionError): - """Error raised when an unstake transaction fails.""" +class NotRegisteredError(ChainTransactionError): + """ + Error raised when a neuron is not registered, and the transaction requires it to be. + """ -class IdentityError(ChainTransactionError): - """Error raised when an identity transaction fails.""" +class ProportionOverflow(ChainTransactionError): + """ + Proportion overflow when setting children. + """ -class NominationError(ChainTransactionError): - """Error raised when a nomination transaction fails.""" +class RegistrationError(ChainTransactionError): + """ + Error raised when a neuron registration transaction fails. + """ + + +class RegistrationNotPermittedOnRootSubnet(ChainTransactionError): + """ + Operation is not permitted on the root subnet. + """ + + +class StakeError(ChainTransactionError): + """ + Error raised when a stake transaction fails. + """ + + +class NotDelegateError(StakeError): + """ + Error raised when a hotkey you are trying to stake to is not a delegate. + """ + + +class SubNetworkDoesNotExist(ChainTransactionError): + """ + The subnet does not exist. + """ class TakeError(ChainTransactionError): - """Error raised when an increase / decrease take transaction fails.""" + """ + Error raised when an increase / decrease take transaction fails. + """ class TransferError(ChainTransactionError): - """Error raised when a transfer transaction fails.""" + """ + Error raised when a transfer transaction fails. + """ -class RegistrationError(ChainTransactionError): - """Error raised when a neuron registration transaction fails.""" +class TooManyChildren(ChainTransactionError): + """ + Too many children MAX 5. + """ -class NotRegisteredError(ChainTransactionError): - """Error raised when a neuron is not registered, and the transaction requires it to be.""" +class TxRateLimitExceeded(ChainTransactionError): + """ + Default transaction rate limit exceeded. + """ -class NotDelegateError(StakeError): - """Error raised when a hotkey you are trying to stake to is not a delegate.""" +class DelegateTxRateLimitExceeded(TxRateLimitExceeded): + """ + A transactor exceeded the rate limit for delegate transaction. + """ -class MetadataError(ChainTransactionError): - """Error raised when metadata commitment transaction fails.""" +class UnstakeError(ChainTransactionError): + """ + Error raised when an unstake transaction fails. + """ + + +class ChainQueryError(ChainError): + """ + Error for any chain query related errors. + """ class InvalidRequestNameError(Exception): diff --git a/bittensor/core/extrinsics/asyncex/commit_reveal.py b/bittensor/core/extrinsics/asyncex/commit_reveal.py index 2a5212b569..d1b528e509 100644 --- a/bittensor/core/extrinsics/asyncex/commit_reveal.py +++ b/bittensor/core/extrinsics/asyncex/commit_reveal.py @@ -70,6 +70,7 @@ async def commit_reveal_v3_extrinsic( version_key: int = version_as_int, wait_for_inclusion: bool = False, wait_for_finalization: bool = False, + block_time: float = 12.0, ) -> tuple[bool, str]: """ Commits and reveals weights for given subtensor and wallet with provided uids and weights. @@ -83,6 +84,7 @@ async def commit_reveal_v3_extrinsic( 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. + block_time (float): The amount of seconds for block duration. Default is 12.0 seconds. Returns: tuple[bool, str]: A tuple where the first element is a boolean indicating success or failure, and the second @@ -114,6 +116,7 @@ async def commit_reveal_v3_extrinsic( current_block=current_block["header"]["number"], netuid=netuid, subnet_reveal_period_epochs=subnet_reveal_period_epochs, + block_time=block_time, ) success, message = await _do_commit_reveal_v3( diff --git a/bittensor/core/extrinsics/asyncex/serving.py b/bittensor/core/extrinsics/asyncex/serving.py index 5b38dafb56..558b58cae5 100644 --- a/bittensor/core/extrinsics/asyncex/serving.py +++ b/bittensor/core/extrinsics/asyncex/serving.py @@ -1,5 +1,5 @@ import asyncio -from typing import Optional, TYPE_CHECKING +from typing import Optional, Union, TYPE_CHECKING from bittensor.core.errors import MetadataError from bittensor.core.settings import version_as_int @@ -226,7 +226,7 @@ async def publish_metadata( wallet: "Wallet", netuid: int, data_type: str, - data: bytes, + data: Union[bytes, dict], wait_for_inclusion: bool = False, wait_for_finalization: bool = True, ) -> bool: @@ -240,8 +240,8 @@ async def publish_metadata( data_type (str): The data type of the information being submitted. It should be one of the following: ``'Sha256'``, ``'Blake256'``, ``'Keccak256'``, or ``'Raw0-128'``. This specifies the format or hashing algorithm used for the data. - data (str): The actual metadata content to be published. This should be formatted or hashed according to the - ``type`` specified. (Note: max ``str`` length is 128 bytes) + data (Union[bytes, dict]): The actual metadata content to be published. This should be formatted or hashed + according to the ``type`` specified. (Note: max ``str`` length is 128 bytes for ``'Raw0-128'``.) wait_for_inclusion (bool, optional): If ``True``, the function will wait for the extrinsic to be included in a block before returning. Defaults to ``False``. wait_for_finalization (bool, optional): If ``True``, the function will wait for the extrinsic to be finalized diff --git a/bittensor/core/extrinsics/asyncex/weights.py b/bittensor/core/extrinsics/asyncex/weights.py index 05fb283e9e..b2221a5263 100644 --- a/bittensor/core/extrinsics/asyncex/weights.py +++ b/bittensor/core/extrinsics/asyncex/weights.py @@ -319,7 +319,7 @@ async def set_weights_extrinsic( ) logging.info( - ":satellite: [magenta]Setting weights on [/magenta][blue]{subtensor.network}[/blue] [magenta]...[/magenta]" + f":satellite: [magenta]Setting weights on [/magenta][blue]{subtensor.network}[/blue] [magenta]...[/magenta]" ) try: success, error_message = await _do_set_weights( diff --git a/bittensor/core/extrinsics/commit_reveal.py b/bittensor/core/extrinsics/commit_reveal.py index f71fae5581..86795f1624 100644 --- a/bittensor/core/extrinsics/commit_reveal.py +++ b/bittensor/core/extrinsics/commit_reveal.py @@ -70,6 +70,7 @@ def commit_reveal_v3_extrinsic( version_key: int = version_as_int, wait_for_inclusion: bool = False, wait_for_finalization: bool = False, + block_time: float = 12.0, ) -> tuple[bool, str]: """ Commits and reveals weights for given subtensor and wallet with provided uids and weights. @@ -83,6 +84,7 @@ def commit_reveal_v3_extrinsic( 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. + block_time (float): The amount of seconds for block duration. Default is 12.0 seconds. Returns: tuple[bool, str]: A tuple where the first element is a boolean indicating success or failure, and the second @@ -114,6 +116,7 @@ def commit_reveal_v3_extrinsic( current_block=current_block, netuid=netuid, subnet_reveal_period_epochs=subnet_reveal_period_epochs, + block_time=block_time, ) success, message = _do_commit_reveal_v3( diff --git a/bittensor/core/extrinsics/serving.py b/bittensor/core/extrinsics/serving.py index c5e3f96ee5..aaed6ad38f 100644 --- a/bittensor/core/extrinsics/serving.py +++ b/bittensor/core/extrinsics/serving.py @@ -1,4 +1,4 @@ -from typing import Optional, TYPE_CHECKING +from typing import Optional, Union, TYPE_CHECKING from bittensor.core.errors import MetadataError from bittensor.core.settings import version_as_int @@ -222,7 +222,7 @@ def publish_metadata( wallet: "Wallet", netuid: int, data_type: str, - data: bytes, + data: Union[bytes, dict], wait_for_inclusion: bool = False, wait_for_finalization: bool = True, ) -> bool: @@ -236,8 +236,8 @@ def publish_metadata( data_type (str): The data type of the information being submitted. It should be one of the following: ``'Sha256'``, ``'Blake256'``, ``'Keccak256'``, or ``'Raw0-128'``. This specifies the format or hashing algorithm used for the data. - data (str): The actual metadata content to be published. This should be formatted or hashed according to the - ``type`` specified. (Note: max ``str`` length is 128 bytes) + data (Union[bytes, dict]): The actual metadata content to be published. This should be formatted or hashed + according to the ``type`` specified. (Note: max ``str`` length is 128 bytes for ``'Raw0-128'``.) wait_for_inclusion (bool, optional): If ``True``, the function will wait for the extrinsic to be included in a block before returning. Defaults to ``False``. wait_for_finalization (bool, optional): If ``True``, the function will wait for the extrinsic to be finalized diff --git a/bittensor/core/extrinsics/set_weights.py b/bittensor/core/extrinsics/set_weights.py index 5e86c9110e..4c1c194708 100644 --- a/bittensor/core/extrinsics/set_weights.py +++ b/bittensor/core/extrinsics/set_weights.py @@ -123,7 +123,7 @@ def set_weights_extrinsic( ) logging.info( - ":satellite: [magenta]Setting weights on [/magenta][blue]{subtensor.network}[/blue] [magenta]...[/magenta]" + f":satellite: [magenta]Setting weights on [/magenta][blue]{subtensor.network}[/blue] [magenta]...[/magenta]" ) try: success, error_message = _do_set_weights( diff --git a/bittensor/core/settings.py b/bittensor/core/settings.py index 8a53f6c423..d3b68c04d3 100644 --- a/bittensor/core/settings.py +++ b/bittensor/core/settings.py @@ -1,4 +1,4 @@ -__version__ = "9.2.0" +__version__ = "9.3.0" import os import re diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index e113c1b033..19b14d09c0 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -1,14 +1,14 @@ import copy from datetime import datetime, timezone - from functools import lru_cache from typing import TYPE_CHECKING, Any, Iterable, Optional, Union, cast import numpy as np import scalecodec from async_substrate_interface.errors import SubstrateRequestException -from async_substrate_interface.types import ScaleObj from async_substrate_interface.sync_substrate import SubstrateInterface +from async_substrate_interface.types import ScaleObj +from bittensor_commit_reveal import get_encrypted_commitment from numpy.typing import NDArray from bittensor.core.async_subtensor import ProposalVoteData @@ -28,7 +28,11 @@ decode_account_id, ) from bittensor.core.chain_data.chain_identity import ChainIdentity -from bittensor.core.chain_data.utils import decode_metadata +from bittensor.core.chain_data.utils import ( + decode_metadata, + decode_revealed_commitment, + decode_revealed_commitment_with_hotkey, +) from bittensor.core.config import Config from bittensor.core.errors import ChainError from bittensor.core.extrinsics.commit_reveal import commit_reveal_v3_extrinsic @@ -80,10 +84,13 @@ from bittensor.utils import ( Certificate, decode_hex_identity_dict, + float_to_u64, format_error_message, + is_valid_ss58_address, torch, u16_normalized_float, u64_normalized_float, + unlock_key, ) from bittensor.utils.balance import ( Balance, @@ -392,7 +399,7 @@ def blocks_since_last_update(self, netuid: int, uid: int) -> Optional[int]: exist. """ call = self.get_hyperparameter(param_name="LastUpdate", netuid=netuid) - return None if call is None else (self.get_current_block() - int(call[uid])) + return None if not call else (self.get_current_block() - int(call[uid])) def bonds( self, netuid: int, block: Optional[int] = None @@ -717,6 +724,47 @@ def get_children( except SubstrateRequestException as e: return False, [], format_error_message(e) + def get_children_pending( + self, + hotkey: str, + netuid: int, + block: Optional[int] = None, + ) -> tuple[ + list[tuple[float, str]], + int, + ]: + """ + This method retrieves the pending children of a given hotkey and netuid. + It queries the SubtensorModule's PendingChildKeys storage function. + + Arguments: + hotkey (str): The hotkey value. + netuid (int): The netuid value. + block (Optional[int]): The block number for which the children are to be retrieved. + + Returns: + list[tuple[float, str]]: A list of children with their proportions. + int: The cool-down block number. + """ + + children, cooldown = self.substrate.query( + module="SubtensorModule", + storage_function="PendingChildKeys", + params=[netuid, hotkey], + block_hash=self.determine_block_hash(block), + ).value + + return ( + [ + ( + u64_normalized_float(proportion), + decode_account_id(child[0]), + ) + for proportion, child in children + ], + cooldown, + ) + def get_commitment(self, netuid: int, uid: int, block: Optional[int] = None) -> str: """ Retrieves the on-chain commitment for a specific neuron in the Bittensor network. @@ -760,6 +808,101 @@ def get_all_commitments( result[decode_account_id(id_[0])] = decode_metadata(value) return result + def get_revealed_commitment_by_hotkey( + self, + netuid: int, + hotkey_ss58_address: str, + block: Optional[int] = None, + ) -> Optional[tuple[tuple[int, str], ...]]: + """Returns hotkey related revealed commitment for a given netuid. + + Arguments: + netuid (int): The unique identifier of the subnetwork. + hotkey_ss58_address (str): The ss58 address of the committee member. + block (Optional[int]): The block number to retrieve the commitment from. Default is ``None``. + + Returns: + result (tuple[int, str): A tuple of reveal block and commitment message. + """ + if not is_valid_ss58_address(address=hotkey_ss58_address): + raise ValueError(f"Invalid ss58 address {hotkey_ss58_address} provided.") + + query = self.query_module( + module="Commitments", + name="RevealedCommitments", + params=[netuid, hotkey_ss58_address], + block=block, + ) + if query is None: + return None + return tuple(decode_revealed_commitment(pair) for pair in query) + + def get_revealed_commitment( + self, + netuid: int, + uid: int, + block: Optional[int] = None, + ) -> Optional[tuple[tuple[int, str], ...]]: + """Returns uid related revealed commitment for a given netuid. + + Arguments: + netuid (int): The unique identifier of the subnetwork. + uid (int): The neuron uid to retrieve the commitment from. + block (Optional[int]): The block number to retrieve the commitment from. Default is ``None``. + + Returns: + result (Optional[tuple[int, str]]: A tuple of reveal block and commitment message. + + Example of result: + ( (12, "Alice message 1"), (152, "Alice message 2") ) + ( (12, "Bob message 1"), (147, "Bob message 2") ) + """ + try: + meta_info = self.get_metagraph_info(netuid, block=block) + if meta_info: + hotkey_ss58_address = meta_info.hotkeys[uid] + else: + raise ValueError(f"Subnet with netuid {netuid} does not exist.") + except IndexError: + raise ValueError(f"Subnet {netuid} does not have a neuron with uid {uid}.") + + return self.get_revealed_commitment_by_hotkey( + netuid=netuid, hotkey_ss58_address=hotkey_ss58_address, block=block + ) + + def get_all_revealed_commitments( + self, netuid: int, block: Optional[int] = None + ) -> dict[str, tuple[tuple[int, str], ...]]: + """Returns all revealed commitments for a given netuid. + + Arguments: + netuid (int): The unique identifier of the subnetwork. + block (Optional[int]): The block number to retrieve the commitment from. Default is ``None``. + + Returns: + result (dict): A dictionary of all revealed commitments in view {ss58_address: (reveal block, commitment message)}. + + Example of result: + { + "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY": ( (12, "Alice message 1"), (152, "Alice message 2") ), + "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty": ( (12, "Bob message 1"), (147, "Bob message 2") ), + } + """ + query = self.query_map( + module="Commitments", + name="RevealedCommitments", + params=[netuid], + block=block, + ) + + result = {} + for pair in query: + hotkey_ss58_address, commitment_message = ( + decode_revealed_commitment_with_hotkey(pair) + ) + result[hotkey_ss58_address] = commitment_message + return result + def get_current_weight_commit_info( self, netuid: int, block: Optional[int] = None ) -> list: @@ -1157,6 +1300,33 @@ def get_neuron_for_pubkey_and_subnet( return NeuronInfo.from_dict(result) + def get_owned_hotkeys( + self, + coldkey_ss58: str, + block: Optional[int] = None, + reuse_block: bool = False, + ) -> list[str]: + """ + Retrieves all hotkeys owned by a specific coldkey address. + + Args: + coldkey_ss58 (str): The SS58 address of the coldkey to query. + block (int): The blockchain block number for the query. + reuse_block (bool): Whether to reuse the last-used blockchain block hash. + + Returns: + list[str]: A list of hotkey SS58 addresses owned by the coldkey. + """ + block_hash = self.determine_block_hash(block) + owned_hotkeys = self.substrate.query( + module="SubtensorModule", + storage_function="OwnedHotkeys", + params=[coldkey_ss58], + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) + return [decode_account_id(hotkey[0]) for hotkey in owned_hotkeys or []] + def get_stake( self, coldkey_ss58: str, @@ -1579,10 +1749,11 @@ def get_vote_data( params=[proposal_hash], block_hash=self.determine_block_hash(block), ) + if vote_data is None: return None - else: - return ProposalVoteData(vote_data) + + return ProposalVoteData.from_dict(vote_data) def get_uid_for_hotkey_on_subnet( self, hotkey_ss58: str, netuid: int, block: Optional[int] = None @@ -1683,6 +1854,75 @@ def immunity_period( ) return None if call is None else int(call) + def set_children( + self, + wallet: "Wallet", + hotkey: str, + netuid: int, + children: list[tuple[float, str]], + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + raise_error: bool = False, + ) -> tuple[bool, str]: + """ + Allows a coldkey to set children keys. + + Arguments: + wallet (bittensor_wallet.Wallet): bittensor wallet instance. + hotkey (str): The ``SS58`` address of the neuron's hotkey. + netuid (int): The netuid value. + children (list[tuple[float, str]]): A list of children with their proportions. + wait_for_inclusion (bool): Waits for the transaction to be included in a block. + wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. + raise_error: Raises relevant exception rather than returning `False` if unsuccessful. + + Returns: + tuple[bool, str]: A tuple where the first element is a boolean indicating success or failure of the + operation, and the second element is a message providing additional information. + + Raises: + DuplicateChild: There are duplicates in the list of children. + InvalidChild: Child is the hotkey. + NonAssociatedColdKey: The coldkey does not own the hotkey or the child is the same as the hotkey. + NotEnoughStakeToSetChildkeys: Parent key doesn't have minimum own stake. + ProportionOverflow: The sum of the proportions does exceed uint64. + RegistrationNotPermittedOnRootSubnet: Attempting to register a child on the root network. + SubNetworkDoesNotExist: Attempting to register to a non-existent network. + TooManyChildren: Too many children in request. + TxRateLimitExceeded: Hotkey hit the rate limit. + bittensor_wallet.errors.KeyFileError: Failed to decode keyfile data. + bittensor_wallet.errors.PasswordError: Decryption failed or wrong password for decryption provided. + """ + + unlock = unlock_key(wallet, raise_error=raise_error) + + if not unlock.success: + return False, unlock.message + + call = self.substrate.compose_call( + call_module="SubtensorModule", + call_function="set_children", + call_params={ + "children": [ + ( + float_to_u64(proportion), + child_hotkey, + ) + for proportion, child_hotkey in children + ], + "hotkey": hotkey, + "netuid": netuid, + }, + ) + + return self.sign_and_send_extrinsic( + call, + wallet, + wait_for_inclusion, + wait_for_finalization, + raise_error=raise_error, + ) + def set_delegate_take( self, wallet: "Wallet", @@ -2051,6 +2291,46 @@ def recycle(self, netuid: int, block: Optional[int] = None) -> Optional[Balance] call = self.get_hyperparameter(param_name="Burn", netuid=netuid, block=block) return None if call is None else Balance.from_rao(int(call)) + def set_reveal_commitment( + self, + wallet, + netuid: int, + data: str, + blocks_until_reveal: int = 360, + block_time: Union[int, float] = 12, + ) -> tuple[bool, int]: + """ + Commits arbitrary data to the Bittensor network by publishing metadata. + + Arguments: + wallet (bittensor_wallet.Wallet): The wallet associated with the neuron committing the data. + netuid (int): The unique identifier of the subnetwork. + data (str): The data to be committed to the network. + blocks_until_reveal (int): The number of blocks from now after which the data will be revealed. Defaults to `360`. + Then amount of blocks in one epoch. + block_time (Union[int, float]): The number of seconds between each block. Defaults to `12`. + + Returns: + bool: `True` if the commitment was successful, `False` otherwise. + + Note: A commitment can be set once per subnet epoch and is reset at the next epoch in the chain automatically. + """ + + encrypted, reveal_round = get_encrypted_commitment( + data, blocks_until_reveal, block_time + ) + + # increase reveal_round in return + 1 because we want to fetch data from the chain after that round was revealed + # and stored. + data_ = {"encrypted": encrypted, "reveal_round": reveal_round} + return publish_metadata( + subtensor=self, + wallet=wallet, + netuid=netuid, + data_type=f"TimelockEncrypted", + data=data_, + ), reveal_round + def subnet(self, netuid: int, block: Optional[int] = None) -> Optional[DynamicInfo]: """ Retrieves the subnet information for a single subnet in the network. @@ -2819,6 +3099,7 @@ def set_weights( wait_for_inclusion: bool = False, wait_for_finalization: bool = False, max_retries: int = 5, + block_time: float = 12.0, ) -> tuple[bool, str]: """ Sets the inter-neuronal weights for the specified neuron. This process involves specifying the influence or @@ -2838,6 +3119,7 @@ def set_weights( wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. Default is ``False``. max_retries (int): The number of maximum attempts to set weights. Default is ``5``. + block_time (float): The amount of seconds for block duration. Default is 12.0 seconds. Returns: tuple[bool, str]: ``True`` if the setting of weights is successful, False otherwise. And `msg`, a string @@ -2879,6 +3161,7 @@ def _blocks_weight_limit() -> bool: version_key=version_key, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + block_time=block_time, ) retries += 1 return success, message diff --git a/bittensor/utils/__init__.py b/bittensor/utils/__init__.py index 534a2eedab..e0da1d9c99 100644 --- a/bittensor/utils/__init__.py +++ b/bittensor/utils/__init__.py @@ -1,4 +1,5 @@ import ast +import decimal import hashlib from collections import namedtuple from typing import Any, Literal, Union, Optional, TYPE_CHECKING @@ -165,6 +166,17 @@ def u64_normalized_float(x: int) -> float: return float(x) / float(U64_MAX) +def float_to_u64(value: float) -> int: + """Converts a float to a u64 int""" + + value = decimal.Decimal(str(value)) + + if not (0 <= value <= 1): + raise ValueError("Input value must be between 0 and 1") + + return int(value * U64_MAX) + + def get_hash(content, encoding="utf-8"): sha3 = hashlib.sha3_256() diff --git a/bittensor/utils/balance.py b/bittensor/utils/balance.py index 33d56be13c..35aa708654 100644 --- a/bittensor/utils/balance.py +++ b/bittensor/utils/balance.py @@ -6,6 +6,35 @@ from bittensor.core import settings +def _check_currencies(self, other): + """Checks that Balance objects have the same netuids to perform arithmetic operations. + + A warning is raised if the netuids differ. + + Example: + >>> balance1 = Balance.from_rao(1000).set_unit(12) + >>> balance2 = Balance.from_rao(500).set_unit(12) + >>> balance1 + balance2 # No warning + + >>> balance3 = Balance.from_rao(200).set_unit(15) + >>> balance1 + balance3 # Raises DeprecationWarning + + In this example: + - `from_rao` creates a Balance instance from the amount in rao (smallest unit). + - `set_unit(12)` sets the unit to correspond to subnet 12 (i.e., Alpha from netuid 12). + """ + if self.netuid != other.netuid: + warnings.simplefilter("default", DeprecationWarning) + warnings.warn( + "Balance objects must have the same netuid (Alpha currency) to perform arithmetic operations.\n" + f"First balance is `{self}`. Second balance is `{other}`.\n\n" + "To create a Balance instance with the correct netuid, use:\n" + "Balance.from_rao(1000).set_unit(12) # 1000 rao in subnet 12", + category=DeprecationWarning, + stacklevel=2, + ) + + class Balance: """ Represents the bittensor balance of the wallet, stored as rao (int). @@ -23,6 +52,7 @@ class Balance: rao_unit: str = settings.RAO_SYMBOL # This is the rao unit rao: int tao: float + netuid: int = 0 def __init__(self, balance: Union[int, float]): """ @@ -78,7 +108,8 @@ def __eq__(self, other: Union[int, float, "Balance"]): if other is None: return False - if hasattr(other, "rao"): + if isinstance(other, Balance): + _check_currencies(self, other) return self.rao == other.rao else: try: @@ -92,7 +123,8 @@ def __ne__(self, other: Union[int, float, "Balance"]): return not self == other def __gt__(self, other: Union[int, float, "Balance"]): - if hasattr(other, "rao"): + if isinstance(other, Balance): + _check_currencies(self, other) return self.rao > other.rao else: try: @@ -103,7 +135,8 @@ def __gt__(self, other: Union[int, float, "Balance"]): raise NotImplementedError("Unsupported type") def __lt__(self, other: Union[int, float, "Balance"]): - if hasattr(other, "rao"): + if isinstance(other, Balance): + _check_currencies(self, other) return self.rao < other.rao else: try: @@ -115,94 +148,112 @@ def __lt__(self, other: Union[int, float, "Balance"]): def __le__(self, other: Union[int, float, "Balance"]): try: + if isinstance(other, Balance): + _check_currencies(self, other) return self < other or self == other except TypeError: raise NotImplementedError("Unsupported type") def __ge__(self, other: Union[int, float, "Balance"]): try: + if isinstance(other, Balance): + _check_currencies(self, other) return self > other or self == other except TypeError: raise NotImplementedError("Unsupported type") def __add__(self, other: Union[int, float, "Balance"]): - if hasattr(other, "rao"): - return Balance.from_rao(int(self.rao + other.rao)) + if isinstance(other, Balance): + _check_currencies(self, other) + return Balance.from_rao(int(self.rao + other.rao)).set_unit(self.netuid) else: try: # Attempt to cast to int from rao - return Balance.from_rao(int(self.rao + other)) + return Balance.from_rao(int(self.rao + other)).set_unit(self.netuid) except (ValueError, TypeError): raise NotImplementedError("Unsupported type") def __radd__(self, other: Union[int, float, "Balance"]): try: + if isinstance(other, Balance): + _check_currencies(self, other) return self + other except TypeError: raise NotImplementedError("Unsupported type") def __sub__(self, other: Union[int, float, "Balance"]): try: + if isinstance(other, Balance): + _check_currencies(self, other) return self + -other except TypeError: raise NotImplementedError("Unsupported type") def __rsub__(self, other: Union[int, float, "Balance"]): try: + if isinstance(other, Balance): + _check_currencies(self, other) return -self + other except TypeError: raise NotImplementedError("Unsupported type") def __mul__(self, other: Union[int, float, "Balance"]): - if hasattr(other, "rao"): - return Balance.from_rao(int(self.rao * other.rao)) + if isinstance(other, Balance): + _check_currencies(self, other) + return Balance.from_rao(int(self.rao * other.rao)).set_unit(self.netuid) else: try: # Attempt to cast to int from rao - return Balance.from_rao(int(self.rao * other)) + return Balance.from_rao(int(self.rao * other)).set_unit(self.netuid) except (ValueError, TypeError): raise NotImplementedError("Unsupported type") def __rmul__(self, other: Union[int, float, "Balance"]): + if isinstance(other, Balance): + _check_currencies(self, other) return self * other def __truediv__(self, other: Union[int, float, "Balance"]): - if hasattr(other, "rao"): - return Balance.from_rao(int(self.rao / other.rao)) + if isinstance(other, Balance): + _check_currencies(self, other) + return Balance.from_rao(int(self.rao / other.rao)).set_unit(self.netuid) else: try: # Attempt to cast to int from rao - return Balance.from_rao(int(self.rao / other)) + return Balance.from_rao(int(self.rao / other)).set_unit(self.netuid) except (ValueError, TypeError): raise NotImplementedError("Unsupported type") def __rtruediv__(self, other: Union[int, float, "Balance"]): - if hasattr(other, "rao"): - return Balance.from_rao(int(other.rao / self.rao)) + if isinstance(other, Balance): + _check_currencies(self, other) + return Balance.from_rao(int(other.rao / self.rao)).set_unit(self.netuid) else: try: # Attempt to cast to int from rao - return Balance.from_rao(int(other / self.rao)) + return Balance.from_rao(int(other / self.rao)).set_unit(self.netuid) except (ValueError, TypeError): raise NotImplementedError("Unsupported type") def __floordiv__(self, other: Union[int, float, "Balance"]): - if hasattr(other, "rao"): - return Balance.from_rao(int(self.tao // other.tao)) + if isinstance(other, Balance): + _check_currencies(self, other) + return Balance.from_rao(int(self.tao // other.tao)).set_unit(self.netuid) else: try: # Attempt to cast to int from rao - return Balance.from_rao(int(self.rao // other)) + return Balance.from_rao(int(self.rao // other)).set_unit(self.netuid) except (ValueError, TypeError): raise NotImplementedError("Unsupported type") def __rfloordiv__(self, other: Union[int, float, "Balance"]): - if hasattr(other, "rao"): - return Balance.from_rao(int(other.rao // self.rao)) + if isinstance(other, Balance): + _check_currencies(self, other) + return Balance.from_rao(int(other.rao // self.rao)).set_unit(self.netuid) else: try: # Attempt to cast to int from rao - return Balance.from_rao(int(other // self.rao)) + return Balance.from_rao(int(other // self.rao)).set_unit(self.netuid) except (ValueError, TypeError): raise NotImplementedError("Unsupported type") @@ -210,16 +261,16 @@ def __nonzero__(self) -> bool: return bool(self.rao) def __neg__(self): - return Balance.from_rao(-self.rao) + return Balance.from_rao(-self.rao).set_unit(self.netuid) def __pos__(self): - return Balance.from_rao(self.rao) + return Balance.from_rao(self.rao).set_unit(self.netuid) def __abs__(self): - return Balance.from_rao(abs(self.rao)) + return Balance.from_rao(abs(self.rao)).set_unit(self.netuid) @staticmethod - def from_float(amount: float, netuid: int = 0): + def from_float(amount: float, netuid: int = 0) -> "Balance": """ Given tao, return :func:`Balance` object with rao(``int``) and tao(``float``), where rao = int(tao*pow(10,9)) Args: @@ -233,7 +284,7 @@ def from_float(amount: float, netuid: int = 0): return Balance(rao_).set_unit(netuid) @staticmethod - def from_tao(amount: float, netuid: int = 0): + def from_tao(amount: float, netuid: int = 0) -> "Balance": """ Given tao, return Balance object with rao(``int``) and tao(``float``), where rao = int(tao*pow(10,9)) @@ -248,7 +299,7 @@ def from_tao(amount: float, netuid: int = 0): return Balance(rao_).set_unit(netuid) @staticmethod - def from_rao(amount: int, netuid: int = 0): + def from_rao(amount: int, netuid: int = 0) -> "Balance": """ Given rao, return Balance object with rao(``int``) and tao(``float``), where rao = int(tao*pow(10,9)) @@ -262,7 +313,7 @@ def from_rao(amount: int, netuid: int = 0): return Balance(amount).set_unit(netuid) @staticmethod - def get_unit(netuid: int): + def get_unit(netuid: int) -> str: base = len(units) if netuid < base: return units[netuid] @@ -274,6 +325,7 @@ def get_unit(netuid: int): return result def set_unit(self, netuid: int): + self.netuid = netuid self.unit = Balance.get_unit(netuid) self.rao_unit = Balance.get_unit(netuid) return self @@ -777,18 +829,18 @@ def fixed_to_float( ] -def tao(amount: float) -> Balance: +def tao(amount: float, netuid: int = 0) -> Balance: """ Helper function to create a Balance object from a float (Tao) """ - return Balance.from_tao(amount) + return Balance.from_tao(amount).set_unit(netuid) -def rao(amount: int) -> Balance: +def rao(amount: int, netuid: int = 0) -> Balance: """ Helper function to create a Balance object from an int (Rao) """ - return Balance.from_rao(amount) + return Balance.from_rao(amount).set_unit(netuid) def check_and_convert_to_balance( diff --git a/bittensor/utils/easy_imports.py b/bittensor/utils/easy_imports.py index 7718ceede0..59ebeda7ba 100644 --- a/bittensor/utils/easy_imports.py +++ b/bittensor/utils/easy_imports.py @@ -70,24 +70,32 @@ DelegateTakeTooHigh, DelegateTakeTooLow, DelegateTxRateLimitExceeded, + DuplicateChild, HotKeyAccountNotExists, IdentityError, InternalServerError, + InvalidChild, InvalidRequestNameError, MetadataError, NominationError, NonAssociatedColdKey, NotDelegateError, + NotEnoughStakeToSetChildkeys, NotRegisteredError, NotVerifiedException, PostProcessException, PriorityException, + ProportionOverflow, RegistrationError, + RegistrationNotPermittedOnRootSubnet, RunException, StakeError, + SubNetworkDoesNotExist, SynapseDendriteNoneException, SynapseParsingError, + TooManyChildren, TransferError, + TxRateLimitExceeded, UnknownSynapseError, UnstakeError, ) diff --git a/bittensor/utils/registration/pow.py b/bittensor/utils/registration/pow.py index 8989e64acf..078c73d09f 100644 --- a/bittensor/utils/registration/pow.py +++ b/bittensor/utils/registration/pow.py @@ -17,8 +17,6 @@ import numpy from Crypto.Hash import keccak -from rich import console as rich_console, status as rich_status -from rich.console import Console from bittensor.utils.btlogging import logging from bittensor.utils.formatting import get_human_readable, millify @@ -516,14 +514,38 @@ class RegistrationStatistics: block_hash: str +class Status: + def __init__(self, status: str): + self._status = status + + def start(self): + pass + + def stop(self): + pass + + def update(self, status: str): + self._status = status + + +class Console: + @staticmethod + def status(status: str): + return Status(status) + + @staticmethod + def log(text: str): + print(text) + + class RegistrationStatisticsLogger: """Logs statistics for a registration.""" - status: Optional[rich_status.Status] + status: Optional["Status"] def __init__( self, - console: Optional[rich_console.Console] = None, + console: Optional["Console"] = None, output_in_place: bool = True, ) -> None: if console is None: diff --git a/bittensor/utils/version.py b/bittensor/utils/version.py index 040a46a311..c8f899d760 100644 --- a/bittensor/utils/version.py +++ b/bittensor/utils/version.py @@ -3,8 +3,9 @@ from typing import Optional import requests -from packaging.version import Version +from packaging.version import Version, InvalidVersion +from bittensor import __name__ from bittensor.core.settings import __version__, PIPADDRESS from bittensor.utils.btlogging import logging @@ -115,3 +116,27 @@ def version_checking(timeout: int = 15): check_version(timeout) except VersionCheckError: logging.exception("Version check failed") + + +def check_latest_version_in_pypi(): + """Check for the latest version of the package on PyPI.""" + package_name = __name__ + url = f"https://pypi.org/pypi/{package_name}/json" + + try: + response = requests.get(url, timeout=5) + response.raise_for_status() + latest_version = response.json()["info"]["version"] + installed_version = __version__ + try: + if Version(installed_version) < Version(latest_version): + print( + f"\n🔔 New version is available `{package_name} v.{latest_version}`" + ) + print("📦 Use command `pip install --upgrade bittensor` to update.") + except InvalidVersion: + # stay silent if InvalidVersion + pass + except (requests.RequestException, KeyError) as e: + # stay silent if not internet connection or pypi.org issue + pass diff --git a/bittensor/utils/weight_utils.py b/bittensor/utils/weight_utils.py index a93f584728..5c98b1f383 100644 --- a/bittensor/utils/weight_utils.py +++ b/bittensor/utils/weight_utils.py @@ -245,7 +245,6 @@ def process_weights_for_netuid( """ logging.debug("process_weights_for_netuid()") - logging.debug(f"weights: {weights}") logging.debug(f"netuid {netuid}") logging.debug(f"subtensor: {subtensor}") logging.debug(f"metagraph: {metagraph}") @@ -254,6 +253,48 @@ def process_weights_for_netuid( if metagraph is None: metagraph = subtensor.metagraph(netuid) + return process_weights( + uids=uids, + weights=weights, + num_neurons=metagraph.n, + min_allowed_weights=subtensor.min_allowed_weights(netuid=netuid), + max_weight_limit=subtensor.max_weight_limit(netuid=netuid), + exclude_quantile=exclude_quantile, + ) + + +def process_weights( + uids: Union[NDArray[np.int64], "torch.Tensor"], + weights: Union[NDArray[np.float32], "torch.Tensor"], + num_neurons: int, + min_allowed_weights: Optional[int], + max_weight_limit: Optional[float], + exclude_quantile: int = 0, +) -> Union[ + tuple["torch.Tensor", "torch.FloatTensor"], + tuple[NDArray[np.int64], NDArray[np.float32]], +]: + """ + Processes weight tensors for a given weights and UID arrays and hyperparams, applying constraints + and normalization based on the subtensor and metagraph data. This function can handle both NumPy arrays and PyTorch + tensors. + + Args: + uids (Union[NDArray[np.int64], "torch.Tensor"]): Array of unique identifiers of the neurons. + weights (Union[NDArray[np.float32], "torch.Tensor"]): Array of weights associated with the user IDs. + num_neurons (int): The number of neurons in the network. + min_allowed_weights (Optional[int]): Subnet hyperparam Minimum number of allowed weights. + max_weight_limit (Optional[float]): Subnet hyperparam Maximum weight limit. + exclude_quantile (int): Quantile threshold for excluding lower weights. Defaults to ``0``. + + Returns: + Union[tuple["torch.Tensor", "torch.FloatTensor"], tuple[NDArray[np.int64], NDArray[np.float32]]]: tuple + containing the array of user IDs and the corresponding normalized weights. The data type of the return + matches the type of the input weights (NumPy or PyTorch). + """ + logging.debug("process_weights()") + logging.debug(f"weights: {weights}") + # Cast weights to floats. if use_torch(): if not isinstance(weights, torch.FloatTensor): @@ -265,8 +306,6 @@ def process_weights_for_netuid( # Network configuration parameters from an subtensor. # These parameters determine the range of acceptable weights for each neuron. quantile = exclude_quantile / U16_MAX - min_allowed_weights = subtensor.min_allowed_weights(netuid=netuid) - max_weight_limit = subtensor.max_weight_limit(netuid=netuid) logging.debug(f"quantile: {quantile}") logging.debug(f"min_allowed_weights: {min_allowed_weights}") logging.debug(f"max_weight_limit: {max_weight_limit}") @@ -280,12 +319,12 @@ def process_weights_for_netuid( non_zero_weight_uids = uids[non_zero_weight_idx] non_zero_weights = weights[non_zero_weight_idx] nzw_size = non_zero_weights.numel() if use_torch() else non_zero_weights.size - if nzw_size == 0 or metagraph.n < min_allowed_weights: + if nzw_size == 0 or num_neurons < min_allowed_weights: logging.warning("No non-zero weights returning all ones.") final_weights = ( - torch.ones((metagraph.n)).to(metagraph.n) / metagraph.n + torch.ones(num_neurons).to(num_neurons) / num_neurons if use_torch() - else np.ones((metagraph.n), dtype=np.int64) / metagraph.n + else np.ones(num_neurons, dtype=np.int64) / num_neurons ) logging.debug(f"final_weights: {final_weights}") final_weights_count = ( @@ -303,11 +342,11 @@ def process_weights_for_netuid( logging.warning( "No non-zero weights less then min allowed weight, returning all ones." ) - # ( const ): Should this be np.zeros( ( metagraph.n ) ) to reset everyone to build up weight? + # ( const ): Should this be np.zeros( ( num_neurons ) ) to reset everyone to build up weight? weights = ( - torch.ones((metagraph.n)).to(metagraph.n) * 1e-5 + torch.ones(num_neurons).to(num_neurons) * 1e-5 if use_torch() - else np.ones((metagraph.n), dtype=np.int64) * 1e-5 + else np.ones(num_neurons, dtype=np.int64) * 1e-5 ) # creating minimum even non-zero weights weights[non_zero_weight_idx] += non_zero_weights logging.debug(f"final_weights: {weights}") diff --git a/pyproject.toml b/pyproject.toml index 2c01c2858d..2232e33960 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "bittensor" -version = "9.2.0" +version = "9.3.0" description = "Bittensor" readme = "README.md" authors = [ @@ -22,23 +22,20 @@ dependencies = [ "munch~=2.5.0", "numpy~=2.0.1", "msgpack-numpy-opentensor~=0.5.0", - "nest_asyncio", - "netaddr", + "nest_asyncio==1.6.0", + "netaddr==1.3.0", "packaging", "python-statemachine~=2.1", "pycryptodome>=3.18.0,<4.0.0", - "pyyaml", - "retry", - "requests", - "rich", + "pyyaml>=6.0", + "retry==0.9.2", + "requests>=2.0.0,<3.0", "pydantic>=2.3, <3", - "python-Levenshtein", "scalecodec==1.2.11", "uvicorn", - "websockets>=14.1", - "bittensor-commit-reveal>=0.2.0", - "bittensor-wallet>=3.0.4", - "async-substrate-interface>=1.0.8" + "bittensor-commit-reveal>=0.3.1", + "bittensor-wallet>=3.0.8", + "async-substrate-interface>=1.1.0" ] [project.optional-dependencies] @@ -67,6 +64,9 @@ dev = [ torch = [ "torch>=1.13.1,<2.6.0" ] +cli = [ + "bittensor-cli>=9.0.2" +] [project.urls] # more details can be found here diff --git a/requirements/prod.txt b/requirements/prod.txt index bcd699d8b7..893d925ce9 100644 --- a/requirements/prod.txt +++ b/requirements/prod.txt @@ -21,6 +21,6 @@ python-Levenshtein scalecodec==1.2.11 uvicorn websockets>=14.1 -bittensor-commit-reveal>=0.2.0 -bittensor-wallet>=3.0.4 +bittensor-commit-reveal>=0.3.1 +bittensor-wallet>=3.0.7 async-substrate-interface>=1.0.4 diff --git a/tests/e2e_tests/conftest.py b/tests/e2e_tests/conftest.py index 51a865b1b8..0170cbd302 100644 --- a/tests/e2e_tests/conftest.py +++ b/tests/e2e_tests/conftest.py @@ -19,6 +19,9 @@ setup_wallet, ) +LOCALNET_IMAGE_NAME = "ghcr.io/opentensor/subtensor-localnet:devnet-ready" +CONTAINER_NAME_PREFIX = "test_local_chain_" + def wait_for_node_start(process, timestamp=None): """Waits for node to start in the docker.""" @@ -75,7 +78,7 @@ def local_chain(request): logging.warning("Docker not found in the operating system!") logging.warning(docker_command) logging.warning("Tests are run in legacy mode.") - yield from legacy_runner(request) + yield from legacy_runner(params) def legacy_runner(params): @@ -93,7 +96,6 @@ def legacy_runner(params): # Compile commands to send to process cmds = shlex.split(f"{script_path} {args}") - with subprocess.Popen( cmds, start_new_session=True, @@ -132,6 +134,7 @@ def is_docker_running(): stderr=subprocess.DEVNULL, check=True, ) + subprocess.run(["docker", "pull", LOCALNET_IMAGE_NAME], check=True) return True except subprocess.CalledProcessError: return False @@ -161,8 +164,32 @@ def try_start_docker(): print("Docker wasn't run. Manual start may be required.") return False - container_name = f"test_local_chain_{str(time.time()).replace(".", "_")}" - image_name = "ghcr.io/opentensor/subtensor-localnet:latest" + def stop_existing_test_containers(): + """Stop running Docker containers with names starting with 'test_local_chain_'.""" + try: + existing_container_result = subprocess.run( + [ + "docker", + "ps", + "--filter", + f"name={CONTAINER_NAME_PREFIX}", + "--format", + "{{.ID}}", + ], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + check=True, + ) + container_ids = existing_container_result.stdout.strip().splitlines() + for cid in container_ids: + if cid: + print(f"Stopping existing container: {cid}") + subprocess.run(["docker", "stop", cid], check=True) + except subprocess.CalledProcessError as e: + print(f"Failed to stop existing containers: {e}") + + container_name = f"{CONTAINER_NAME_PREFIX}{str(time.time()).replace('.', '_')}" # Command to start container cmds = [ @@ -175,12 +202,14 @@ def try_start_docker(): "9944:9944", "-p", "9945:9945", - image_name, + LOCALNET_IMAGE_NAME, params, ] try_start_docker() + stop_existing_test_containers() + # Start container with subprocess.Popen( cmds, diff --git a/tests/e2e_tests/test_commit_reveal_v3.py b/tests/e2e_tests/test_commit_reveal_v3.py index 52d2324c62..6d45cbd94d 100644 --- a/tests/e2e_tests/test_commit_reveal_v3.py +++ b/tests/e2e_tests/test_commit_reveal_v3.py @@ -12,7 +12,7 @@ ) -@pytest.mark.parametrize("local_chain", [False], indirect=True) +@pytest.mark.parametrize("local_chain", [True], indirect=True) @pytest.mark.asyncio async def test_commit_and_reveal_weights_cr3(local_chain, subtensor, alice_wallet): """ @@ -29,6 +29,7 @@ async def test_commit_and_reveal_weights_cr3(local_chain, subtensor, alice_walle Raises: AssertionError: If any of the checks or verifications fail """ + BLOCK_TIME = 0.25 # 12 for non-fast-block, 0.25 for fast block netuid = 2 logging.console.info("Testing test_commit_and_reveal_weights") @@ -71,9 +72,8 @@ async def test_commit_and_reveal_weights_cr3(local_chain, subtensor, alice_walle 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 + # Change the tempo of the subnet + tempo_set = 50 assert ( sudo_set_admin_utils( local_chain, @@ -101,8 +101,8 @@ async def test_commit_and_reveal_weights_cr3(local_chain, subtensor, alice_walle f"Checking if window is too low with Current block: {current_block}, next tempo: {upcoming_tempo}" ) - # Wait for 2 tempos to pass as CR3 only reveals weights after 2 tempos - subtensor.wait_for_block(20) + # Wait for 2 tempos to pass as CR3 only reveals weights after 2 tempos + 1 + subtensor.wait_for_block((tempo_set * 2) + 1) # Lower than this might mean weights will get revealed before we can check them if upcoming_tempo - current_block < 3: @@ -127,6 +127,7 @@ async def test_commit_and_reveal_weights_cr3(local_chain, subtensor, alice_walle weights=weight_vals, wait_for_inclusion=True, wait_for_finalization=True, + block_time=BLOCK_TIME, ) # Assert committing was a success @@ -148,7 +149,7 @@ async def test_commit_and_reveal_weights_cr3(local_chain, subtensor, alice_walle # Ensure the expected drand round is well in the future assert ( - expected_reveal_round > latest_drand_round + 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 diff --git a/tests/e2e_tests/test_commit_weights.py b/tests/e2e_tests/test_commit_weights.py index 4d966c8c37..c4701ad71e 100644 --- a/tests/e2e_tests/test_commit_weights.py +++ b/tests/e2e_tests/test_commit_weights.py @@ -7,6 +7,7 @@ from tests.e2e_tests.utils.chain_interactions import ( sudo_set_admin_utils, sudo_set_hyperparameter_bool, + use_and_wait_for_next_nonce, wait_epoch, ) @@ -227,42 +228,44 @@ async def test_commit_weights_uses_next_nonce(local_chain, subtensor, alice_wall 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 + async with use_and_wait_for_next_nonce(subtensor, alice_wallet): + 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 + + async with use_and_wait_for_next_nonce(subtensor, alice_wallet): + 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 + + async with use_and_wait_for_next_nonce(subtensor, alice_wallet): + 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(10) # Wait for the txs to be included in the chain diff --git a/tests/e2e_tests/test_commitment.py b/tests/e2e_tests/test_commitment.py index 66319c01d0..0df20688ea 100644 --- a/tests/e2e_tests/test_commitment.py +++ b/tests/e2e_tests/test_commitment.py @@ -1,12 +1,13 @@ import pytest +from async_substrate_interface.errors import SubstrateRequestException from bittensor import logging -from async_substrate_interface.errors import SubstrateRequestException +from tests.e2e_tests.utils.chain_interactions import sudo_set_admin_utils logging.set_trace() -def test_commitment(subtensor, alice_wallet): +def test_commitment(local_chain, subtensor, alice_wallet): with pytest.raises(SubstrateRequestException, match="AccountNotAllowedCommit"): subtensor.set_commitment( alice_wallet, @@ -37,14 +38,27 @@ def test_commitment(subtensor, alice_wallet): data="Hello World!", ) + status, error = sudo_set_admin_utils( + local_chain, + alice_wallet, + call_module="Commitments", + call_function="set_max_space", + call_params={ + "netuid": 1, + "new_limit": len("Hello World!"), + }, + ) + + assert status is True, error + with pytest.raises( SubstrateRequestException, - match="CommitmentSetRateLimitExceeded", + match="SpaceLimitExceeded", ): subtensor.set_commitment( alice_wallet, netuid=1, - data="Hello World!", + data="Hello World!1", ) assert "Hello World!" == subtensor.get_commitment( @@ -59,7 +73,7 @@ def test_commitment(subtensor, alice_wallet): @pytest.mark.asyncio -async def test_commitment_async(async_subtensor, alice_wallet): +async def test_commitment_async(local_chain, async_subtensor, alice_wallet): async with async_subtensor as sub: with pytest.raises(SubstrateRequestException, match="AccountNotAllowedCommit"): await sub.set_commitment( @@ -91,14 +105,27 @@ async def test_commitment_async(async_subtensor, alice_wallet): data="Hello World!", ) + status, error = sudo_set_admin_utils( + local_chain, + alice_wallet, + call_module="Commitments", + call_function="set_max_space", + call_params={ + "netuid": 1, + "new_limit": len("Hello World!"), + }, + ) + + assert status is True, error + with pytest.raises( SubstrateRequestException, - match="CommitmentSetRateLimitExceeded", + match="SpaceLimitExceeded", ): await sub.set_commitment( alice_wallet, netuid=1, - data="Hello World!", + data="Hello World!1", ) assert "Hello World!" == await sub.get_commitment( diff --git a/tests/e2e_tests/test_delegate.py b/tests/e2e_tests/test_delegate.py index c5ea365611..59f7bbbb75 100644 --- a/tests/e2e_tests/test_delegate.py +++ b/tests/e2e_tests/test_delegate.py @@ -3,11 +3,15 @@ import bittensor from bittensor.core.chain_data.chain_identity import ChainIdentity from bittensor.core.chain_data.delegate_info import DelegatedInfo, DelegateInfo +from bittensor.core.chain_data.proposal_vote_data import ProposalVoteData from bittensor.utils.balance import Balance from tests.e2e_tests.utils.chain_interactions import ( + propose, set_identity, sudo_set_admin_utils, + vote, ) +from tests.helpers.helpers import CLOSE_IN_VALUE DEFAULT_DELEGATE_TAKE = 0.179995422293431 @@ -321,3 +325,105 @@ def test_nominator_min_required_stake(local_chain, subtensor, alice_wallet, bob_ ) assert stake == Balance(0) + + +def test_get_vote_data(subtensor, alice_wallet): + """ + Tests: + - Sends Propose + - Checks existing Proposals + - Votes + - Checks Proposal is updated + """ + + subtensor.root_register(alice_wallet) + + proposals = subtensor.query_map( + "Triumvirate", + "ProposalOf", + params=[], + ) + + assert proposals.records == [] + + success, error = propose( + subtensor, + alice_wallet, + proposal=subtensor.substrate.compose_call( + call_module="Triumvirate", + call_function="set_members", + call_params={ + "new_members": [], + "prime": None, + "old_count": 0, + }, + ), + duration=1_000_000, + ) + + assert error == "" + assert success is True + + proposals = subtensor.query_map( + "Triumvirate", + "ProposalOf", + params=[], + ) + proposals = { + bytes(proposal_hash[0]): proposal.value for proposal_hash, proposal in proposals + } + + assert list(proposals.values()) == [ + { + "Triumvirate": ( + { + "set_members": { + "new_members": (), + "prime": None, + "old_count": 0, + }, + }, + ), + }, + ] + + proposal_hash = list(proposals.keys())[0] + proposal_hash = f"0x{proposal_hash.hex()}" + + proposal = subtensor.get_vote_data( + proposal_hash, + ) + + assert proposal == ProposalVoteData( + ayes=[], + end=CLOSE_IN_VALUE(1_000_000, subtensor.block), + index=0, + nays=[], + threshold=3, + ) + + success, error = vote( + subtensor, + alice_wallet, + alice_wallet.hotkey.ss58_address, + proposal_hash, + index=0, + approve=True, + ) + + assert error == "" + assert success is True + + proposal = subtensor.get_vote_data( + proposal_hash, + ) + + assert proposal == ProposalVoteData( + ayes=[ + alice_wallet.hotkey.ss58_address, + ], + end=CLOSE_IN_VALUE(1_000_000, subtensor.block), + index=0, + nays=[], + threshold=3, + ) diff --git a/tests/e2e_tests/test_dendrite.py b/tests/e2e_tests/test_dendrite.py index b2e891ff73..229a7f40b9 100644 --- a/tests/e2e_tests/test_dendrite.py +++ b/tests/e2e_tests/test_dendrite.py @@ -34,6 +34,13 @@ async def test_dendrite(local_chain, subtensor, templates, alice_wallet, bob_wal # Verify subnet created successfully assert subtensor.subnet_exists(netuid), "Subnet wasn't created successfully" + # Make sure Alice is Top Validator + assert subtensor.add_stake( + alice_wallet, + netuid=netuid, + amount=Balance.from_tao(1), + ) + # update max_allowed_validators so only one neuron can get validator_permit assert sudo_set_admin_utils( local_chain, diff --git a/tests/e2e_tests/test_hotkeys.py b/tests/e2e_tests/test_hotkeys.py index 126690d46c..86ff768688 100644 --- a/tests/e2e_tests/test_hotkeys.py +++ b/tests/e2e_tests/test_hotkeys.py @@ -1,7 +1,8 @@ import pytest +import bittensor from tests.e2e_tests.utils.chain_interactions import ( - set_children, + sudo_set_admin_utils, wait_epoch, ) @@ -56,16 +57,44 @@ def test_hotkeys(subtensor, alice_wallet): @pytest.mark.asyncio -async def test_children(subtensor, alice_wallet, bob_wallet): +async def test_children(local_chain, subtensor, alice_wallet, bob_wallet): """ Tests: - Get default children (empty list) - Update children list + - Checking pending children - Checking cooldown period - Trigger rate limit - Clear children list """ + with pytest.raises(bittensor.RegistrationNotPermittedOnRootSubnet): + subtensor.set_children( + alice_wallet, + alice_wallet.hotkey.ss58_address, + netuid=0, + children=[], + raise_error=True, + ) + + with pytest.raises(bittensor.NonAssociatedColdKey): + subtensor.set_children( + alice_wallet, + alice_wallet.hotkey.ss58_address, + netuid=1, + children=[], + raise_error=True, + ) + + with pytest.raises(bittensor.SubNetworkDoesNotExist): + subtensor.set_children( + alice_wallet, + alice_wallet.hotkey.ss58_address, + netuid=2, + children=[], + raise_error=True, + ) + subtensor.burned_register( alice_wallet, netuid=1, @@ -84,16 +113,82 @@ async def test_children(subtensor, alice_wallet, bob_wallet): assert success is True assert children == [] - success, error = set_children( - subtensor, + with pytest.raises(bittensor.InvalidChild): + subtensor.set_children( + alice_wallet, + alice_wallet.hotkey.ss58_address, + netuid=1, + children=[ + ( + 1.0, + alice_wallet.hotkey.ss58_address, + ), + ], + raise_error=True, + ) + + with pytest.raises(bittensor.TooManyChildren): + subtensor.set_children( + alice_wallet, + alice_wallet.hotkey.ss58_address, + netuid=1, + children=[ + ( + 0.1, + bob_wallet.hotkey.ss58_address, + ) + for _ in range(10) + ], + raise_error=True, + ) + + with pytest.raises(bittensor.ProportionOverflow): + subtensor.set_children( + alice_wallet, + alice_wallet.hotkey.ss58_address, + netuid=1, + children=[ + ( + 1.0, + bob_wallet.hotkey.ss58_address, + ), + ( + 1.0, + "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", + ), + ], + raise_error=True, + ) + + with pytest.raises(bittensor.DuplicateChild): + subtensor.set_children( + alice_wallet, + alice_wallet.hotkey.ss58_address, + netuid=1, + children=[ + ( + 0.5, + bob_wallet.hotkey.ss58_address, + ), + ( + 0.5, + bob_wallet.hotkey.ss58_address, + ), + ], + raise_error=True, + ) + + subtensor.set_children( alice_wallet, + alice_wallet.hotkey.ss58_address, netuid=1, children=[ ( - 2**64 - 1, + 1.0, bob_wallet.hotkey.ss58_address, ), ], + raise_error=True, ) assert error == "" @@ -101,6 +196,7 @@ async def test_children(subtensor, alice_wallet, bob_wallet): set_children_block = subtensor.get_current_block() + # children not set yet (have to wait cool-down period) success, children, error = subtensor.get_children( alice_wallet.hotkey.ss58_address, block=set_children_block, @@ -111,7 +207,20 @@ async def test_children(subtensor, alice_wallet, bob_wallet): assert children == [] assert error == "" - subtensor.wait_for_block(set_children_block + SET_CHILDREN_COOLDOWN_PERIOD) + # children are in pending state + pending, cooldown = subtensor.get_children_pending( + alice_wallet.hotkey.ss58_address, + netuid=1, + ) + + assert pending == [ + ( + 1.0, + "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty", + ), + ] + + subtensor.wait_for_block(cooldown) await wait_epoch(subtensor, netuid=1) @@ -129,29 +238,42 @@ async def test_children(subtensor, alice_wallet, bob_wallet): ) ] - success, error = set_children( - subtensor, - alice_wallet, + # pending queue is empty + pending, cooldown = subtensor.get_children_pending( + alice_wallet.hotkey.ss58_address, netuid=1, - children=[], ) - assert "`TxRateLimitExceeded(Module)`" in error - assert success is False + assert pending == [] + + with pytest.raises(bittensor.TxRateLimitExceeded): + subtensor.set_children( + alice_wallet, + alice_wallet.hotkey.ss58_address, + netuid=1, + children=[], + raise_error=True, + ) subtensor.wait_for_block(set_children_block + SET_CHILDREN_RATE_LIMIT) - success, error = set_children( - subtensor, + subtensor.set_children( alice_wallet, + alice_wallet.hotkey.ss58_address, netuid=1, children=[], + raise_error=True, ) + set_children_block = subtensor.get_current_block() - assert error == "" - assert success is True + pending, cooldown = subtensor.get_children_pending( + alice_wallet.hotkey.ss58_address, + netuid=1, + ) - subtensor.wait_for_block(subtensor.block + SET_CHILDREN_COOLDOWN_PERIOD) + assert pending == [] + + subtensor.wait_for_block(cooldown) await wait_epoch(subtensor, netuid=1) @@ -163,3 +285,28 @@ async def test_children(subtensor, alice_wallet, bob_wallet): assert error == "" assert success is True assert children == [] + + subtensor.wait_for_block(set_children_block + SET_CHILDREN_RATE_LIMIT) + + sudo_set_admin_utils( + local_chain, + alice_wallet, + call_function="sudo_set_stake_threshold", + call_params={ + "min_stake": 1_000_000_000_000, + }, + ) + + with pytest.raises(bittensor.NotEnoughStakeToSetChildkeys): + subtensor.set_children( + alice_wallet, + alice_wallet.hotkey.ss58_address, + netuid=1, + children=[ + ( + 1.0, + bob_wallet.hotkey.ss58_address, + ), + ], + raise_error=True, + ) diff --git a/tests/e2e_tests/test_incentive.py b/tests/e2e_tests/test_incentive.py index ac71a54fc2..56f23ddd52 100644 --- a/tests/e2e_tests/test_incentive.py +++ b/tests/e2e_tests/test_incentive.py @@ -3,11 +3,14 @@ import pytest from tests.e2e_tests.utils.chain_interactions import ( + root_set_subtensor_hyperparameter_values, sudo_set_admin_utils, wait_epoch, wait_interval, ) +DURATION_OF_START_CALL = 10 + @pytest.mark.asyncio async def test_incentive(local_chain, subtensor, templates, alice_wallet, bob_wallet): @@ -53,7 +56,7 @@ async def test_incentive(local_chain, subtensor, templates, alice_wallet, bob_wa assert alice_neuron.validator_permit is True assert alice_neuron.dividends == 0 - assert alice_neuron.stake.tao > 0 + assert alice_neuron.stake.tao == 0 assert alice_neuron.validator_trust == 0 assert alice_neuron.incentive == 0 assert alice_neuron.consensus == 0 @@ -66,6 +69,20 @@ async def test_incentive(local_chain, subtensor, templates, alice_wallet, bob_wa assert bob_neuron.rank == 0 assert bob_neuron.trust == 0 + subtensor.wait_for_block(DURATION_OF_START_CALL) + + # Subnet "Start Call" https://github.com/opentensor/bits/pull/13 + status, error = await root_set_subtensor_hyperparameter_values( + local_chain, + alice_wallet, + call_function="start_call", + call_params={ + "netuid": netuid, + }, + ) + + assert status is True, error + # update weights_set_rate_limit for fast-blocks tempo = subtensor.tempo(netuid) status, error = sudo_set_admin_utils( @@ -84,8 +101,7 @@ async def test_incentive(local_chain, subtensor, templates, alice_wallet, bob_wa async with templates.miner(bob_wallet, netuid): async with templates.validator(alice_wallet, netuid) as validator: # wait for the Validator to process and set_weights - async with asyncio.timeout(60): - await validator.set_weights.wait() + await asyncio.wait_for(validator.set_weights.wait(), 60) # Wait till new epoch await wait_interval(tempo, subtensor, netuid) @@ -105,9 +121,26 @@ async def test_incentive(local_chain, subtensor, templates, alice_wallet, bob_wa assert alice_neuron.rank < 0.5 bob_neuron = metagraph.neurons[1] + assert bob_neuron.incentive > 0.5 assert bob_neuron.consensus > 0.5 assert bob_neuron.rank > 0.5 assert bob_neuron.trust == 1 + bonds = subtensor.bonds(netuid) + + assert bonds == [ + ( + 0, + [ + (0, 65535), + (1, 65535), + ], + ), + ( + 1, + [], + ), + ] + print("✅ Passed test_incentive") diff --git a/tests/e2e_tests/test_metagraph.py b/tests/e2e_tests/test_metagraph.py index f199cf44e3..29bb9ceaa9 100644 --- a/tests/e2e_tests/test_metagraph.py +++ b/tests/e2e_tests/test_metagraph.py @@ -202,12 +202,12 @@ def test_metagraph_info(subtensor, alice_wallet): blocks_since_last_step=1, subnet_emission=Balance(0), alpha_in=Balance.from_tao(10), - alpha_out=Balance.from_tao(2), + alpha_out=Balance.from_tao(1), tao_in=Balance.from_tao(10), - alpha_out_emission=Balance.from_tao(1), + alpha_out_emission=Balance(0), alpha_in_emission=Balance(0), tao_in_emission=Balance(0), - pending_alpha_emission=Balance.from_tao(0.820004577), + pending_alpha_emission=Balance(0), pending_root_emission=Balance(0), subnet_volume=Balance(0), moving_price=Balance(0), diff --git a/tests/e2e_tests/test_reveal_commitements.py b/tests/e2e_tests/test_reveal_commitements.py new file mode 100644 index 0000000000..37af2eebd2 --- /dev/null +++ b/tests/e2e_tests/test_reveal_commitements.py @@ -0,0 +1,99 @@ +import time + +import pytest + +from bittensor.utils.btlogging import logging + + +@pytest.mark.parametrize("local_chain", [True], indirect=True) +@pytest.mark.asyncio +async def test_set_reveal_commitment(local_chain, subtensor, alice_wallet, bob_wallet): + """ + Tests the set/reveal commitments with TLE (time-locked encrypted commitments) mechanism. + + Steps: + 1. Register a subnet through Alice + 2. Register Bob's neuron and add stake + 3. Set commitment from Alice hotkey + 4. Set commitment from Bob hotkey + 5. Wait until commitment is revealed. + 5. Verify commitment is revealed by Alice and Bob and available via mutual call. + 6. Verify commitment is revealed by Alice and Bob and available via separate calls. + Raises: + AssertionError: If any of the checks or verifications fail + + Note: Actually we can run this tests in fast block mode. For this we need to set `BLOCK_TIME` to 0.25 and replace + `False` to `True` in `pytest.mark.parametrize` decorator. + """ + BLOCK_TIME = 0.25 # 12 for non-fast-block, 0.25 for fast block + BLOCKS_UNTIL_REVEAL = 10 + + NETUID = 2 + + logging.console.info("Testing Drand encrypted commitments.") + + # Register subnet as Alice + assert subtensor.register_subnet( + alice_wallet, True, True + ), "Unable to register the subnet" + + # Register Bob's neuron + assert subtensor.burned_register( + bob_wallet, NETUID, True, True + ), "Bob's neuron was not register." + + # Verify subnet 2 created successfully + assert subtensor.subnet_exists(NETUID), "Subnet wasn't created successfully" + + # Set commitment from Alice hotkey + message_alice = f"This is test message with time {time.time()} from Alice." + + response = subtensor.set_reveal_commitment( + alice_wallet, NETUID, message_alice, BLOCKS_UNTIL_REVEAL, BLOCK_TIME + ) + assert response[0] is True + + # Set commitment from Bob's hotkey + message_bob = f"This is test message with time {time.time()} from Bob." + + response = subtensor.set_reveal_commitment( + bob_wallet, NETUID, message_bob, BLOCKS_UNTIL_REVEAL, block_time=BLOCK_TIME + ) + assert response[0] is True + + target_reveal_round = response[1] + + # Sometimes the chain doesn't update the repository right away and the commit doesn't appear in the expected + # `last_drand_round`. In this case need to wait a bit. + print(f"Waiting for reveal round {target_reveal_round}") + while subtensor.last_drand_round() <= target_reveal_round + 1: + # wait one drand period (3 sec) + print(f"Current last reveled drand round {subtensor.last_drand_round()}") + time.sleep(3) + + actual_all = subtensor.get_all_revealed_commitments(NETUID) + + alice_result = actual_all.get(alice_wallet.hotkey.ss58_address) + assert alice_result is not None, "Alice's commitment was not received." + + bob_result = actual_all.get(bob_wallet.hotkey.ss58_address) + assert bob_result is not None, "Bob's commitment was not received." + + alice_actual_block, alice_actual_message = alice_result[0] + bob_actual_block, bob_actual_message = bob_result[0] + + # We do not check the release block because it is a dynamic number. It depends on the load of the chain, the number + # of commits in the chain and the computing power. + assert message_alice == alice_actual_message + assert message_bob == bob_actual_message + + # Assertions for get_revealed_commitment (based of hotkey) + actual_alice_block, actual_alice_message = subtensor.get_revealed_commitment( + NETUID, 0 + )[0] + actual_bob_block, actual_bob_message = subtensor.get_revealed_commitment(NETUID, 1)[ + 0 + ] + + assert message_alice == actual_alice_message + assert message_bob == actual_bob_message diff --git a/tests/e2e_tests/test_set_weights.py b/tests/e2e_tests/test_set_weights.py index 8a279e3ccf..8211e62aa9 100644 --- a/tests/e2e_tests/test_set_weights.py +++ b/tests/e2e_tests/test_set_weights.py @@ -6,6 +6,7 @@ from tests.e2e_tests.utils.chain_interactions import ( sudo_set_hyperparameter_bool, sudo_set_admin_utils, + use_and_wait_for_next_nonce, wait_epoch, ) @@ -108,16 +109,17 @@ async def test_set_weights_uses_next_nonce(local_chain, subtensor, alice_wallet) # 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}" + async with use_and_wait_for_next_nonce(subtensor, alice_wallet): + 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, message # Wait for the txs to be included in the chain await wait_epoch(subtensor, netuid=netuids[-1], times=4) diff --git a/tests/e2e_tests/test_staking.py b/tests/e2e_tests/test_staking.py index 11b7b45054..7742b97414 100644 --- a/tests/e2e_tests/test_staking.py +++ b/tests/e2e_tests/test_staking.py @@ -1,7 +1,7 @@ from bittensor import logging from bittensor.core.chain_data.stake_info import StakeInfo from bittensor.utils.balance import Balance -from tests.e2e_tests.utils.chain_interactions import ANY_BALANCE +from tests.e2e_tests.utils.chain_interactions import get_dynamic_balance from tests.helpers.helpers import ApproxBalance logging.enable_info() @@ -40,7 +40,7 @@ def test_single_operation(subtensor, alice_wallet, bob_wallet): alice_wallet, bob_wallet.hotkey.ss58_address, netuid=1, - amount=Balance.from_tao(10_000), + amount=Balance.from_tao(1), wait_for_inclusion=True, wait_for_finalization=True, ) @@ -177,8 +177,10 @@ def test_batch_operations(subtensor, alice_wallet, bob_wallet): ) assert balances == { - alice_wallet.coldkey.ss58_address: ANY_BALANCE, - bob_wallet.coldkey.ss58_address: Balance.from_tao(999_998), + alice_wallet.coldkey.ss58_address: get_dynamic_balance( + balances[alice_wallet.coldkey.ss58_address].rao, 2 + ), + bob_wallet.coldkey.ss58_address: Balance.from_tao(999_998).set_unit(3), } alice_balance = balances[alice_wallet.coldkey.ss58_address] @@ -240,7 +242,9 @@ def test_batch_operations(subtensor, alice_wallet, bob_wallet): ) assert balances == { - alice_wallet.coldkey.ss58_address: ANY_BALANCE, + alice_wallet.coldkey.ss58_address: get_dynamic_balance( + balances[alice_wallet.coldkey.ss58_address].rao, 2 + ), bob_wallet.coldkey.ss58_address: Balance.from_tao(999_998), } assert balances[alice_wallet.coldkey.ss58_address] > alice_balance @@ -516,3 +520,155 @@ def test_safe_swap_stake_scenarios(subtensor, alice_wallet, bob_wallet): assert dest_stake > Balance( 0 ), "Destination stake should be non-zero after successful swap" + + +def test_move_stake(subtensor, alice_wallet, bob_wallet): + """ + Tests: + - Adding stake + - Moving stake from one hotkey-subnet pair to another + """ + + netuid = 1 + subtensor.burned_register( + alice_wallet, + netuid=1, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + assert subtensor.add_stake( + alice_wallet, + alice_wallet.hotkey.ss58_address, + netuid=netuid, + amount=Balance.from_tao(1_000), + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + stakes = subtensor.get_stake_for_coldkey(alice_wallet.coldkey.ss58_address) + + assert stakes == [ + StakeInfo( + hotkey_ss58=alice_wallet.hotkey.ss58_address, + coldkey_ss58=alice_wallet.coldkey.ss58_address, + netuid=netuid, + stake=get_dynamic_balance(stakes[0].stake.rao, netuid), + locked=Balance(0), + emission=get_dynamic_balance(stakes[0].emission.rao, netuid), + drain=0, + is_registered=True, + ), + ] + + subtensor.register_subnet(bob_wallet) + + assert subtensor.move_stake( + alice_wallet, + origin_hotkey=alice_wallet.hotkey.ss58_address, + origin_netuid=1, + destination_hotkey=bob_wallet.hotkey.ss58_address, + destination_netuid=2, + amount=stakes[0].stake, + wait_for_finalization=True, + wait_for_inclusion=True, + ) + + stakes = subtensor.get_stake_for_coldkey(alice_wallet.coldkey.ss58_address) + + netuid = 2 + assert stakes == [ + StakeInfo( + hotkey_ss58=bob_wallet.hotkey.ss58_address, + coldkey_ss58=alice_wallet.coldkey.ss58_address, + netuid=netuid, + stake=get_dynamic_balance(stakes[0].stake.rao, netuid), + locked=Balance(0), + emission=get_dynamic_balance(stakes[0].emission.rao, netuid), + drain=0, + is_registered=True, + ), + ] + + +def test_transfer_stake(subtensor, alice_wallet, bob_wallet, dave_wallet): + """ + Tests: + - Adding stake + - Transferring stake from one coldkey-subnet pair to another + """ + netuid = 1 + + subtensor.burned_register( + alice_wallet, + netuid=netuid, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + assert subtensor.add_stake( + alice_wallet, + alice_wallet.hotkey.ss58_address, + netuid=netuid, + amount=Balance.from_tao(1_000), + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + alice_stakes = subtensor.get_stake_for_coldkey(alice_wallet.coldkey.ss58_address) + + assert alice_stakes == [ + StakeInfo( + hotkey_ss58=alice_wallet.hotkey.ss58_address, + coldkey_ss58=alice_wallet.coldkey.ss58_address, + netuid=netuid, + stake=get_dynamic_balance(alice_stakes[0].stake.rao, netuid), + locked=Balance(0), + emission=get_dynamic_balance(alice_stakes[0].emission.rao, netuid), + drain=0, + is_registered=True, + ), + ] + + bob_stakes = subtensor.get_stake_for_coldkey(bob_wallet.coldkey.ss58_address) + + assert bob_stakes == [] + + subtensor.register_subnet(dave_wallet) + subtensor.burned_register( + bob_wallet, + netuid=2, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + assert subtensor.transfer_stake( + alice_wallet, + destination_coldkey_ss58=bob_wallet.coldkey.ss58_address, + hotkey_ss58=alice_wallet.hotkey.ss58_address, + origin_netuid=1, + destination_netuid=2, + amount=alice_stakes[0].stake, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + alice_stakes = subtensor.get_stake_for_coldkey(alice_wallet.coldkey.ss58_address) + + assert alice_stakes == [] + + bob_stakes = subtensor.get_stake_for_coldkey(bob_wallet.coldkey.ss58_address) + + netuid = 2 + assert bob_stakes == [ + StakeInfo( + hotkey_ss58=alice_wallet.hotkey.ss58_address, + coldkey_ss58=bob_wallet.coldkey.ss58_address, + netuid=2, + stake=get_dynamic_balance(bob_stakes[0].stake.rao, netuid), + locked=Balance(0), + emission=get_dynamic_balance(bob_stakes[0].emission.rao, netuid), + drain=0, + is_registered=False, + ), + ] diff --git a/tests/e2e_tests/utils/chain_interactions.py b/tests/e2e_tests/utils/chain_interactions.py index 2f59dfd1f7..d571f7ff80 100644 --- a/tests/e2e_tests/utils/chain_interactions.py +++ b/tests/e2e_tests/utils/chain_interactions.py @@ -4,9 +4,10 @@ """ import asyncio -import unittest.mock +import contextlib from typing import Union, Optional, TYPE_CHECKING +from bittensor.utils.balance import Balance from bittensor.utils.btlogging import logging # for typing purposes @@ -16,10 +17,9 @@ from async_substrate_interface import SubstrateInterface, ExtrinsicReceipt -ANY_BALANCE = unittest.mock.Mock( - rao=unittest.mock.ANY, - unit=unittest.mock.ANY, -) +def get_dynamic_balance(rao: int, netuid: int = 0): + """Returns a Balance object with the given rao and netuid for testing purposes with synamic values.""" + return Balance(rao).set_unit(netuid) def sudo_set_hyperparameter_bool( @@ -74,7 +74,7 @@ def sudo_set_hyperparameter_values( return response.is_success -async def wait_epoch(subtensor: "Subtensor", netuid: int = 1, times: int = 1): +async def wait_epoch(subtensor: "Subtensor", netuid: int = 1, **kwargs): """ Waits for the next epoch to start on a specific subnet. @@ -90,7 +90,7 @@ async def wait_epoch(subtensor: "Subtensor", netuid: int = 1, times: int = 1): raise Exception("could not determine tempo") tempo = q_tempo[0].value logging.info(f"tempo = {tempo}") - await wait_interval(tempo * times, subtensor, netuid) + await wait_interval(tempo, subtensor, netuid, **kwargs) def next_tempo(current_block: int, tempo: int, netuid: int) -> int: @@ -105,10 +105,7 @@ def next_tempo(current_block: int, tempo: int, netuid: int) -> int: 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_ + return (((current_block + netuid) // tempo) + 1) * tempo + 1 async def wait_interval( @@ -117,6 +114,7 @@ async def wait_interval( netuid: int = 1, reporting_interval: int = 1, sleep: float = 0.25, + times: int = 1, ): """ Waits until the next tempo interval starts for a specific subnet. @@ -126,7 +124,11 @@ async def wait_interval( the current block number until the next tempo interval starts. """ current_block = subtensor.get_current_block() - next_tempo_block_start = next_tempo(current_block, tempo, netuid) + next_tempo_block_start = current_block + + for _ in range(times): + next_tempo_block_start = next_tempo(next_tempo_block_start, tempo, netuid) + last_reported = None while current_block < next_tempo_block_start: @@ -144,12 +146,37 @@ async def wait_interval( ) +@contextlib.asynccontextmanager +async def use_and_wait_for_next_nonce( + subtensor: "Subtensor", + wallet: "Wallet", + sleep: float = 0.25, + timeout: float = 15.0, +): + """ + ContextManager that makes sure the Nonce has been consumed after sending Extrinsic. + """ + + nonce = subtensor.substrate.get_account_next_index(wallet.hotkey.ss58_address) + + yield + + async def wait_for_new_nonce(): + while nonce == subtensor.substrate.get_account_next_index( + wallet.hotkey.ss58_address + ): + await asyncio.sleep(sleep) + + await asyncio.wait_for(wait_for_new_nonce(), timeout) + + # Helper to execute sudo wrapped calls on the chain def sudo_set_admin_utils( substrate: "SubstrateInterface", wallet: "Wallet", call_function: str, call_params: dict, + call_module: str = "AdminUtils", ) -> tuple[bool, Optional[dict]]: """ Wraps the call in sudo to set hyperparameter values using AdminUtils. @@ -159,12 +186,13 @@ def sudo_set_admin_utils( 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. + call_module (str): The AdminUtils module to call. Defaults to "AdminUtils". Returns: tuple[bool, Optional[dict]]: (success status, error details). """ inner_call = substrate.compose_call( - call_module="AdminUtils", + call_module=call_module, call_function=call_function, call_params=call_params, ) @@ -192,7 +220,7 @@ async def root_set_subtensor_hyperparameter_values( call_function: str, call_params: dict, return_error_message: bool = False, -) -> tuple[bool, str]: +) -> tuple[bool, Optional[dict]]: """ Sets liquid alpha values using AdminUtils. Mimics setting hyperparams """ @@ -209,27 +237,7 @@ async def root_set_subtensor_hyperparameter_values( wait_for_finalization=True, ) - if return_error_message: - return response.is_success, response.error_message - - return response.is_success, "" - - -def set_children(subtensor, wallet, netuid, children): - return subtensor.sign_and_send_extrinsic( - subtensor.substrate.compose_call( - call_module="SubtensorModule", - call_function="set_children", - call_params={ - "children": children, - "hotkey": wallet.hotkey.ss58_address, - "netuid": netuid, - }, - ), - wallet, - wait_for_inclusion=True, - wait_for_finalization=True, - ) + return response.is_success, response.error_message def set_identity( @@ -261,3 +269,45 @@ def set_identity( wait_for_inclusion=True, wait_for_finalization=True, ) + + +def propose(subtensor, wallet, proposal, duration): + return subtensor.sign_and_send_extrinsic( + subtensor.substrate.compose_call( + call_module="Triumvirate", + call_function="propose", + call_params={ + "proposal": proposal, + "length_bound": len(proposal.data), + "duration": duration, + }, + ), + wallet, + wait_for_finalization=True, + wait_for_inclusion=True, + ) + + +def vote( + subtensor, + wallet, + hotkey, + proposal, + index, + approve, +): + return subtensor.sign_and_send_extrinsic( + subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="vote", + call_params={ + "approve": approve, + "hotkey": hotkey, + "index": index, + "proposal": proposal, + }, + ), + wallet, + wait_for_inclusion=True, + wait_for_finalization=True, + ) diff --git a/tests/e2e_tests/utils/e2e_test_utils.py b/tests/e2e_tests/utils/e2e_test_utils.py index 00e2594b8b..e10cbfa6d8 100644 --- a/tests/e2e_tests/utils/e2e_test_utils.py +++ b/tests/e2e_tests/utils/e2e_test_utils.py @@ -116,8 +116,7 @@ async def __aenter__(self): self.__reader_task = asyncio.create_task(self._reader()) - async with asyncio.timeout(30): - await self.started.wait() + await asyncio.wait_for(self.started.wait(), 30) return self @@ -166,8 +165,7 @@ async def __aenter__(self): self.__reader_task = asyncio.create_task(self._reader()) - async with asyncio.timeout(30): - await self.started.wait() + await asyncio.wait_for(self.started.wait(), 30) return self diff --git a/tests/unit_tests/extrinsics/asyncex/test_commit_reveal.py b/tests/unit_tests/extrinsics/asyncex/test_commit_reveal.py index 4dec244651..7802ccf9bb 100644 --- a/tests/unit_tests/extrinsics/asyncex/test_commit_reveal.py +++ b/tests/unit_tests/extrinsics/asyncex/test_commit_reveal.py @@ -213,6 +213,7 @@ async def test_commit_reveal_v3_extrinsic_success_with_torch( tempo=mock_hyperparams.return_value.tempo, netuid=fake_netuid, current_block=mock_block.return_value["header"]["number"], + block_time=12.0, ) mock_do_commit_reveal_v3.assert_awaited_once_with( subtensor=subtensor, diff --git a/tests/unit_tests/extrinsics/test_commit_reveal.py b/tests/unit_tests/extrinsics/test_commit_reveal.py index 406bd0a824..37b131e391 100644 --- a/tests/unit_tests/extrinsics/test_commit_reveal.py +++ b/tests/unit_tests/extrinsics/test_commit_reveal.py @@ -199,6 +199,7 @@ def test_commit_reveal_v3_extrinsic_success_with_torch( tempo=mock_hyperparams.return_value.tempo, netuid=fake_netuid, current_block=mock_block.return_value, + block_time=12.0, ) mock_do_commit_reveal_v3.assert_called_once_with( subtensor=subtensor, diff --git a/tests/unit_tests/test_async_subtensor.py b/tests/unit_tests/test_async_subtensor.py index 3ddbf14f99..3acab650e9 100644 --- a/tests/unit_tests/test_async_subtensor.py +++ b/tests/unit_tests/test_async_subtensor.py @@ -12,6 +12,7 @@ from bittensor.core.chain_data.neuron_info import NeuronInfo from bittensor.core.chain_data.stake_info import StakeInfo from bittensor.core.chain_data import proposal_vote_data +from bittensor.utils import U64_MAX from bittensor.utils.balance import Balance from tests.helpers.helpers import assert_submit_signed_extrinsic @@ -47,17 +48,17 @@ def test_decode_ss58_tuples_in_proposal_vote_data(mocker): } # Call - async_subtensor.ProposalVoteData(fake_proposal_dict) + async_subtensor.ProposalVoteData.from_dict(fake_proposal_dict) # Asserts assert mocked_decode_account_id.call_count == len(fake_proposal_dict["ayes"]) + len( fake_proposal_dict["nays"] ) assert mocked_decode_account_id.mock_calls == [ - mocker.call("0"), - mocker.call("1"), - mocker.call("2"), - mocker.call("3"), + mocker.call("0 line"), + mocker.call("1 line"), + mocker.call("2 line"), + mocker.call("3 line"), ] @@ -1953,6 +1954,40 @@ async def test_get_children_substrate_request_exception(subtensor, mocker): assert result == (False, [], "Formatted error message") +@pytest.mark.asyncio +async def test_get_children_pending(mock_substrate, subtensor): + mock_substrate.query.return_value.value = [ + [ + ( + U64_MAX, + (tuple(bytearray(32)),), + ), + ], + 123, + ] + + children, cooldown = await subtensor.get_children_pending( + "hotkey_ss58", + netuid=1, + ) + + assert children == [ + ( + 1.0, + "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", + ), + ] + assert cooldown == 123 + + mock_substrate.query.assert_called_once_with( + module="SubtensorModule", + storage_function="PendingChildKeys", + params=[1, "hotkey_ss58"], + block_hash=None, + reuse_block_hash=False, + ) + + @pytest.mark.asyncio async def test_get_subnet_hyperparameters_success(subtensor, mocker): """Tests get_subnet_hyperparameters with successful hyperparameter retrieval.""" @@ -2054,7 +2089,9 @@ async def test_get_vote_data_success(subtensor, mocker): mocked_proposal_vote_data = mocker.Mock() mocker.patch.object( - async_subtensor, "ProposalVoteData", return_value=mocked_proposal_vote_data + async_subtensor.ProposalVoteData, + "from_dict", + return_value=mocked_proposal_vote_data, ) # Call @@ -2491,6 +2528,44 @@ async def test_register_success(subtensor, fake_wallet, mocker): assert result == mocked_register_extrinsic.return_value +@pytest.mark.asyncio +async def test_set_children(mock_substrate, subtensor, fake_wallet, mocker): + mock_substrate.submit_extrinsic.return_value = mocker.Mock( + is_success=mocker.AsyncMock(return_value=True)(), + ) + + await subtensor.set_children( + fake_wallet, + fake_wallet.hotkey.ss58_address, + netuid=1, + children=[ + ( + 1.0, + "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", + ), + ], + ) + + assert_submit_signed_extrinsic( + mock_substrate, + fake_wallet.coldkey, + call_module="SubtensorModule", + call_function="set_children", + call_params={ + "children": [ + ( + U64_MAX, + "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", + ) + ], + "hotkey": fake_wallet.hotkey.ss58_address, + "netuid": 1, + }, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + @pytest.mark.asyncio async def test_set_delegate_take_equal(subtensor, fake_wallet, mocker): mocker.patch.object(subtensor, "get_delegate_take", return_value=0.18) @@ -2881,3 +2956,59 @@ async def test_get_timestamp(mocker, subtensor): ) actual_result = await subtensor.get_timestamp(block=fake_block) assert expected_result == actual_result + + +@pytest.mark.asyncio +async def test_get_owned_hotkeys_happy_path(subtensor, mocker): + """Tests that the output of get_owned_hotkeys.""" + # Prep + fake_coldkey = "fake_hotkey" + fake_hotkey = "fake_hotkey" + fake_hotkeys = [ + [ + fake_hotkey, + ] + ] + mocked_subtensor = mocker.AsyncMock(return_value=fake_hotkeys) + mocker.patch.object(subtensor.substrate, "query", new=mocked_subtensor) + + mocked_decode_account_id = mocker.Mock() + mocker.patch.object( + async_subtensor, "decode_account_id", new=mocked_decode_account_id + ) + + # Call + result = await subtensor.get_owned_hotkeys(fake_coldkey) + + # Asserts + mocked_subtensor.assert_awaited_once_with( + module="SubtensorModule", + storage_function="OwnedHotkeys", + params=[fake_coldkey], + block_hash=None, + reuse_block_hash=False, + ) + assert result == [mocked_decode_account_id.return_value] + mocked_decode_account_id.assert_called_once_with(fake_hotkey) + + +@pytest.mark.asyncio +async def test_get_owned_hotkeys_return_empty(subtensor, mocker): + """Tests that the output of get_owned_hotkeys is empty.""" + # Prep + fake_coldkey = "fake_hotkey" + mocked_subtensor = mocker.AsyncMock(return_value=[]) + mocker.patch.object(subtensor.substrate, "query", new=mocked_subtensor) + + # Call + result = await subtensor.get_owned_hotkeys(fake_coldkey) + + # Asserts + mocked_subtensor.assert_awaited_once_with( + module="SubtensorModule", + storage_function="OwnedHotkeys", + params=[fake_coldkey], + block_hash=None, + reuse_block_hash=False, + ) + assert result == [] diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index 3af7f70072..513ce7ab3b 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -3159,6 +3159,7 @@ def test_set_weights_with_commit_reveal_enabled(subtensor, fake_wallet, mocker): version_key=subtensor_module.version_as_int, wait_for_inclusion=fake_wait_for_inclusion, wait_for_finalization=fake_wait_for_finalization, + block_time=12.0, ) assert result == mocked_commit_reveal_v3_extrinsic.return_value @@ -3332,3 +3333,57 @@ def test_stake_fee_methods(mocker, subtensor): ], block=None, ) + + +def test_get_owned_hotkeys_happy_path(subtensor, mocker): + """Tests that the output of get_owned_hotkeys.""" + # Prep + fake_coldkey = "fake_hotkey" + fake_hotkey = "fake_hotkey" + fake_hotkeys = [ + [ + fake_hotkey, + ] + ] + mocked_subtensor = mocker.Mock(return_value=fake_hotkeys) + mocker.patch.object(subtensor.substrate, "query", new=mocked_subtensor) + + mocked_decode_account_id = mocker.Mock() + mocker.patch.object( + subtensor_module, "decode_account_id", new=mocked_decode_account_id + ) + + # Call + result = subtensor.get_owned_hotkeys(fake_coldkey) + + # Asserts + mocked_subtensor.assert_called_once_with( + module="SubtensorModule", + storage_function="OwnedHotkeys", + params=[fake_coldkey], + block_hash=None, + reuse_block_hash=False, + ) + assert result == [mocked_decode_account_id.return_value] + mocked_decode_account_id.assert_called_once_with(fake_hotkey) + + +def test_get_owned_hotkeys_return_empty(subtensor, mocker): + """Tests that the output of get_owned_hotkeys is empty.""" + # Prep + fake_coldkey = "fake_hotkey" + mocked_subtensor = mocker.Mock(return_value=[]) + mocker.patch.object(subtensor.substrate, "query", new=mocked_subtensor) + + # Call + result = subtensor.get_owned_hotkeys(fake_coldkey) + + # Asserts + mocked_subtensor.assert_called_once_with( + module="SubtensorModule", + storage_function="OwnedHotkeys", + params=[fake_coldkey], + block_hash=None, + reuse_block_hash=False, + ) + assert result == [] diff --git a/tests/unit_tests/test_subtensor_extended.py b/tests/unit_tests/test_subtensor_extended.py index b0cd2afbf3..ec67747f97 100644 --- a/tests/unit_tests/test_subtensor_extended.py +++ b/tests/unit_tests/test_subtensor_extended.py @@ -12,6 +12,7 @@ from bittensor.core.chain_data.neuron_info_lite import NeuronInfoLite from bittensor.core.chain_data.prometheus_info import PrometheusInfo from bittensor.core.chain_data.stake_info import StakeInfo +from bittensor.utils import U16_MAX, U64_MAX from bittensor.utils.balance import Balance from tests.helpers.helpers import assert_submit_signed_extrinsic @@ -23,7 +24,7 @@ def mock_delegate_info(): "total_stake": {}, "nominators": [], "owner_ss58": tuple(bytearray(32)), - "take": 2**16 - 1, + "take": U16_MAX, "validator_permits": [], "registrations": [], "return_per_1000": 2, @@ -364,7 +365,7 @@ def test_get_block_hash_none(mock_substrate, subtensor): def test_get_children(mock_substrate, subtensor, fake_wallet): mock_substrate.query.return_value.value = [ ( - 2**64 - 1, + U64_MAX, (tuple(bytearray(32)),), ), ] @@ -391,6 +392,38 @@ def test_get_children(mock_substrate, subtensor, fake_wallet): ) +def test_get_children_pending(mock_substrate, subtensor): + mock_substrate.query.return_value.value = [ + [ + ( + U64_MAX, + (tuple(bytearray(32)),), + ), + ], + 123, + ] + + children, cooldown = subtensor.get_children_pending( + "hotkey_ss58", + netuid=1, + ) + + assert children == [ + ( + 1.0, + "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", + ), + ] + assert cooldown == 123 + + mock_substrate.query.assert_called_once_with( + module="SubtensorModule", + storage_function="PendingChildKeys", + params=[1, "hotkey_ss58"], + block_hash=None, + ) + + def test_get_current_weight_commit_info(mock_substrate, subtensor, fake_wallet, mocker): mock_substrate.query_map.return_value.records = [ ( @@ -878,6 +911,39 @@ def test_neurons_lite(mock_substrate, subtensor, mock_neuron_info): ) +def test_set_children(mock_substrate, subtensor, fake_wallet, mocker): + subtensor.set_children( + fake_wallet, + fake_wallet.hotkey.ss58_address, + netuid=1, + children=[ + ( + 1.0, + "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", + ), + ], + ) + + assert_submit_signed_extrinsic( + mock_substrate, + fake_wallet.coldkey, + call_module="SubtensorModule", + call_function="set_children", + call_params={ + "children": [ + ( + U64_MAX, + "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", + ) + ], + "hotkey": fake_wallet.hotkey.ss58_address, + "netuid": 1, + }, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + def test_set_delegate_take_equal(mock_substrate, subtensor, fake_wallet, mocker): mocker.patch.object(subtensor, "get_delegate_take", return_value=0.18)