From 3bc829ff0622917a0315a113fae3fda9645a5312 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20=C5=BBy=C5=BAniewski?= Date: Fri, 14 Mar 2025 13:15:42 +0100 Subject: [PATCH 01/84] feat: set_children and get_pending_children methods --- bittensor/core/async_subtensor.py | 122 +++++++++++++ bittensor/core/errors.py | 118 ++++++++++--- bittensor/core/subtensor.py | 112 ++++++++++++ bittensor/utils/__init__.py | 12 ++ bittensor/utils/easy_imports.py | 8 + tests/e2e_tests/test_hotkeys.py | 181 ++++++++++++++++++-- tests/e2e_tests/utils/chain_interactions.py | 17 -- tests/unit_tests/test_async_subtensor.py | 73 ++++++++ tests/unit_tests/test_subtensor_extended.py | 70 +++++++- 9 files changed, 653 insertions(+), 60 deletions(-) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index 546be4a9d0..c96923bf12 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -77,10 +77,12 @@ from bittensor.utils import ( Certificate, decode_hex_identity_dict, + float_to_u64, format_error_message, torch, u16_normalized_float, u64_normalized_float, + unlock_key, ) from bittensor.utils.balance import ( Balance, @@ -934,6 +936,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, @@ -3269,6 +3322,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", 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/subtensor.py b/bittensor/core/subtensor.py index 244080ff51..35cf416812 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -80,10 +80,12 @@ from bittensor.utils import ( Certificate, decode_hex_identity_dict, + float_to_u64, format_error_message, torch, u16_normalized_float, u64_normalized_float, + unlock_key, ) from bittensor.utils.balance import ( Balance, @@ -717,6 +719,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. @@ -1572,6 +1615,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", 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/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/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/utils/chain_interactions.py b/tests/e2e_tests/utils/chain_interactions.py index 2f59dfd1f7..287b2a4ae4 100644 --- a/tests/e2e_tests/utils/chain_interactions.py +++ b/tests/e2e_tests/utils/chain_interactions.py @@ -215,23 +215,6 @@ async def root_set_subtensor_hyperparameter_values( 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, - ) - - def set_identity( subtensor, wallet, diff --git a/tests/unit_tests/test_async_subtensor.py b/tests/unit_tests/test_async_subtensor.py index 3ddbf14f99..89c3290f72 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 @@ -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.""" @@ -2491,6 +2526,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) 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) From d6bce543d34b67a08f8996c9ba06341649b741d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20=C5=BBy=C5=BAniewski?= Date: Fri, 14 Mar 2025 15:53:34 +0100 Subject: [PATCH 02/84] fix: wait_epoch didn't take tempo +1 into account when called with `times` arg --- tests/e2e_tests/utils/chain_interactions.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/tests/e2e_tests/utils/chain_interactions.py b/tests/e2e_tests/utils/chain_interactions.py index 2f59dfd1f7..ae2117e113 100644 --- a/tests/e2e_tests/utils/chain_interactions.py +++ b/tests/e2e_tests/utils/chain_interactions.py @@ -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: @@ -117,6 +117,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 +127,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: From 52d0c755801ff548162c8ff323df3973cb485676 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20=C5=BBy=C5=BAniewski?= Date: Fri, 14 Mar 2025 16:12:45 +0100 Subject: [PATCH 03/84] e2e tests for bonds and get_vote_data (with minor refactoring) --- bittensor/core/async_subtensor.py | 5 +- .../core/chain_data/proposal_vote_data.py | 31 ++--- bittensor/core/subtensor.py | 5 +- tests/e2e_tests/test_delegate.py | 106 ++++++++++++++++++ tests/e2e_tests/test_incentive.py | 17 +++ tests/e2e_tests/utils/chain_interactions.py | 42 +++++++ tests/unit_tests/test_async_subtensor.py | 14 ++- 7 files changed, 197 insertions(+), 23 deletions(-) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index 546be4a9d0..3ac6c6aeff 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -1909,10 +1909,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, 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/subtensor.py b/bittensor/core/subtensor.py index 244080ff51..878c8ab501 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -1468,10 +1468,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 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_incentive.py b/tests/e2e_tests/test_incentive.py index 0467a0cd81..7f01085428 100644 --- a/tests/e2e_tests/test_incentive.py +++ b/tests/e2e_tests/test_incentive.py @@ -100,9 +100,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/utils/chain_interactions.py b/tests/e2e_tests/utils/chain_interactions.py index 2f59dfd1f7..0407f21c71 100644 --- a/tests/e2e_tests/utils/chain_interactions.py +++ b/tests/e2e_tests/utils/chain_interactions.py @@ -261,3 +261,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/unit_tests/test_async_subtensor.py b/tests/unit_tests/test_async_subtensor.py index 3ddbf14f99..aad0cace71 100644 --- a/tests/unit_tests/test_async_subtensor.py +++ b/tests/unit_tests/test_async_subtensor.py @@ -47,17 +47,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"), ] @@ -2054,7 +2054,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 From 5ba140123878e447f86cc820c26c3d9cc95f30d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20=C5=BBy=C5=BAniewski?= Date: Fri, 14 Mar 2025 16:22:07 +0100 Subject: [PATCH 04/84] Trigger CI From e4b9cc9f8261ffb9b6581f4dbb4b72b013671314 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20=C5=BBy=C5=BAniewski?= Date: Mon, 17 Mar 2025 12:53:49 +0100 Subject: [PATCH 05/84] e2e tests for move_stake and transfer_stake --- tests/e2e_tests/test_staking.py | 148 ++++++++++++++++++++++++++++++++ 1 file changed, 148 insertions(+) diff --git a/tests/e2e_tests/test_staking.py b/tests/e2e_tests/test_staking.py index 11b7b45054..60177eb7b1 100644 --- a/tests/e2e_tests/test_staking.py +++ b/tests/e2e_tests/test_staking.py @@ -516,3 +516,151 @@ 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 + """ + + 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=1, + 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=1, + stake=ANY_BALANCE, + locked=Balance(0), + emission=ANY_BALANCE, + 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) + + assert stakes == [ + StakeInfo( + hotkey_ss58=bob_wallet.hotkey.ss58_address, + coldkey_ss58=alice_wallet.coldkey.ss58_address, + netuid=2, + stake=ANY_BALANCE, + locked=Balance(0), + emission=ANY_BALANCE, + drain=0, + is_registered=True, + ), + ] + + +def test_transfer_stake(subtensor, alice_wallet, bob_wallet, dave_wallet): + """ + Tests: + - Adding stake + - Transfering stake from one coldkey-subnet pair to another + """ + + 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=1, + 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=1, + stake=ANY_BALANCE, + locked=Balance(0), + emission=ANY_BALANCE, + 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) + + assert bob_stakes == [ + StakeInfo( + hotkey_ss58=alice_wallet.hotkey.ss58_address, + coldkey_ss58=bob_wallet.coldkey.ss58_address, + netuid=2, + stake=ANY_BALANCE, + locked=Balance(0), + emission=ANY_BALANCE, + drain=0, + is_registered=False, + ), + ] From 17e38aa76b3e226993de54c3b808fa6d831bd215 Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 18 Mar 2025 00:30:33 -0700 Subject: [PATCH 06/84] update docker image name --- tests/e2e_tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e_tests/conftest.py b/tests/e2e_tests/conftest.py index 51a865b1b8..f552a57a4e 100644 --- a/tests/e2e_tests/conftest.py +++ b/tests/e2e_tests/conftest.py @@ -162,7 +162,7 @@ def try_start_docker(): return False container_name = f"test_local_chain_{str(time.time()).replace(".", "_")}" - image_name = "ghcr.io/opentensor/subtensor-localnet:latest" + image_name = "ghcr.io/opentensor/subtensor-localnet:devnet-ready" # Command to start container cmds = [ From a60fd83c4842d0e918e687eb307a83bb77a37f46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20=C5=BBy=C5=BAniewski?= Date: Tue, 18 Mar 2025 15:39:28 +0100 Subject: [PATCH 07/84] fix: next_tempo should always return next upcoming epoch --- tests/e2e_tests/utils/chain_interactions.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/e2e_tests/utils/chain_interactions.py b/tests/e2e_tests/utils/chain_interactions.py index ae2117e113..a42b9ec6f5 100644 --- a/tests/e2e_tests/utils/chain_interactions.py +++ b/tests/e2e_tests/utils/chain_interactions.py @@ -105,6 +105,7 @@ def next_tempo(current_block: int, tempo: int, netuid: int) -> int: Returns: int: The next tempo block number. """ + current_block += 1 interval = tempo + 1 last_epoch = current_block - 1 - (current_block + netuid + 1) % interval next_tempo_ = last_epoch + interval From 2b117e6d5813ec94116092ebd75d0c8c64688f1e Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 18 Mar 2025 13:36:56 -0700 Subject: [PATCH 08/84] python-version --- .github/workflows/e2e-subtensor-tests.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/e2e-subtensor-tests.yaml b/.github/workflows/e2e-subtensor-tests.yaml index 97df3354be..a3bcc6bc81 100644 --- a/.github/workflows/e2e-subtensor-tests.yaml +++ b/.github/workflows/e2e-subtensor-tests.yaml @@ -74,6 +74,7 @@ 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 From 7050245aa9d28ef625d327ffe84cc5536cf4fdf5 Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 18 Mar 2025 13:40:15 -0700 Subject: [PATCH 09/84] add name for matrix jobs --- .github/workflows/e2e-subtensor-tests.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/e2e-subtensor-tests.yaml b/.github/workflows/e2e-subtensor-tests.yaml index a3bcc6bc81..c656186a1b 100644 --- a/.github/workflows/e2e-subtensor-tests.yaml +++ b/.github/workflows/e2e-subtensor-tests.yaml @@ -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 From c6fc50822ef1a3f05f78af60711b3fa38932d525 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 19 Mar 2025 15:00:35 -0700 Subject: [PATCH 10/84] add pypi version checker --- bittensor/utils/version.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) 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 From 714f4fb0b5cf4d5aa6024fe82aca7e7bd0b84fa9 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 19 Mar 2025 15:01:10 -0700 Subject: [PATCH 11/84] add call --- bittensor/__main__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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() From 38f2156af3db6b21efe878eaa768a19f42a4e431 Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 20 Mar 2025 19:48:04 -0700 Subject: [PATCH 12/84] add get_dynamic_balance for tests purposes --- tests/e2e_tests/utils/chain_interactions.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/tests/e2e_tests/utils/chain_interactions.py b/tests/e2e_tests/utils/chain_interactions.py index 58529c4a3a..6a8661778b 100644 --- a/tests/e2e_tests/utils/chain_interactions.py +++ b/tests/e2e_tests/utils/chain_interactions.py @@ -4,9 +4,9 @@ """ import asyncio -import unittest.mock from typing import Union, Optional, TYPE_CHECKING +from bittensor.utils.balance import Balance from bittensor.utils.btlogging import logging # for typing purposes @@ -16,10 +16,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( @@ -198,7 +197,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 """ @@ -218,7 +217,7 @@ async def root_set_subtensor_hyperparameter_values( if return_error_message: return response.is_success, response.error_message - return response.is_success, "" + return response.is_success, None def set_identity( From 220537dd0bcb8b65d3f4e54951e602de833bf227 Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 20 Mar 2025 19:48:26 -0700 Subject: [PATCH 13/84] Improve Balance class --- bittensor/utils/balance.py | 99 ++++++++++++++++++++++++++------------ 1 file changed, 67 insertions(+), 32 deletions(-) diff --git a/bittensor/utils/balance.py b/bittensor/utils/balance.py index 33d56be13c..c8d8ff583b 100644 --- a/bittensor/utils/balance.py +++ b/bittensor/utils/balance.py @@ -6,6 +6,18 @@ from bittensor.core import settings +def _check_currencies(self, other): + """Checks that Balance objects have the same netuids to perform arithmetic operations.""" + if self.netuid != other.netuid: + warnings.simplefilter("default", DeprecationWarning) + warnings.warn( + "Balance objects must have the same netuid (Alpha currency) to perform arithmetic operations. " + f"First balance is `{self}`. Second balance is `{other}`. ", + category=DeprecationWarning, + stacklevel=2, + ) + + class Balance: """ Represents the bittensor balance of the wallet, stored as rao (int). @@ -23,6 +35,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 +91,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 +106,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 +118,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 +131,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 +244,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 +267,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 +282,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 +296,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 +308,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 +812,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( From 56f3f644e21c317d8c9489a1022bd57bc780e9e7 Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 20 Mar 2025 19:48:35 -0700 Subject: [PATCH 14/84] fix test --- tests/e2e_tests/test_staking.py | 46 +++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/tests/e2e_tests/test_staking.py b/tests/e2e_tests/test_staking.py index 60177eb7b1..79f9d68e52 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() @@ -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 @@ -525,6 +529,7 @@ def test_move_stake(subtensor, alice_wallet, bob_wallet): - Moving stake from one hotkey-subnet pair to another """ + netuid = 1 subtensor.burned_register( alice_wallet, netuid=1, @@ -535,7 +540,7 @@ def test_move_stake(subtensor, alice_wallet, bob_wallet): assert subtensor.add_stake( alice_wallet, alice_wallet.hotkey.ss58_address, - netuid=1, + netuid=netuid, amount=Balance.from_tao(1_000), wait_for_inclusion=True, wait_for_finalization=True, @@ -547,10 +552,10 @@ def test_move_stake(subtensor, alice_wallet, bob_wallet): StakeInfo( hotkey_ss58=alice_wallet.hotkey.ss58_address, coldkey_ss58=alice_wallet.coldkey.ss58_address, - netuid=1, - stake=ANY_BALANCE, + netuid=netuid, + stake=get_dynamic_balance(stakes[0].stake.rao, netuid), locked=Balance(0), - emission=ANY_BALANCE, + emission=get_dynamic_balance(stakes[0].emission.rao, netuid), drain=0, is_registered=True, ), @@ -571,14 +576,15 @@ def test_move_stake(subtensor, alice_wallet, bob_wallet): 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=2, - stake=ANY_BALANCE, + netuid=netuid, + stake=get_dynamic_balance(stakes[0].stake.rao, netuid), locked=Balance(0), - emission=ANY_BALANCE, + emission=get_dynamic_balance(stakes[0].emission.rao, netuid), drain=0, is_registered=True, ), @@ -589,12 +595,13 @@ def test_transfer_stake(subtensor, alice_wallet, bob_wallet, dave_wallet): """ Tests: - Adding stake - - Transfering stake from one coldkey-subnet pair to another + - Transferring stake from one coldkey-subnet pair to another """ + netuid = 1 subtensor.burned_register( alice_wallet, - netuid=1, + netuid=netuid, wait_for_inclusion=True, wait_for_finalization=True, ) @@ -602,7 +609,7 @@ def test_transfer_stake(subtensor, alice_wallet, bob_wallet, dave_wallet): assert subtensor.add_stake( alice_wallet, alice_wallet.hotkey.ss58_address, - netuid=1, + netuid=netuid, amount=Balance.from_tao(1_000), wait_for_inclusion=True, wait_for_finalization=True, @@ -614,10 +621,10 @@ def test_transfer_stake(subtensor, alice_wallet, bob_wallet, dave_wallet): StakeInfo( hotkey_ss58=alice_wallet.hotkey.ss58_address, coldkey_ss58=alice_wallet.coldkey.ss58_address, - netuid=1, - stake=ANY_BALANCE, + netuid=netuid, + stake=get_dynamic_balance(alice_stakes[0].stake.rao, netuid), locked=Balance(0), - emission=ANY_BALANCE, + emission=get_dynamic_balance(alice_stakes[0].emission.rao, netuid), drain=0, is_registered=True, ), @@ -652,14 +659,15 @@ def test_transfer_stake(subtensor, alice_wallet, bob_wallet, dave_wallet): 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=ANY_BALANCE, + stake=get_dynamic_balance(bob_stakes[0].stake.rao, netuid), locked=Balance(0), - emission=ANY_BALANCE, + emission=get_dynamic_balance(bob_stakes[0].emission.rao, netuid), drain=0, is_registered=False, ), From 16f7ecbbb36dd9d57c049a5f0166051cb6f69c11 Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 20 Mar 2025 20:30:35 -0700 Subject: [PATCH 15/84] add pull command --- tests/e2e_tests/conftest.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/e2e_tests/conftest.py b/tests/e2e_tests/conftest.py index f552a57a4e..d1c6813147 100644 --- a/tests/e2e_tests/conftest.py +++ b/tests/e2e_tests/conftest.py @@ -19,6 +19,8 @@ setup_wallet, ) +LOCALNET_IMAGE_NAME = "ghcr.io/opentensor/subtensor-localnet:devnet-ready" + def wait_for_node_start(process, timestamp=None): """Waits for node to start in the docker.""" @@ -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 @@ -162,7 +165,6 @@ def try_start_docker(): return False container_name = f"test_local_chain_{str(time.time()).replace(".", "_")}" - image_name = "ghcr.io/opentensor/subtensor-localnet:devnet-ready" # Command to start container cmds = [ @@ -175,7 +177,7 @@ def try_start_docker(): "9944:9944", "-p", "9945:9945", - image_name, + LOCALNET_IMAGE_NAME, params, ] From c3c82068d6d91be58ab6b0e3adf4cc9d63691863 Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 20 Mar 2025 20:57:12 -0700 Subject: [PATCH 16/84] devnet-ready --- .github/workflows/e2e-subtensor-tests.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/e2e-subtensor-tests.yaml b/.github/workflows/e2e-subtensor-tests.yaml index c656186a1b..266a908b62 100644 --- a/.github/workflows/e2e-subtensor-tests.yaml +++ b/.github/workflows/e2e-subtensor-tests.yaml @@ -52,7 +52,7 @@ jobs: run: docker pull ghcr.io/opentensor/subtensor-localnet:latest - 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 From 0f186c4c80ce058bd2d8949856a65f63400eb07d Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 20 Mar 2025 20:59:13 -0700 Subject: [PATCH 17/84] ghcr.io/opentensor/subtensor-localnet:devnet-ready --- .github/workflows/e2e-subtensor-tests.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/e2e-subtensor-tests.yaml b/.github/workflows/e2e-subtensor-tests.yaml index 266a908b62..13c6bbe2f8 100644 --- a/.github/workflows/e2e-subtensor-tests.yaml +++ b/.github/workflows/e2e-subtensor-tests.yaml @@ -49,7 +49,7 @@ 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:devnet-ready From 90417397802a40debd87901e907ba20c312f0dc8 Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 21 Mar 2025 17:35:09 -0700 Subject: [PATCH 18/84] add get_owned_hotkeys to subtensor and async one + tests --- bittensor/core/async_subtensor.py | 30 +++++++++++++ bittensor/core/subtensor.py | 27 ++++++++++++ tests/unit_tests/test_async_subtensor.py | 54 ++++++++++++++++++++++++ tests/unit_tests/test_subtensor.py | 52 +++++++++++++++++++++++ 4 files changed, 163 insertions(+) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index d1a54b1dcd..b890f2972e 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -1572,6 +1572,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, diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 567a79b70d..7d5b1f914a 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -1200,6 +1200,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, diff --git a/tests/unit_tests/test_async_subtensor.py b/tests/unit_tests/test_async_subtensor.py index 930fb12718..fc52c83fff 100644 --- a/tests/unit_tests/test_async_subtensor.py +++ b/tests/unit_tests/test_async_subtensor.py @@ -2956,3 +2956,57 @@ 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) + subtensor.substrate.query = mocked_subtensor + + mocked_decode_account_id = mocker.Mock() + async_subtensor.decode_account_id = 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=[]) + subtensor.substrate.query = 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..36697b5535 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -3332,3 +3332,55 @@ 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) + subtensor.substrate.query = mocked_subtensor + + mocked_decode_account_id = mocker.Mock() + subtensor_module.decode_account_id = 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=[]) + subtensor.substrate.query = 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 == [] From ad4f68098cd3afcee725a628140861c1a403bfff Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 21 Mar 2025 17:51:28 -0700 Subject: [PATCH 19/84] correct patching (doesn't affect to another tests) --- tests/unit_tests/test_async_subtensor.py | 6 +++--- tests/unit_tests/test_subtensor.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/unit_tests/test_async_subtensor.py b/tests/unit_tests/test_async_subtensor.py index fc52c83fff..bbfe50ab63 100644 --- a/tests/unit_tests/test_async_subtensor.py +++ b/tests/unit_tests/test_async_subtensor.py @@ -2970,10 +2970,10 @@ async def test_get_owned_hotkeys_happy_path(subtensor, mocker): ] ] mocked_subtensor = mocker.AsyncMock(return_value=fake_hotkeys) - subtensor.substrate.query = mocked_subtensor + mocker.patch.object(subtensor.substrate, "query", new=mocked_subtensor) mocked_decode_account_id = mocker.Mock() - async_subtensor.decode_account_id = mocked_decode_account_id + mocker.patch.object(async_subtensor, "decode_account_id", new=mocked_decode_account_id) # Call result = await subtensor.get_owned_hotkeys(fake_coldkey) @@ -2996,7 +2996,7 @@ async def test_get_owned_hotkeys_return_empty(subtensor, mocker): # Prep fake_coldkey = "fake_hotkey" mocked_subtensor = mocker.AsyncMock(return_value=[]) - subtensor.substrate.query = mocked_subtensor + mocker.patch.object(subtensor.substrate, "query", new=mocked_subtensor) # Call result = await subtensor.get_owned_hotkeys(fake_coldkey) diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index 36697b5535..b3b0049210 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -3345,10 +3345,10 @@ def test_get_owned_hotkeys_happy_path(subtensor, mocker): ] ] mocked_subtensor = mocker.Mock(return_value=fake_hotkeys) - subtensor.substrate.query = mocked_subtensor + mocker.patch.object(subtensor.substrate, "query", new=mocked_subtensor) mocked_decode_account_id = mocker.Mock() - subtensor_module.decode_account_id = mocked_decode_account_id + mocker.patch.object(subtensor_module, "decode_account_id", new=mocked_decode_account_id) # Call result = subtensor.get_owned_hotkeys(fake_coldkey) @@ -3370,7 +3370,7 @@ def test_get_owned_hotkeys_return_empty(subtensor, mocker): # Prep fake_coldkey = "fake_hotkey" mocked_subtensor = mocker.Mock(return_value=[]) - subtensor.substrate.query = mocked_subtensor + mocker.patch.object(subtensor.substrate, "query", new=mocked_subtensor) # Call result = subtensor.get_owned_hotkeys(fake_coldkey) From 59e88f1f63a3de87276051db2366413be7e53354 Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 21 Mar 2025 17:52:51 -0700 Subject: [PATCH 20/84] ruff --- tests/unit_tests/test_async_subtensor.py | 4 +++- tests/unit_tests/test_subtensor.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/unit_tests/test_async_subtensor.py b/tests/unit_tests/test_async_subtensor.py index bbfe50ab63..3acab650e9 100644 --- a/tests/unit_tests/test_async_subtensor.py +++ b/tests/unit_tests/test_async_subtensor.py @@ -2973,7 +2973,9 @@ async def test_get_owned_hotkeys_happy_path(subtensor, mocker): 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) + mocker.patch.object( + async_subtensor, "decode_account_id", new=mocked_decode_account_id + ) # Call result = await subtensor.get_owned_hotkeys(fake_coldkey) diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index b3b0049210..3215d49a0e 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -3348,7 +3348,9 @@ def test_get_owned_hotkeys_happy_path(subtensor, mocker): 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) + mocker.patch.object( + subtensor_module, "decode_account_id", new=mocked_decode_account_id + ) # Call result = subtensor.get_owned_hotkeys(fake_coldkey) From fcdb92df24040f69868580dbb9980cece7c58d3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20=C5=BBy=C5=BAniewski?= Date: Mon, 24 Mar 2025 12:05:32 +0100 Subject: [PATCH 21/84] CI/CD: Set up Python version --- .github/workflows/e2e-subtensor-tests.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/e2e-subtensor-tests.yaml b/.github/workflows/e2e-subtensor-tests.yaml index c656186a1b..738e65976e 100644 --- a/.github/workflows/e2e-subtensor-tests.yaml +++ b/.github/workflows/e2e-subtensor-tests.yaml @@ -80,6 +80,11 @@ jobs: - 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 From 05af3745b1fd43d9aa430ec53ed8940ae28fbfd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20=C5=BBy=C5=BAniewski?= Date: Mon, 24 Mar 2025 12:12:36 +0100 Subject: [PATCH 22/84] fix: f-string quotes (compatibiliy with pre 3.12) --- tests/e2e_tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e_tests/conftest.py b/tests/e2e_tests/conftest.py index f552a57a4e..4550ce299b 100644 --- a/tests/e2e_tests/conftest.py +++ b/tests/e2e_tests/conftest.py @@ -161,7 +161,7 @@ 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(".", "_")}" + container_name = f"test_local_chain_{str(time.time()).replace('.', '_')}" image_name = "ghcr.io/opentensor/subtensor-localnet:devnet-ready" # Command to start container From a59988fea4a79657dbbf0d767567597e63e0abfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20=C5=BBy=C5=BAniewski?= Date: Mon, 24 Mar 2025 12:21:39 +0100 Subject: [PATCH 23/84] fix: use asyncio.wait_for for backward compatibility --- tests/e2e_tests/test_incentive.py | 3 +-- tests/e2e_tests/utils/e2e_test_utils.py | 6 ++---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/tests/e2e_tests/test_incentive.py b/tests/e2e_tests/test_incentive.py index 60d7702c1d..bb5e3749ec 100644 --- a/tests/e2e_tests/test_incentive.py +++ b/tests/e2e_tests/test_incentive.py @@ -84,8 +84,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) 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 From 12e8c1c44ec650072d74ac317d53a3a03f898ce4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20=C5=BBy=C5=BAniewski?= Date: Mon, 24 Mar 2025 12:33:48 +0100 Subject: [PATCH 24/84] fix: wait for new nonce --- tests/e2e_tests/test_commit_weights.py | 75 +++++++++++---------- tests/e2e_tests/test_set_weights.py | 22 +++--- tests/e2e_tests/utils/chain_interactions.py | 25 +++++++ 3 files changed, 76 insertions(+), 46 deletions(-) diff --git a/tests/e2e_tests/test_commit_weights.py b/tests/e2e_tests/test_commit_weights.py index 4d966c8c37..a27238797a 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_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_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_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_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_set_weights.py b/tests/e2e_tests/test_set_weights.py index 8a279e3ccf..9be3bbe144 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_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_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/utils/chain_interactions.py b/tests/e2e_tests/utils/chain_interactions.py index 58529c4a3a..bd9c242088 100644 --- a/tests/e2e_tests/utils/chain_interactions.py +++ b/tests/e2e_tests/utils/chain_interactions.py @@ -4,6 +4,7 @@ """ import asyncio +import contextlib import unittest.mock from typing import Union, Optional, TYPE_CHECKING @@ -150,6 +151,30 @@ async def wait_interval( ) +@contextlib.asynccontextmanager +async def use_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", From f14b9534c0456d96c26821596a0f429315611529 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20=C5=BBy=C5=BAniewski?= Date: Mon, 24 Mar 2025 18:22:25 +0100 Subject: [PATCH 25/84] rename use_nonce function --- tests/e2e_tests/test_commit_weights.py | 8 ++++---- tests/e2e_tests/test_set_weights.py | 4 ++-- tests/e2e_tests/utils/chain_interactions.py | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/e2e_tests/test_commit_weights.py b/tests/e2e_tests/test_commit_weights.py index a27238797a..c4701ad71e 100644 --- a/tests/e2e_tests/test_commit_weights.py +++ b/tests/e2e_tests/test_commit_weights.py @@ -7,7 +7,7 @@ from tests.e2e_tests.utils.chain_interactions import ( sudo_set_admin_utils, sudo_set_hyperparameter_bool, - use_nonce, + use_and_wait_for_next_nonce, wait_epoch, ) @@ -228,7 +228,7 @@ 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 - async with use_nonce(subtensor, alice_wallet): + async with use_and_wait_for_next_nonce(subtensor, alice_wallet): success, message = subtensor.commit_weights( alice_wallet, netuid, @@ -241,7 +241,7 @@ async def test_commit_weights_uses_next_nonce(local_chain, subtensor, alice_wall assert success is True - async with use_nonce(subtensor, alice_wallet): + async with use_and_wait_for_next_nonce(subtensor, alice_wallet): success, message = subtensor.commit_weights( alice_wallet, netuid, @@ -254,7 +254,7 @@ async def test_commit_weights_uses_next_nonce(local_chain, subtensor, alice_wall assert success is True - async with use_nonce(subtensor, alice_wallet): + async with use_and_wait_for_next_nonce(subtensor, alice_wallet): success, message = subtensor.commit_weights( alice_wallet, netuid, diff --git a/tests/e2e_tests/test_set_weights.py b/tests/e2e_tests/test_set_weights.py index 9be3bbe144..8211e62aa9 100644 --- a/tests/e2e_tests/test_set_weights.py +++ b/tests/e2e_tests/test_set_weights.py @@ -6,7 +6,7 @@ from tests.e2e_tests.utils.chain_interactions import ( sudo_set_hyperparameter_bool, sudo_set_admin_utils, - use_nonce, + use_and_wait_for_next_nonce, wait_epoch, ) @@ -109,7 +109,7 @@ async def test_set_weights_uses_next_nonce(local_chain, subtensor, alice_wallet) # Set weights for each subnet for netuid in netuids: - async with use_nonce(subtensor, alice_wallet): + async with use_and_wait_for_next_nonce(subtensor, alice_wallet): success, message = subtensor.set_weights( alice_wallet, netuid, diff --git a/tests/e2e_tests/utils/chain_interactions.py b/tests/e2e_tests/utils/chain_interactions.py index bd9c242088..15998f93e0 100644 --- a/tests/e2e_tests/utils/chain_interactions.py +++ b/tests/e2e_tests/utils/chain_interactions.py @@ -152,7 +152,7 @@ async def wait_interval( @contextlib.asynccontextmanager -async def use_nonce( +async def use_and_wait_for_next_nonce( subtensor: "Subtensor", wallet: "Wallet", sleep: float = 0.25, From 427f53073a4356b8d7310cf43c7d8ff5b89aa527 Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 25 Mar 2025 16:45:44 -0700 Subject: [PATCH 26/84] use arguments instead of whole request object --- tests/e2e_tests/conftest.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/e2e_tests/conftest.py b/tests/e2e_tests/conftest.py index c8265c275a..e0afea8a88 100644 --- a/tests/e2e_tests/conftest.py +++ b/tests/e2e_tests/conftest.py @@ -77,7 +77,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): @@ -95,7 +95,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, From 4ebd90dbc9b14653ab235de3964aa9bc214939e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20=C5=BBy=C5=BAniewski?= Date: Wed, 26 Mar 2025 15:16:59 +0100 Subject: [PATCH 27/84] fix: pass e2e with devnet-ready --- tests/e2e_tests/test_commitment.py | 41 +++++++++++++++++---- tests/e2e_tests/test_incentive.py | 19 +++++++++- tests/e2e_tests/test_metagraph.py | 6 +-- tests/e2e_tests/test_staking.py | 2 +- tests/e2e_tests/utils/chain_interactions.py | 9 ++--- 5 files changed, 59 insertions(+), 18 deletions(-) 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_incentive.py b/tests/e2e_tests/test_incentive.py index bb5e3749ec..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( 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_staking.py b/tests/e2e_tests/test_staking.py index 60177eb7b1..e72936d821 100644 --- a/tests/e2e_tests/test_staking.py +++ b/tests/e2e_tests/test_staking.py @@ -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, ) diff --git a/tests/e2e_tests/utils/chain_interactions.py b/tests/e2e_tests/utils/chain_interactions.py index 15998f93e0..93fabb29ff 100644 --- a/tests/e2e_tests/utils/chain_interactions.py +++ b/tests/e2e_tests/utils/chain_interactions.py @@ -181,6 +181,7 @@ def sudo_set_admin_utils( 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. @@ -195,7 +196,7 @@ def sudo_set_admin_utils( 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, ) @@ -222,7 +223,6 @@ async def root_set_subtensor_hyperparameter_values( wallet: "Wallet", call_function: str, call_params: dict, - return_error_message: bool = False, ) -> tuple[bool, str]: """ Sets liquid alpha values using AdminUtils. Mimics setting hyperparams @@ -240,10 +240,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, "" + return response.is_success, response.error_message def set_identity( From e0ea5ae672c5490c3a813a0c2de5363d1613ce70 Mon Sep 17 00:00:00 2001 From: Andreea Popescu Date: Thu, 27 Mar 2025 13:16:28 +0000 Subject: [PATCH 28/84] split process_weights_for_netuid --- bittensor/utils/weight_utils.py | 57 +++++++++++++++++++++++++++------ 1 file changed, 48 insertions(+), 9 deletions(-) diff --git a/bittensor/utils/weight_utils.py b/bittensor/utils/weight_utils.py index a93f584728..211c251d44 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}") From 6187a9be0c911d143ebe1865813aa049bd63da95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20=C5=BBy=C5=BAniewski?= Date: Fri, 28 Mar 2025 14:15:17 +0100 Subject: [PATCH 29/84] fix test_dendrite by making sure Alice is Top validator in Subnet --- tests/e2e_tests/test_dendrite.py | 7 +++++++ 1 file changed, 7 insertions(+) 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, From a12d023d5c827fa9d47e5cae2def9670731342a2 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Fri, 28 Mar 2025 15:16:20 +0200 Subject: [PATCH 30/84] Adds compatibility check for Python 3.13 --- .circleci/config.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index c610dd7b6c..36c31a8700 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: From 0c746f10a8b24e64879d6fbfb7faa03ae1c937ab Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 28 Mar 2025 16:43:12 -0700 Subject: [PATCH 31/84] add helper functions in `chain_data.utils` --- bittensor/core/chain_data/utils.py | 38 +++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/bittensor/core/chain_data/utils.py b/bittensor/core/chain_data/utils.py index c7d6986038..2387fceb0a 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.types import ScaleObj + class ChainDataType(Enum): NeuronInfo = 1 @@ -135,3 +138,36 @@ 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( + value: Optional["ScaleObj"], +) -> Optional[tuple[int, str]]: + """Decode the revealed commitment data from the given input if it is not None.""" + if value is None: + return None + + 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 + + v = next(iter(value)) + reveled_block = v[1] + cut = scale_decode_offset(v[0]) + + reveled_commitment = bytes(v[0][cut:]).decode("utf-8", errors="ignore") + return reveled_block, reveled_commitment + + +def decode_revealed_commitment_with_hotkey(data: list) -> dict[str, tuple[int, str]]: + """Decode revealed commitment using a hotkey.""" + key, value = data + ss58_address = decode_account_id(key[0]) + return {ss58_address: decode_revealed_commitment(value)} From a74320ada9b8400f8294de455cadb9d7800dcbae Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 28 Mar 2025 16:44:04 -0700 Subject: [PATCH 32/84] update serving extrinsics --- bittensor/core/extrinsics/asyncex/serving.py | 8 ++++---- bittensor/core/extrinsics/serving.py | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) 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/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 From 771a30d713cdfafc238ae961185c90f727483185 Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 28 Mar 2025 16:44:23 -0700 Subject: [PATCH 33/84] add functional to subtensors --- bittensor/core/async_subtensor.py | 109 +++++++++++++++++++++++++++++- bittensor/core/subtensor.py | 98 ++++++++++++++++++++++++++- 2 files changed, 203 insertions(+), 4 deletions(-) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index d1a54b1dcd..d89dfdf976 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -27,9 +27,14 @@ decode_account_id, DynamicInfo, ) +from bittensor_commit_reveal import get_encrypted_commitment 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 @@ -1059,6 +1064,68 @@ async def get_all_commitments( result[decode_account_id(id_[0])] = decode_metadata(value) return result + async def get_reveled_commitment( + self, + netuid: int, + hotkey_ss58_address: Optional[str] = None, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> Optional[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. + """ + query = await self.query_module( + module="Commitments", + name="RevealedCommitments", + params=[netuid, hotkey_ss58_address], + block=block, + block_hash=block_hash, + reuse_block=reuse_block, + ) + return decode_revealed_commitment(query) + + 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[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)}. + """ + query = await self.query_map( + module="Commitments", + name="RevealedCommitments", + params=[netuid], + block=block, + block_hash=block_hash, + reuse_block=reuse_block, + ) + + result = {} + async for item in query: + result.update(decode_revealed_commitment_with_hotkey(item)) + return result + async def get_current_weight_commit_info( self, netuid: int, @@ -2620,6 +2687,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, diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 567a79b70d..8154401ebb 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 @@ -803,6 +807,54 @@ def get_all_commitments( result[decode_account_id(id_[0])] = decode_metadata(value) return result + def get_reveled_commitment( + self, + netuid: int, + hotkey_ss58_address: Optional[str] = None, + block: Optional[int] = None, + ) -> Optional[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. + + Returns: + result (tuple[int, str): A tuple of reveal block and commitment message. + """ + query = self.query_module( + module="Commitments", + name="RevealedCommitments", + params=[netuid, hotkey_ss58_address], + block=block, + ) + return decode_revealed_commitment(query) + + def get_all_revealed_commitments( + self, netuid: int, block: Optional[int] = None + ) -> dict[str, 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)}. + """ + query = self.query_map( + module="Commitments", + name="RevealedCommitments", + params=[netuid], + block=block, + ) + + result = {} + for item in query: + result.update(decode_revealed_commitment_with_hotkey(item)) + return result + def get_current_weight_commit_info( self, netuid: int, block: Optional[int] = None ) -> list: @@ -2164,6 +2216,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. From f49a055610c434ae731ffadaf2e190c637e54f2e Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 28 Mar 2025 17:05:46 -0700 Subject: [PATCH 34/84] add e2e test for --- tests/e2e_tests/test_reveal_commitements.py | 97 +++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 tests/e2e_tests/test_reveal_commitements.py diff --git a/tests/e2e_tests/test_reveal_commitements.py b/tests/e2e_tests/test_reveal_commitements.py new file mode 100644 index 0000000000..7e0ef5ca98 --- /dev/null +++ b/tests/e2e_tests/test_reveal_commitements.py @@ -0,0 +1,97 @@ +import time + +import pytest + +from bittensor.utils.btlogging import logging + + +@pytest.mark.parametrize("local_chain", [False], 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 = 12 # 12 for non-fast-block, 0.25 for fast block + BLOCKS_UNTIL_REVEAL = 15 + + 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. + while subtensor.last_drand_round() < target_reveal_round + 1: + # wait one drand period (3 sec) + 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 + bob_actual_block, bob_actual_message = bob_result + + # 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_reveled_commitment (based of hotkey) + actual_alice_block, actual_alice_message = subtensor.get_reveled_commitment( + NETUID, alice_wallet.hotkey.ss58_address + ) + actual_bob_block, actual_bob_message = subtensor.get_reveled_commitment( + NETUID, bob_wallet.hotkey.ss58_address + ) + + assert message_alice == actual_alice_message + assert message_bob == actual_bob_message From cff93c459454ad5b72c42fefcf5c281ac14f21f9 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 31 Mar 2025 09:34:02 -0700 Subject: [PATCH 35/84] fix typo --- bittensor/core/async_subtensor.py | 2 +- bittensor/core/subtensor.py | 2 +- tests/e2e_tests/test_reveal_commitements.py | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index d89dfdf976..e3454387df 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -1064,7 +1064,7 @@ async def get_all_commitments( result[decode_account_id(id_[0])] = decode_metadata(value) return result - async def get_reveled_commitment( + async def get_revealed_commitment( self, netuid: int, hotkey_ss58_address: Optional[str] = None, diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 8154401ebb..16c7cb9024 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -807,7 +807,7 @@ def get_all_commitments( result[decode_account_id(id_[0])] = decode_metadata(value) return result - def get_reveled_commitment( + def get_revealed_commitment( self, netuid: int, hotkey_ss58_address: Optional[str] = None, diff --git a/tests/e2e_tests/test_reveal_commitements.py b/tests/e2e_tests/test_reveal_commitements.py index 7e0ef5ca98..0edc962620 100644 --- a/tests/e2e_tests/test_reveal_commitements.py +++ b/tests/e2e_tests/test_reveal_commitements.py @@ -85,11 +85,11 @@ async def test_set_reveal_commitment(local_chain, subtensor, alice_wallet, bob_w assert message_alice == alice_actual_message assert message_bob == bob_actual_message - # Assertions for get_reveled_commitment (based of hotkey) - actual_alice_block, actual_alice_message = subtensor.get_reveled_commitment( + # Assertions for get_revealed_commitment (based of hotkey) + actual_alice_block, actual_alice_message = subtensor.get_revealed_commitment( NETUID, alice_wallet.hotkey.ss58_address ) - actual_bob_block, actual_bob_message = subtensor.get_reveled_commitment( + actual_bob_block, actual_bob_message = subtensor.get_revealed_commitment( NETUID, bob_wallet.hotkey.ss58_address ) From 48c930cc27cfbe883e8e90fc1a16e91ce54362ba Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 31 Mar 2025 13:15:28 -0700 Subject: [PATCH 36/84] bumping dependency version for `bittensor-commit-reveal` to 0.3.0 --- requirements/prod.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/prod.txt b/requirements/prod.txt index bcd699d8b7..e5a89a3cb1 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-commit-reveal>=0.3.0 bittensor-wallet>=3.0.4 async-substrate-interface>=1.0.4 From 4d347e8cf0d7af95574b71f8fb560337a78c208d Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 31 Mar 2025 13:28:00 -0700 Subject: [PATCH 37/84] test `Update & Activate venv` without uv --- .circleci/config.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 36c31a8700..456d1c088a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -97,9 +97,9 @@ jobs: name: Update & Activate venv command: | python -m venv .venv - . .venv/bin/activate - python -m pip install --upgrade uv - uv sync --all-extras --dev + python -m pip install --upgrade pip + python -m pip install '.[dev]' + pip install flake8 - save_cache: name: Save cached venv From 85a4d8b20bee435cff35d844976afe0e81bcf080 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 31 Mar 2025 13:31:47 -0700 Subject: [PATCH 38/84] reverse last changes --- .circleci/config.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 456d1c088a..a91883289e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -97,9 +97,9 @@ jobs: name: Update & Activate venv command: | python -m venv .venv - python -m pip install --upgrade pip - python -m pip install '.[dev]' - pip install flake8 + python -m pip install --upgrade uv + uv sync --all-extras --dev + uv pip install flake8 - save_cache: name: Save cached venv From 91c621decee5994f45f4d8eccf056ca531258818 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 31 Mar 2025 13:40:25 -0700 Subject: [PATCH 39/84] fix --- .circleci/config.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index a91883289e..6d836ac360 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -97,6 +97,7 @@ jobs: name: Update & Activate venv command: | python -m venv .venv + . .venv/bin/activate python -m pip install --upgrade uv uv sync --all-extras --dev uv pip install flake8 From 8ffcf6feddf13856f0dc6dbb4a7d471ca00584ff Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 31 Mar 2025 14:35:01 -0700 Subject: [PATCH 40/84] test `pip` instead of `uv` --- .circleci/config.yml | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 6d836ac360..288e5cefdf 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -38,8 +38,8 @@ jobs: command: | python -m venv .venv . .venv/bin/activate - python -m pip install --upgrade uv - uv pip install ruff -c requirements/dev.txt + python -m pip install --upgrade pip + pip install ruff -c requirements/dev.txt - save_cache: name: Save cached ruff venv @@ -98,9 +98,8 @@ jobs: command: | python -m venv .venv . .venv/bin/activate - python -m pip install --upgrade uv - uv sync --all-extras --dev - uv pip install flake8 + python -m pip install --upgrade pip + upython -m pip install '.[dev]' - save_cache: name: Save cached venv @@ -112,7 +111,7 @@ jobs: name: Install Bittensor command: | . .venv/bin/activate - uv sync --all-extras --dev + pip install -e '.[dev]' - run: name: Instantiate Mock Wallet @@ -190,9 +189,9 @@ jobs: command: | python -m venv .venv . .venv/bin/activate - python -m pip install --upgrade uv - uv sync --all-extras --dev - uv pip install flake8 + python -m pip install --upgrade pip + python -m pip install '.[dev]' + pip install flake8 - save_cache: name: Save cached venv @@ -204,7 +203,7 @@ jobs: name: Install Bittensor command: | . .venv/bin/activate - uv sync --all-extras --dev + pip install -e '.[dev]' - run: name: Lint with flake8 @@ -233,7 +232,7 @@ jobs: - run: name: Combine Coverage command: | - uv pip install --upgrade coveralls + pip3 install --upgrade coveralls coveralls --finish --rcfile .coveragerc || echo "Failed to upload coverage" check-version-updated: From e718c54400ec3635062072e1594491d26eaf0ec2 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 31 Mar 2025 14:38:42 -0700 Subject: [PATCH 41/84] typo --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 288e5cefdf..9b723ebbfa 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -99,7 +99,7 @@ jobs: python -m venv .venv . .venv/bin/activate python -m pip install --upgrade pip - upython -m pip install '.[dev]' + python -m pip install '.[dev]' - save_cache: name: Save cached venv From 8d0a98d392a1b68f79f6c63c7d0123c673a7ccd9 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 31 Mar 2025 14:42:18 -0700 Subject: [PATCH 42/84] add rustup --- .circleci/config.yml | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 9b723ebbfa..ba6870fc25 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -36,10 +36,12 @@ jobs: - run: name: Update & Activate ruff venv command: | + curl https://sh.rustup.rs -sSf | sh -s -- -y + . "$HOME/.cargo/env" python -m venv .venv . .venv/bin/activate - python -m pip install --upgrade pip - pip install ruff -c requirements/dev.txt + python -m pip install --upgrade uv + uv pip install ruff -c requirements/dev.txt - save_cache: name: Save cached ruff venv @@ -98,8 +100,8 @@ jobs: command: | python -m venv .venv . .venv/bin/activate - python -m pip install --upgrade pip - python -m pip install '.[dev]' + python -m pip install --upgrade uv + uv sync --all-extras --dev - save_cache: name: Save cached venv @@ -111,7 +113,7 @@ jobs: name: Install Bittensor command: | . .venv/bin/activate - pip install -e '.[dev]' + uv sync --all-extras --dev - run: name: Instantiate Mock Wallet @@ -189,9 +191,9 @@ jobs: command: | python -m venv .venv . .venv/bin/activate - python -m pip install --upgrade pip - python -m pip install '.[dev]' - pip install flake8 + python -m pip install --upgrade uv + uv sync --all-extras --dev + uv pip install flake8 - save_cache: name: Save cached venv @@ -203,7 +205,7 @@ jobs: name: Install Bittensor command: | . .venv/bin/activate - pip install -e '.[dev]' + uv sync --all-extras --dev - run: name: Lint with flake8 @@ -232,7 +234,7 @@ jobs: - run: name: Combine Coverage command: | - pip3 install --upgrade coveralls + uv pip install --upgrade coveralls coveralls --finish --rcfile .coveragerc || echo "Failed to upload coverage" check-version-updated: @@ -290,9 +292,6 @@ 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: @@ -340,4 +339,4 @@ workflows: filters: branches: only: - - master + - master \ No newline at end of file From cb494f6311d84b2d3d46a604da9e6a74e272c400 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 31 Mar 2025 14:54:52 -0700 Subject: [PATCH 43/84] add Install Rust toolchain --- .circleci/config.yml | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index ba6870fc25..e73cc88d26 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -36,8 +36,6 @@ jobs: - run: name: Update & Activate ruff venv command: | - curl https://sh.rustup.rs -sSf | sh -s -- -y - . "$HOME/.cargo/env" python -m venv .venv . .venv/bin/activate python -m pip install --upgrade uv @@ -95,6 +93,21 @@ jobs: - v2-pypi-py<< parameters.python-version >>-{{ checksum "requirements/prod.txt" }}+{{ checksum "requirements/dev.txt" }} - v2-pypi-py<< parameters.python-version >> + - run: + name: Install Rust toolchain + command: | + curl https://sh.rustup.rs -sSf | sh -s -- -y + echo 'export PATH="$HOME/.cargo/bin:$PATH"' >> $BASH_ENV + source $BASH_ENV + rustc --version + cargo --version + + - run: + name: Install maturin + command: | + source $BASH_ENV + pip install maturin + - run: name: Update & Activate venv command: | From 4d0c4f75e5187888f2922f5a271d489b58c39ff5 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 31 Mar 2025 14:57:59 -0700 Subject: [PATCH 44/84] update Setup Rust and Install Dependencies --- .circleci/config.yml | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index e73cc88d26..e9be2c6398 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -94,23 +94,13 @@ jobs: - v2-pypi-py<< parameters.python-version >> - run: - name: Install Rust toolchain + name: Setup Rust and Install Dependencies command: | curl https://sh.rustup.rs -sSf | sh -s -- -y - echo 'export PATH="$HOME/.cargo/bin:$PATH"' >> $BASH_ENV - source $BASH_ENV + export PATH="$HOME/.cargo/bin:$PATH" rustc --version cargo --version - - - run: - name: Install maturin - command: | - source $BASH_ENV pip install maturin - - - run: - name: Update & Activate venv - command: | python -m venv .venv . .venv/bin/activate python -m pip install --upgrade uv From 1c35dc61bec2a95b84b0b9fb6416400436bda76f Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 31 Mar 2025 15:34:52 -0700 Subject: [PATCH 45/84] bring `.circleci/config.yml` back to staging state + test python 3.13.1 --- .circleci/config.yml | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index e9be2c6398..813ff6088e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -94,13 +94,8 @@ jobs: - v2-pypi-py<< parameters.python-version >> - run: - name: Setup Rust and Install Dependencies + name: Update & Activate venv command: | - curl https://sh.rustup.rs -sSf | sh -s -- -y - export PATH="$HOME/.cargo/bin:$PATH" - rustc --version - cargo --version - pip install maturin python -m venv .venv . .venv/bin/activate python -m pip install --upgrade uv @@ -295,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: @@ -307,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", "3.13.1"] requires: - check-if-pr-is-draft - unit-tests-all-python-versions: @@ -316,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", "3.13.1" ] requires: - check-if-pr-is-draft #- coveralls: @@ -342,4 +340,4 @@ workflows: filters: branches: only: - - master \ No newline at end of file + - master From adf6d3997256efefc9eda0c7d88a28684312a84e Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 31 Mar 2025 15:51:32 -0700 Subject: [PATCH 46/84] remove "3.13.1" --- .circleci/config.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 813ff6088e..fbe39a6645 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -305,7 +305,7 @@ workflows: - build-and-test: matrix: parameters: - python-version: [ "3.9.13", "3.10.6", "3.11.4", "3.12.7", "3.13.1"] + 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: @@ -314,7 +314,7 @@ workflows: - lint-and-type-check: matrix: parameters: - python-version: [ "3.9.13", "3.10.6", "3.11.4", "3.12.7", "3.13.1" ] + python-version: [ "3.9.13", "3.10.6", "3.11.4", "3.12.7"] requires: - check-if-pr-is-draft #- coveralls: From a1c5e8157f6b4bfb3a99ddf34a7995c26441eb2f Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 31 Mar 2025 17:04:30 -0700 Subject: [PATCH 47/84] bumping `bittensor-commit-reveal` version to 0.3.1 --- requirements/prod.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/prod.txt b/requirements/prod.txt index e5a89a3cb1..433c92189a 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.3.0 +bittensor-commit-reveal>=0.3.1 bittensor-wallet>=3.0.4 async-substrate-interface>=1.0.4 From 3c4de4b2aa6b5ae35f5703c0f1e3a662a90af4f2 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 31 Mar 2025 20:52:17 -0700 Subject: [PATCH 48/84] improve --- tests/e2e_tests/utils/chain_interactions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e_tests/utils/chain_interactions.py b/tests/e2e_tests/utils/chain_interactions.py index eb54a75630..e01574dc08 100644 --- a/tests/e2e_tests/utils/chain_interactions.py +++ b/tests/e2e_tests/utils/chain_interactions.py @@ -5,7 +5,6 @@ import asyncio import contextlib -import unittest.mock from typing import Union, Optional, TYPE_CHECKING from bittensor.utils.balance import Balance @@ -191,6 +190,7 @@ 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). From e18224a0f040504b9820b311ec99d4d6ba37b602 Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 1 Apr 2025 08:37:45 -0700 Subject: [PATCH 49/84] review fix --- bittensor/core/chain_data/utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bittensor/core/chain_data/utils.py b/bittensor/core/chain_data/utils.py index 2387fceb0a..9984c2ecc3 100644 --- a/bittensor/core/chain_data/utils.py +++ b/bittensor/core/chain_data/utils.py @@ -159,11 +159,11 @@ def scale_decode_offset(data: bytes) -> int: return 4 v = next(iter(value)) - reveled_block = v[1] + revealed_block = v[1] cut = scale_decode_offset(v[0]) - reveled_commitment = bytes(v[0][cut:]).decode("utf-8", errors="ignore") - return reveled_block, reveled_commitment + revealed_commitment = bytes(v[0][cut:]).decode("utf-8", errors="ignore") + return revealed_block, revealed_commitment def decode_revealed_commitment_with_hotkey(data: list) -> dict[str, tuple[int, str]]: From 7050f6f45b5f5fb50e538992f68f6e5fffedad9f Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 1 Apr 2025 09:02:38 -0700 Subject: [PATCH 50/84] extend message --- bittensor/utils/balance.py | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/bittensor/utils/balance.py b/bittensor/utils/balance.py index c8d8ff583b..35aa708654 100644 --- a/bittensor/utils/balance.py +++ b/bittensor/utils/balance.py @@ -7,12 +7,29 @@ def _check_currencies(self, other): - """Checks that Balance objects have the same netuids to perform arithmetic operations.""" + """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. " - f"First balance is `{self}`. Second balance is `{other}`. ", + "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, ) From 06c5f346af1ac47dd265ad60ab6fc0cb8e0087b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20=C5=BBy=C5=BAniewski?= Date: Wed, 2 Apr 2025 11:26:12 +0200 Subject: [PATCH 51/84] fix: missing f-string format --- bittensor/core/extrinsics/asyncex/weights.py | 2 +- bittensor/core/extrinsics/set_weights.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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/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( From 37d6ac64aea861ec965937c899389e8d8650f62c Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 2 Apr 2025 14:52:55 -0700 Subject: [PATCH 52/84] bump version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 2c01c2858d..a447cd9ebe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,7 @@ dependencies = [ "scalecodec==1.2.11", "uvicorn", "websockets>=14.1", - "bittensor-commit-reveal>=0.2.0", + "bittensor-commit-reveal>=0.3.1", "bittensor-wallet>=3.0.4", "async-substrate-interface>=1.0.8" ] From 6c61ee58433681222f6b73af1b893f73b119bd3c Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 2 Apr 2025 16:42:08 -0700 Subject: [PATCH 53/84] .circleci/config.yml without uv --- .circleci/config.yml | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index fbe39a6645..095c645819 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -38,8 +38,7 @@ jobs: command: | python -m venv .venv . .venv/bin/activate - python -m pip install --upgrade uv - uv pip install ruff -c requirements/dev.txt + pip install ruff -c requirements/dev.txt - save_cache: name: Save cached ruff venv @@ -98,8 +97,7 @@ jobs: command: | python -m venv .venv . .venv/bin/activate - python -m pip install --upgrade uv - uv sync --all-extras --dev + pip install .[dev,torch] - save_cache: name: Save cached venv @@ -111,7 +109,7 @@ jobs: name: Install Bittensor command: | . .venv/bin/activate - uv sync --all-extras --dev + pip install .[dev,torch] - run: name: Instantiate Mock Wallet @@ -189,9 +187,7 @@ jobs: command: | python -m venv .venv . .venv/bin/activate - python -m pip install --upgrade uv - uv sync --all-extras --dev - uv pip install flake8 + pip install .[dev,torch] - save_cache: name: Save cached venv @@ -203,7 +199,7 @@ jobs: name: Install Bittensor command: | . .venv/bin/activate - uv sync --all-extras --dev + pip install .[dev,torch] - run: name: Lint with flake8 @@ -232,7 +228,7 @@ jobs: - run: name: Combine Coverage command: | - uv pip install --upgrade coveralls + pip install --upgrade coveralls coveralls --finish --rcfile .coveragerc || echo "Failed to upload coverage" check-version-updated: From 4e8027f4df6a1c50c7303c9bb4f9a9700a9a9e19 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 2 Apr 2025 17:35:08 -0700 Subject: [PATCH 54/84] bump btwallet version in deps --- pyproject.toml | 2 +- requirements/prod.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a447cd9ebe..8110850d17 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,7 @@ dependencies = [ "uvicorn", "websockets>=14.1", "bittensor-commit-reveal>=0.3.1", - "bittensor-wallet>=3.0.4", + "bittensor-wallet>=3.0.5", "async-substrate-interface>=1.0.8" ] diff --git a/requirements/prod.txt b/requirements/prod.txt index 433c92189a..326203817c 100644 --- a/requirements/prod.txt +++ b/requirements/prod.txt @@ -22,5 +22,5 @@ scalecodec==1.2.11 uvicorn websockets>=14.1 bittensor-commit-reveal>=0.3.1 -bittensor-wallet>=3.0.4 +bittensor-wallet>=3.0.5 async-substrate-interface>=1.0.4 From 6ce0a68eea1c7ffffdd73ef3beebf08cd2912fa2 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 2 Apr 2025 17:38:21 -0700 Subject: [PATCH 55/84] reverse config.yml --- .circleci/config.yml | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 095c645819..36c31a8700 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -38,7 +38,8 @@ jobs: command: | python -m venv .venv . .venv/bin/activate - pip install ruff -c requirements/dev.txt + python -m pip install --upgrade uv + uv pip install ruff -c requirements/dev.txt - save_cache: name: Save cached ruff venv @@ -97,7 +98,8 @@ jobs: command: | python -m venv .venv . .venv/bin/activate - pip install .[dev,torch] + python -m pip install --upgrade uv + uv sync --all-extras --dev - save_cache: name: Save cached venv @@ -109,7 +111,7 @@ jobs: name: Install Bittensor command: | . .venv/bin/activate - pip install .[dev,torch] + uv sync --all-extras --dev - run: name: Instantiate Mock Wallet @@ -187,7 +189,9 @@ jobs: command: | python -m venv .venv . .venv/bin/activate - pip install .[dev,torch] + python -m pip install --upgrade uv + uv sync --all-extras --dev + uv pip install flake8 - save_cache: name: Save cached venv @@ -199,7 +203,7 @@ jobs: name: Install Bittensor command: | . .venv/bin/activate - pip install .[dev,torch] + uv sync --all-extras --dev - run: name: Lint with flake8 @@ -228,7 +232,7 @@ jobs: - run: name: Combine Coverage command: | - pip install --upgrade coveralls + uv pip install --upgrade coveralls coveralls --finish --rcfile .coveragerc || echo "Failed to upload coverage" check-version-updated: @@ -301,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: @@ -310,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: From ab07a0b13ea54dc0631d41c47edf4d3148c3ba72 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 2 Apr 2025 19:00:59 -0700 Subject: [PATCH 56/84] bum bt-wallet version in deps --- pyproject.toml | 2 +- requirements/prod.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8110850d17..1084ad14a3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,7 @@ dependencies = [ "uvicorn", "websockets>=14.1", "bittensor-commit-reveal>=0.3.1", - "bittensor-wallet>=3.0.5", + "bittensor-wallet>=3.0.6", "async-substrate-interface>=1.0.8" ] diff --git a/requirements/prod.txt b/requirements/prod.txt index 326203817c..d9d6cf0697 100644 --- a/requirements/prod.txt +++ b/requirements/prod.txt @@ -22,5 +22,5 @@ scalecodec==1.2.11 uvicorn websockets>=14.1 bittensor-commit-reveal>=0.3.1 -bittensor-wallet>=3.0.5 +bittensor-wallet>=3.0.6 async-substrate-interface>=1.0.4 From b102012e706dca737ce601bd2c995ae8778c99d9 Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 3 Apr 2025 09:29:45 -0700 Subject: [PATCH 57/84] add if for synapse.dendrite.signature --- bittensor/core/axon.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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}" ) From 25ce76ecd8cca77b32ebfdd8c66b5781caee5c3a Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 3 Apr 2025 09:30:02 -0700 Subject: [PATCH 58/84] add cli in pyproject.toml --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 1084ad14a3..5c45fcc8cb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,6 +67,9 @@ dev = [ torch = [ "torch>=1.13.1,<2.6.0" ] +cli = [ + "bittensor-cli>=9.0.2" +] [project.urls] # more details can be found here From 7c741984e495d68b99bfe51e6efa4d9b64d3869c Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 3 Apr 2025 09:30:48 -0700 Subject: [PATCH 59/84] bump bittensor-wallet version to 3.0.7 --- pyproject.toml | 2 +- requirements/prod.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 5c45fcc8cb..f31cab3c3b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,7 @@ dependencies = [ "uvicorn", "websockets>=14.1", "bittensor-commit-reveal>=0.3.1", - "bittensor-wallet>=3.0.6", + "bittensor-wallet>=3.0.7", "async-substrate-interface>=1.0.8" ] diff --git a/requirements/prod.txt b/requirements/prod.txt index d9d6cf0697..893d925ce9 100644 --- a/requirements/prod.txt +++ b/requirements/prod.txt @@ -22,5 +22,5 @@ scalecodec==1.2.11 uvicorn websockets>=14.1 bittensor-commit-reveal>=0.3.1 -bittensor-wallet>=3.0.6 +bittensor-wallet>=3.0.7 async-substrate-interface>=1.0.4 From c3b16288328cf67299e1337d9e273a598aec4465 Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 3 Apr 2025 09:47:31 -0700 Subject: [PATCH 60/84] remove spaces --- .circleci/config.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 36c31a8700..fbe39a6645 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -305,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: @@ -314,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: From bafe0dad7a95e0cc815414af17f029ec127d24c7 Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 3 Apr 2025 19:09:44 -0700 Subject: [PATCH 61/84] improvement --- bittensor/core/async_subtensor.py | 74 +++++++++++++++++---- bittensor/core/chain_data/utils.py | 45 ++++++++----- bittensor/core/subtensor.py | 65 +++++++++++++++--- tests/e2e_tests/test_reveal_commitements.py | 24 ++++--- 4 files changed, 160 insertions(+), 48 deletions(-) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index 3b1af02dd0..dbae08cb4a 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 @@ -27,7 +28,6 @@ decode_account_id, DynamicInfo, ) -from bittensor_commit_reveal import get_encrypted_commitment 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 ( @@ -38,17 +38,17 @@ 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, @@ -84,6 +84,7 @@ decode_hex_identity_dict, float_to_u64, format_error_message, + is_valid_ss58_address, torch, u16_normalized_float, u64_normalized_float, @@ -1064,14 +1065,14 @@ async def get_all_commitments( result[decode_account_id(id_[0])] = decode_metadata(value) return result - async def get_revealed_commitment( + 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[int, str]]: + ) -> Optional[tuple[tuple[int, str], ...]]: """Returns hotkey related revealed commitment for a given netuid. Arguments: @@ -1084,6 +1085,9 @@ async def get_revealed_commitment( 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", @@ -1092,7 +1096,42 @@ async def get_revealed_commitment( block_hash=block_hash, reuse_block=reuse_block, ) - return decode_revealed_commitment(query) + 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, @@ -1100,7 +1139,7 @@ async def get_all_revealed_commitments( block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> dict[str, tuple[int, str]]: + ) -> dict[str, tuple[tuple[int, str], ...]]: """Returns all revealed commitments for a given netuid. Arguments: @@ -1111,6 +1150,12 @@ async def get_all_revealed_commitments( 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", @@ -1122,8 +1167,11 @@ async def get_all_revealed_commitments( ) result = {} - async for item in query: - result.update(decode_revealed_commitment_with_hotkey(item)) + 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( diff --git a/bittensor/core/chain_data/utils.py b/bittensor/core/chain_data/utils.py index 9984c2ecc3..9915b51c1f 100644 --- a/bittensor/core/chain_data/utils.py +++ b/bittensor/core/chain_data/utils.py @@ -11,7 +11,7 @@ from bittensor.utils.balance import Balance if TYPE_CHECKING: - from async_substrate_interface.types import ScaleObj + from async_substrate_interface.sync_substrate import QueryMapResult class ChainDataType(Enum): @@ -140,12 +140,16 @@ def decode_metadata(metadata: dict) -> str: return bytes(bytes_tuple).decode() -def decode_revealed_commitment( - value: Optional["ScaleObj"], -) -> Optional[tuple[int, str]]: - """Decode the revealed commitment data from the given input if it is not None.""" - if value is None: - return None +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.""" @@ -158,16 +162,25 @@ def scale_decode_offset(data: bytes) -> int: else: return 4 - v = next(iter(value)) - revealed_block = v[1] - cut = scale_decode_offset(v[0]) + com_bytes, revealed_block = encoded_data + offset = scale_decode_offset(com_bytes) - revealed_commitment = bytes(v[0][cut:]).decode("utf-8", errors="ignore") + revealed_commitment = bytes(com_bytes[offset:]).decode("utf-8", errors="ignore") return revealed_block, revealed_commitment -def decode_revealed_commitment_with_hotkey(data: list) -> dict[str, tuple[int, str]]: - """Decode revealed commitment using a hotkey.""" - key, value = data - ss58_address = decode_account_id(key[0]) - return {ss58_address: decode_revealed_commitment(value)} +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/subtensor.py b/bittensor/core/subtensor.py index f928b58857..8f103dc6d2 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -86,6 +86,7 @@ decode_hex_identity_dict, float_to_u64, format_error_message, + is_valid_ss58_address, torch, u16_normalized_float, u64_normalized_float, @@ -787,6 +788,7 @@ def get_commitment(self, netuid: int, uid: int, block: Optional[int] = None) -> return "" metadata = cast(dict, get_metadata(self, netuid, hotkey, block)) + print(metadata) try: return decode_metadata(metadata) @@ -807,33 +809,71 @@ def get_all_commitments( result[decode_account_id(id_[0])] = decode_metadata(value) return result - def get_revealed_commitment( + def get_revealed_commitment_by_hotkey( self, netuid: int, - hotkey_ss58_address: Optional[str] = None, + hotkey_ss58_address: str, block: Optional[int] = None, - ) -> Optional[tuple[int, str]]: + ) -> 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 (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, ) - return decode_revealed_commitment(query) + 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[int, str]]: + ) -> dict[str, tuple[tuple[int, str], ...]]: """Returns all revealed commitments for a given netuid. Arguments: @@ -842,6 +882,12 @@ def get_all_revealed_commitments( 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", @@ -851,8 +897,11 @@ def get_all_revealed_commitments( ) result = {} - for item in query: - result.update(decode_revealed_commitment_with_hotkey(item)) + 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( diff --git a/tests/e2e_tests/test_reveal_commitements.py b/tests/e2e_tests/test_reveal_commitements.py index 0edc962620..37af2eebd2 100644 --- a/tests/e2e_tests/test_reveal_commitements.py +++ b/tests/e2e_tests/test_reveal_commitements.py @@ -5,7 +5,7 @@ from bittensor.utils.btlogging import logging -@pytest.mark.parametrize("local_chain", [False], indirect=True) +@pytest.mark.parametrize("local_chain", [True], indirect=True) @pytest.mark.asyncio async def test_set_reveal_commitment(local_chain, subtensor, alice_wallet, bob_wallet): """ @@ -25,8 +25,8 @@ async def test_set_reveal_commitment(local_chain, subtensor, alice_wallet, bob_w 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 = 12 # 12 for non-fast-block, 0.25 for fast block - BLOCKS_UNTIL_REVEAL = 15 + BLOCK_TIME = 0.25 # 12 for non-fast-block, 0.25 for fast block + BLOCKS_UNTIL_REVEAL = 10 NETUID = 2 @@ -65,8 +65,10 @@ async def test_set_reveal_commitment(local_chain, subtensor, alice_wallet, bob_w # 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. - while subtensor.last_drand_round() < target_reveal_round + 1: + 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) @@ -77,8 +79,8 @@ async def test_set_reveal_commitment(local_chain, subtensor, alice_wallet, bob_w 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 - bob_actual_block, bob_actual_message = bob_result + 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. @@ -87,11 +89,11 @@ async def test_set_reveal_commitment(local_chain, subtensor, alice_wallet, bob_w # Assertions for get_revealed_commitment (based of hotkey) actual_alice_block, actual_alice_message = subtensor.get_revealed_commitment( - NETUID, alice_wallet.hotkey.ss58_address - ) - actual_bob_block, actual_bob_message = subtensor.get_revealed_commitment( - NETUID, bob_wallet.hotkey.ss58_address - ) + 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 From 28880074f68d3ecb7ce504d6f41b9694305c8765 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Fri, 4 Apr 2025 17:15:56 +0200 Subject: [PATCH 62/84] Bump async-substrate-interface --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index f31cab3c3b..d09623ad19 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,7 @@ dependencies = [ "websockets>=14.1", "bittensor-commit-reveal>=0.3.1", "bittensor-wallet>=3.0.7", - "async-substrate-interface>=1.0.8" + "async-substrate-interface>=1.0.9" ] [project.optional-dependencies] From 3435cd15b68e071db7eac0ef35ce3dbae249c680 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Fri, 4 Apr 2025 17:55:38 +0200 Subject: [PATCH 63/84] Cleans up some versioning, removes unnecessary requirements (namely rich) --- bittensor/utils/registration/pow.py | 30 +++++++++++++++++++++++++---- pyproject.toml | 12 +++++------- 2 files changed, 31 insertions(+), 11 deletions(-) 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/pyproject.toml b/pyproject.toml index f31cab3c3b..a82502a509 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,20 +22,18 @@ 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.3.1", "bittensor-wallet>=3.0.7", "async-substrate-interface>=1.0.8" From 336136ea70fdf4351d9f4aa234966aa7302b46d1 Mon Sep 17 00:00:00 2001 From: Roman <167799377+roman-opentensor@users.noreply.github.com> Date: Fri, 4 Apr 2025 09:02:42 -0700 Subject: [PATCH 64/84] Update bittensor/core/subtensor.py Co-authored-by: BD Himes <37844818+thewhaleking@users.noreply.github.com> --- bittensor/core/subtensor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 8f103dc6d2..ec3654d953 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -788,7 +788,6 @@ def get_commitment(self, netuid: int, uid: int, block: Optional[int] = None) -> return "" metadata = cast(dict, get_metadata(self, netuid, hotkey, block)) - print(metadata) try: return decode_metadata(metadata) From 2e707089110ef281fb55143b14e5e67b0be0628a Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 4 Apr 2025 13:42:16 -0700 Subject: [PATCH 65/84] add `stop_existing_test_containers` - stops container if it's exits --- tests/e2e_tests/conftest.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/tests/e2e_tests/conftest.py b/tests/e2e_tests/conftest.py index e0afea8a88..cc340ff26a 100644 --- a/tests/e2e_tests/conftest.py +++ b/tests/e2e_tests/conftest.py @@ -20,6 +20,7 @@ ) LOCALNET_IMAGE_NAME = "ghcr.io/opentensor/subtensor-localnet:devnet-ready" +CONTAINER_NAME_PREFIX = "test_local_chain_" def wait_for_node_start(process, timestamp=None): @@ -163,7 +164,25 @@ 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('.', '_')}" + 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 = [ @@ -182,6 +201,8 @@ def try_start_docker(): try_start_docker() + stop_existing_test_containers() + # Start container with subprocess.Popen( cmds, From 4a1334da0f3a9ff710b809c5972f124b38a49842 Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 4 Apr 2025 14:01:35 -0700 Subject: [PATCH 66/84] ruff --- tests/e2e_tests/conftest.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/e2e_tests/conftest.py b/tests/e2e_tests/conftest.py index cc340ff26a..0170cbd302 100644 --- a/tests/e2e_tests/conftest.py +++ b/tests/e2e_tests/conftest.py @@ -168,11 +168,18 @@ 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}}"], + [ + "docker", + "ps", + "--filter", + f"name={CONTAINER_NAME_PREFIX}", + "--format", + "{{.ID}}", + ], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, - check=True + check=True, ) container_ids = existing_container_result.stdout.strip().splitlines() for cid in container_ids: From 173989f6eda3b6109b6e0249c5f2ab4a846d7cc4 Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 4 Apr 2025 16:28:02 -0700 Subject: [PATCH 67/84] improve `next_tempo` + netuid offset --- tests/e2e_tests/utils/chain_interactions.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/tests/e2e_tests/utils/chain_interactions.py b/tests/e2e_tests/utils/chain_interactions.py index 93fabb29ff..99e03beaf3 100644 --- a/tests/e2e_tests/utils/chain_interactions.py +++ b/tests/e2e_tests/utils/chain_interactions.py @@ -106,11 +106,7 @@ def next_tempo(current_block: int, tempo: int, netuid: int) -> int: Returns: int: The next tempo block number. """ - current_block += 1 - 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( @@ -191,6 +187,7 @@ 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, optional): The AdminUtils module to call. Defaults to "AdminUtils". Returns: tuple[bool, Optional[dict]]: (success status, error details). From 33e402496779886ef81b6966f4a8c55c73d70517 Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 4 Apr 2025 16:28:40 -0700 Subject: [PATCH 68/84] add block_time argument to crv3 extrinsic --- bittensor/core/extrinsics/commit_reveal.py | 3 +++ 1 file changed, 3 insertions(+) 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( From 4c8cdbf9480cfd5e8173a68e66925cb49b0a7a94 Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 4 Apr 2025 16:28:55 -0700 Subject: [PATCH 69/84] add block_time argument to crv3 call in subtensor --- bittensor/core/subtensor.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index ec3654d953..7ddb049957 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -3099,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 @@ -3118,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 @@ -3159,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 From e62c17614cbc3f7952003e59d9f159a4ed3785df Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 4 Apr 2025 16:29:08 -0700 Subject: [PATCH 70/84] fix unit test --- tests/unit_tests/extrinsics/test_commit_reveal.py | 1 + 1 file changed, 1 insertion(+) 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, From 3dc33a586e0ec948519fc86707eb84394a6cff4f Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 4 Apr 2025 16:29:23 -0700 Subject: [PATCH 71/84] improve CRv3 e2e test --- tests/e2e_tests/test_commit_reveal_v3.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/e2e_tests/test_commit_reveal_v3.py b/tests/e2e_tests/test_commit_reveal_v3.py index 52d2324c62..a7d04f4e05 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") @@ -127,6 +128,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 +150,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 From 4e81520fa35120843302b3db4683b7ed37da49d3 Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 4 Apr 2025 16:50:35 -0700 Subject: [PATCH 72/84] fix `wait_for_block` logic in CRv3 test --- tests/e2e_tests/test_commit_reveal_v3.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/e2e_tests/test_commit_reveal_v3.py b/tests/e2e_tests/test_commit_reveal_v3.py index a7d04f4e05..1313cd9e80 100644 --- a/tests/e2e_tests/test_commit_reveal_v3.py +++ b/tests/e2e_tests/test_commit_reveal_v3.py @@ -72,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 = 20 assert ( sudo_set_admin_utils( local_chain, @@ -102,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: From b8c5dd16385525cd59696b6b16117822be5671c9 Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 4 Apr 2025 16:57:45 -0700 Subject: [PATCH 73/84] fix one more unit test --- tests/unit_tests/test_subtensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index 3215d49a0e..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 From 706e6acea47eab9a250a0361f25ab6799e663418 Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 4 Apr 2025 17:03:38 -0700 Subject: [PATCH 74/84] increase tempo for test (chain has not enough time to store revealed data) --- tests/e2e_tests/test_commit_reveal_v3.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e_tests/test_commit_reveal_v3.py b/tests/e2e_tests/test_commit_reveal_v3.py index 1313cd9e80..6d45cbd94d 100644 --- a/tests/e2e_tests/test_commit_reveal_v3.py +++ b/tests/e2e_tests/test_commit_reveal_v3.py @@ -73,7 +73,7 @@ async def test_commit_and_reveal_weights_cr3(local_chain, subtensor, alice_walle logging.console.info("sudo_set_weights_set_rate_limit executed: set to 0") # Change the tempo of the subnet - tempo_set = 20 + tempo_set = 50 assert ( sudo_set_admin_utils( local_chain, From 5632150be573e3ed1ce0c2145367e3d04b0382ca Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 4 Apr 2025 17:19:20 -0700 Subject: [PATCH 75/84] fix async CRv2 --- bittensor/core/extrinsics/asyncex/commit_reveal.py | 3 +++ 1 file changed, 3 insertions(+) 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( From 3d256e77dcbf49104ad497e0637d3ad2cb875521 Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 4 Apr 2025 17:20:55 -0700 Subject: [PATCH 76/84] improve async subtensor call --- bittensor/core/async_subtensor.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index dbae08cb4a..9a132f4258 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -3812,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 @@ -3831,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 @@ -3879,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 From 6e002ff7bc4e3be47be888b48b007f672601cdee Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 4 Apr 2025 17:21:20 -0700 Subject: [PATCH 77/84] fix async CRv3 unit test --- tests/unit_tests/extrinsics/asyncex/test_commit_reveal.py | 1 + 1 file changed, 1 insertion(+) 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, From cb3bc679147946ae34d557e4bf547196cf381985 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Mon, 7 Apr 2025 20:47:40 +0200 Subject: [PATCH 78/84] Bump substrate. --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 80fae5c6e7..ceed41972a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,7 @@ dependencies = [ "uvicorn", "bittensor-commit-reveal>=0.3.1", "bittensor-wallet>=3.0.7", - "async-substrate-interface>=1.0.9" + "async-substrate-interface>=1.1.0" ] [project.optional-dependencies] From 042e21a7fdedbd3ac44e7aa14ef036b78204c487 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Mon, 7 Apr 2025 22:36:59 +0200 Subject: [PATCH 79/84] Trigger no-op From a28869681d1f02f13964db1aecdc70b57f90f421 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Tue, 8 Apr 2025 16:59:06 +0200 Subject: [PATCH 80/84] Formatting. --- bittensor/utils/weight_utils.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bittensor/utils/weight_utils.py b/bittensor/utils/weight_utils.py index 211c251d44..5c98b1f383 100644 --- a/bittensor/utils/weight_utils.py +++ b/bittensor/utils/weight_utils.py @@ -322,9 +322,9 @@ def process_weights( if nzw_size == 0 or num_neurons < min_allowed_weights: logging.warning("No non-zero weights returning all ones.") final_weights = ( - torch.ones((num_neurons)).to(num_neurons) / num_neurons + torch.ones(num_neurons).to(num_neurons) / num_neurons if use_torch() - else np.ones((num_neurons), dtype=np.int64) / num_neurons + else np.ones(num_neurons, dtype=np.int64) / num_neurons ) logging.debug(f"final_weights: {final_weights}") final_weights_count = ( @@ -344,9 +344,9 @@ def process_weights( ) # ( const ): Should this be np.zeros( ( num_neurons ) ) to reset everyone to build up weight? weights = ( - torch.ones((num_neurons)).to(num_neurons) * 1e-5 + torch.ones(num_neurons).to(num_neurons) * 1e-5 if use_torch() - else np.ones((num_neurons), 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}") From cb4e5f42ed0c1bcb9a5ba825915b3515f16a4844 Mon Sep 17 00:00:00 2001 From: sashaphmn Date: Wed, 9 Apr 2025 18:20:21 +0300 Subject: [PATCH 81/84] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) 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) From 6dcca4466aa7ab51b7955ab43977e9e864b568c0 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Wed, 9 Apr 2025 18:31:47 +0200 Subject: [PATCH 82/84] Remove unused levenshtein requirement. --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ceed41972a..c77f7280e7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,6 @@ dependencies = [ "retry==0.9.2", "requests>=2.0.0,<3.0", "pydantic>=2.3, <3", - "python-Levenshtein", "scalecodec==1.2.11", "uvicorn", "bittensor-commit-reveal>=0.3.1", From 8cee51302f52028b84a8aaa815ca45b8894b7130 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Wed, 9 Apr 2025 12:55:46 -0700 Subject: [PATCH 83/84] Bumps version and changelog --- CHANGELOG.md | 30 ++++++++++++++++++++++++++++++ VERSION | 2 +- bittensor/core/settings.py | 2 +- pyproject.toml | 4 ++-- 4 files changed, 34 insertions(+), 4 deletions(-) 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/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/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/pyproject.toml b/pyproject.toml index c77f7280e7..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 = [ @@ -34,7 +34,7 @@ dependencies = [ "scalecodec==1.2.11", "uvicorn", "bittensor-commit-reveal>=0.3.1", - "bittensor-wallet>=3.0.7", + "bittensor-wallet>=3.0.8", "async-substrate-interface>=1.1.0" ] From 6751f1bebb81970fd08e09dfc54d7ac227650f2c Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 9 Apr 2025 15:12:56 -0700 Subject: [PATCH 84/84] fix --- bittensor/core/subtensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 7ddb049957..19b14d09c0 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -399,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