From bbe6c8af9424064df1fbb33b20a34736b56b80cf Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 12 Aug 2024 18:05:54 -0700 Subject: [PATCH 1/3] Adds commit-reveal back and adds e2e for liquid alpha & commit reveal --- bittensor/core/subtensor.py | 270 ++++++++++++++++++ .../deprecated/extrinsics/commit_weights.py | 126 ++++++++ bittensor/utils/weight_utils.py | 60 +++- tests/e2e_tests/test_commit_weights.py | 161 +++++++++++ tests/e2e_tests/test_liquid_alpha.py | 187 ++++++++++++ tests/e2e_tests/utils/chain_interactions.py | 55 ++++ 6 files changed, 857 insertions(+), 2 deletions(-) create mode 100644 bittensor/utils/deprecated/extrinsics/commit_weights.py create mode 100644 tests/e2e_tests/test_commit_weights.py create mode 100644 tests/e2e_tests/test_liquid_alpha.py diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 062e02350e..3ffd92d5a7 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -49,9 +49,14 @@ from bittensor.utils.deprecated.extrinsics.set_weights import ( set_weights_extrinsic, ) +from bittensor.utils.deprecated.extrinsics.commit_weights import ( + commit_weights_extrinsic, + reveal_weights_extrinsic, +) from bittensor.utils.deprecated.extrinsics.transfer import ( transfer_extrinsic, ) +from bittensor.utils.weight_utils import generate_weight_hash from bittensor.core import settings from bittensor.core.axon import Axon from bittensor.core.chain_data import ( @@ -1836,3 +1841,268 @@ def get_existential_deposit( if result is None or not hasattr(result, "value"): return None return Balance.from_rao(result.value) + + def commit_weights( + self, + wallet: "Wallet", + netuid: int, + salt: List[int], + uids: Union[NDArray[np.int64], list], + weights: Union[NDArray[np.int64], list], + version_key: int = settings.version_as_int, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = False, + prompt: bool = False, + max_retries: int = 5, + ) -> Tuple[bool, str]: + """ + Commits a hash of the neuron's weights to the Bittensor blockchain using the provided wallet. + This action serves as a commitment or snapshot of the neuron's current weight distribution. + + Args: + wallet (bittensor.wallet): The wallet associated with the neuron committing the weights. + netuid (int): The unique identifier of the subnet. + salt (List[int]): list of randomly generated integers as salt to generated weighted hash. + uids (np.ndarray): NumPy array of neuron UIDs for which weights are being committed. + weights (np.ndarray): NumPy array of weight values corresponding to each UID. + version_key (int, optional): Version key for compatibility with the network. + wait_for_inclusion (bool, optional): Waits for the transaction to be included in a block. + wait_for_finalization (bool, optional): Waits for the transaction to be finalized on the blockchain. + prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. + max_retries (int, optional): The number of maximum attempts to commit weights. (Default: 5) + + Returns: + Tuple[bool, str]: ``True`` if the weight commitment is successful, False otherwise. And `msg`, a string + value describing the success or potential error. + + This function allows neurons to create a tamper-proof record of their weight distribution at a specific point in time, + enhancing transparency and accountability within the Bittensor network. + """ + retries = 0 + success = False + message = "No attempt made. Perhaps it is too soon to commit weights!" + + logging.info( + "Committing weights with params: netuid={}, uids={}, weights={}, version_key={}".format( + netuid, uids, weights, version_key + ) + ) + + # Generate the hash of the weights + commit_hash = generate_weight_hash( + address=wallet.hotkey.ss58_address, + netuid=netuid, + uids=list(uids), + values=list(weights), + salt=salt, + version_key=version_key, + ) + + logging.info("Commit Hash: {}".format(commit_hash)) + + while retries < max_retries: + try: + success, message = commit_weights_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + commit_hash=commit_hash, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + prompt=prompt, + ) + if success: + break + except Exception as e: + logging.error(f"Error committing weights: {e}") + finally: + retries += 1 + + return success, message + + def _do_commit_weights( + self, + wallet: "Wallet", + netuid: int, + commit_hash: str, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = False, + ) -> Tuple[bool, Optional[str]]: + """ + Internal method to send a transaction to the Bittensor blockchain, committing the hash of a neuron's weights. + This method constructs and submits the transaction, handling retries and blockchain communication. + + Args: + wallet (bittensor.wallet): The wallet associated with the neuron committing the weights. + netuid (int): The unique identifier of the subnet. + commit_hash (str): The hash of the neuron's weights to be committed. + wait_for_inclusion (bool, optional): Waits for the transaction to be included in a block. + wait_for_finalization (bool, optional): Waits for the transaction to be finalized on the blockchain. + + Returns: + Tuple[bool, Optional[str]]: A tuple containing a success flag and an optional error message. + + This method ensures that the weight commitment is securely recorded on the Bittensor blockchain, providing a + verifiable record of the neuron's weight distribution at a specific point in time. + """ + + @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) + def make_substrate_call_with_retry(): + call = self.substrate.compose_call( + call_module="SubtensorModule", + call_function="commit_weights", + call_params={ + "netuid": netuid, + "commit_hash": commit_hash, + }, + ) + extrinsic = self.substrate.create_signed_extrinsic( + call=call, + keypair=wallet.hotkey, + ) + response = self.substrate.submit_extrinsic( + extrinsic, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + if not wait_for_finalization and not wait_for_inclusion: + return True, None + + response.process_events() + if response.is_success: + return True, None + else: + return False, response.error_message + + return make_substrate_call_with_retry() + + def reveal_weights( + self, + wallet: "Wallet", + netuid: int, + uids: Union[NDArray[np.int64], list], + weights: Union[NDArray[np.int64], list], + salt: Union[NDArray[np.int64], list], + version_key: int = settings.version_as_int, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = False, + prompt: bool = False, + max_retries: int = 5, + ) -> Tuple[bool, str]: + """ + Reveals the weights for a specific subnet on the Bittensor blockchain using the provided wallet. + This action serves as a revelation of the neuron's previously committed weight distribution. + + Args: + wallet (bittensor.wallet): The wallet associated with the neuron revealing the weights. + netuid (int): The unique identifier of the subnet. + uids (np.ndarray): NumPy array of neuron UIDs for which weights are being revealed. + weights (np.ndarray): NumPy array of weight values corresponding to each UID. + salt (np.ndarray): NumPy array of salt values corresponding to the hash function. + version_key (int, optional): Version key for compatibility with the network. + wait_for_inclusion (bool, optional): Waits for the transaction to be included in a block. + wait_for_finalization (bool, optional): Waits for the transaction to be finalized on the blockchain. + prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. + max_retries (int, optional): The number of maximum attempts to reveal weights. (Default: 5) + + Returns: + Tuple[bool, str]: ``True`` if the weight revelation is successful, False otherwise. And `msg`, a string + value describing the success or potential error. + + This function allows neurons to reveal their previously committed weight distribution, ensuring transparency + and accountability within the Bittensor network. + """ + + retries = 0 + success = False + message = "No attempt made. Perhaps it is too soon to reveal weights!" + + while retries < max_retries: + try: + success, message = reveal_weights_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + uids=list(uids), + weights=list(weights), + salt=list(salt), + version_key=version_key, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + prompt=prompt, + ) + if success: + break + except Exception as e: + logging.error(f"Error revealing weights: {e}") + finally: + retries += 1 + + return success, message + + def _do_reveal_weights( + self, + wallet: "Wallet", + netuid: int, + uids: List[int], + values: List[int], + salt: List[int], + version_key: int, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = False, + ) -> Tuple[bool, Optional[str]]: + """ + Internal method to send a transaction to the Bittensor blockchain, revealing the weights for a specific subnet. + This method constructs and submits the transaction, handling retries and blockchain communication. + + Args: + wallet (bittensor.wallet): The wallet associated with the neuron revealing the weights. + netuid (int): The unique identifier of the subnet. + uids (List[int]): List of neuron UIDs for which weights are being revealed. + values (List[int]): List of weight values corresponding to each UID. + salt (List[int]): List of salt values corresponding to the hash function. + version_key (int): Version key for compatibility with the network. + wait_for_inclusion (bool, optional): Waits for the transaction to be included in a block. + wait_for_finalization (bool, optional): Waits for the transaction to be finalized on the blockchain. + + Returns: + Tuple[bool, Optional[str]]: A tuple containing a success flag and an optional error message. + + This method ensures that the weight revelation is securely recorded on the Bittensor blockchain, providing transparency + and accountability for the neuron's weight distribution. + """ + + @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) + def make_substrate_call_with_retry(): + call = self.substrate.compose_call( + call_module="SubtensorModule", + call_function="reveal_weights", + call_params={ + "netuid": netuid, + "uids": uids, + "values": values, + "salt": salt, + "version_key": version_key, + }, + ) + extrinsic = self.substrate.create_signed_extrinsic( + call=call, + keypair=wallet.hotkey, + ) + response = self.substrate.submit_extrinsic( + extrinsic, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + if not wait_for_finalization and not wait_for_inclusion: + return True, None + + response.process_events() + if response.is_success: + return True, None + else: + return False, format_error_message(response.error_message) + + return make_substrate_call_with_retry() diff --git a/bittensor/utils/deprecated/extrinsics/commit_weights.py b/bittensor/utils/deprecated/extrinsics/commit_weights.py new file mode 100644 index 0000000000..9fe6ae7ffa --- /dev/null +++ b/bittensor/utils/deprecated/extrinsics/commit_weights.py @@ -0,0 +1,126 @@ +# The MIT License (MIT) +# Copyright © 2021 Yuma Rao +# Copyright © 2023 Opentensor Foundation + +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the “Software”), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of +# the Software. + +# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +"""Module commit weights and reveal weights extrinsic.""" + +from typing import List, Tuple + +from rich.prompt import Confirm + +import bittensor +from bittensor.utils import format_error_message + + +def commit_weights_extrinsic( + subtensor: "bittensor.subtensor", + wallet: "bittensor.wallet", + netuid: int, + commit_hash: str, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = False, + prompt: bool = False, +) -> Tuple[bool, str]: + """ + Commits a hash of the neuron's weights to the Bittensor blockchain using the provided wallet. + This function is a wrapper around the `_do_commit_weights` method, handling user prompts and error messages. + Args: + subtensor (bittensor.subtensor): The subtensor instance used for blockchain interaction. + wallet (bittensor.wallet): The wallet associated with the neuron committing the weights. + netuid (int): The unique identifier of the subnet. + commit_hash (str): The hash of the neuron's weights to be committed. + wait_for_inclusion (bool, optional): Waits for the transaction to be included in a block. + wait_for_finalization (bool, optional): Waits for the transaction to be finalized on the blockchain. + prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. + Returns: + Tuple[bool, str]: ``True`` if the weight commitment is successful, False otherwise. And `msg`, a string + value describing the success or potential error. + This function provides a user-friendly interface for committing weights to the Bittensor blockchain, ensuring proper + error handling and user interaction when required. + """ + if prompt and not Confirm.ask(f"Would you like to commit weights?"): + return False, "User cancelled the operation." + + success, error_message = subtensor._do_commit_weights( + wallet=wallet, + netuid=netuid, + commit_hash=commit_hash, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + if success: + bittensor.logging.info("Successfully committed weights.") + return True, "Successfully committed weights." + else: + bittensor.logging.error(f"Failed to commit weights: {error_message}") + return False, format_error_message(error_message) + + +def reveal_weights_extrinsic( + subtensor: "bittensor.subtensor", + wallet: "bittensor.wallet", + netuid: int, + uids: List[int], + weights: List[int], + salt: List[int], + version_key: int, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = False, + prompt: bool = False, +) -> Tuple[bool, str]: + """ + Reveals the weights for a specific subnet on the Bittensor blockchain using the provided wallet. + This function is a wrapper around the `_do_reveal_weights` method, handling user prompts and error messages. + Args: + subtensor (bittensor.subtensor): The subtensor instance used for blockchain interaction. + wallet (bittensor.wallet): The wallet associated with the neuron revealing the weights. + netuid (int): The unique identifier of the subnet. + uids (List[int]): List of neuron UIDs for which weights are being revealed. + weights (List[int]): List of weight values corresponding to each UID. + salt (List[int]): List of salt values corresponding to the hash function. + version_key (int): Version key for compatibility with the network. + wait_for_inclusion (bool, optional): Waits for the transaction to be included in a block. + wait_for_finalization (bool, optional): Waits for the transaction to be finalized on the blockchain. + prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. + Returns: + Tuple[bool, str]: ``True`` if the weight revelation is successful, False otherwise. And `msg`, a string + value describing the success or potential error. + This function provides a user-friendly interface for revealing weights on the Bittensor blockchain, ensuring proper + error handling and user interaction when required. + """ + + if prompt and not Confirm.ask(f"Would you like to reveal weights?"): + return False, "User cancelled the operation." + + success, error_message = subtensor._do_reveal_weights( + wallet=wallet, + netuid=netuid, + uids=uids, + values=weights, + salt=salt, + version_key=version_key, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + if success: + bittensor.logging.info("Successfully revealed weights.") + return True, "Successfully revealed weights." + else: + bittensor.logging.error(f"Failed to reveal weights: {error_message}") + return False, error_message diff --git a/bittensor/utils/weight_utils.py b/bittensor/utils/weight_utils.py index 3333108369..5e5fa0b73f 100644 --- a/bittensor/utils/weight_utils.py +++ b/bittensor/utils/weight_utils.py @@ -17,15 +17,18 @@ """Conversion for weight between chain representation and np.array or torch.Tensor""" +import hashlib import logging import typing -from typing import Tuple, List, Union +from typing import List, Tuple, Union import numpy as np from numpy.typing import NDArray +from scalecodec import U16, ScaleBytes, Vec +from substrateinterface import Keypair from bittensor.utils.btlogging import logging -from bittensor.utils.registration import torch, use_torch, legacy_torch_api_compat +from bittensor.utils.registration import legacy_torch_api_compat, torch, use_torch if typing.TYPE_CHECKING: from bittensor.core.metagraph import Metagraph @@ -357,3 +360,56 @@ def process_weights_for_netuid( logging.debug("final_weights", normalized_weights) return non_zero_weight_uids, normalized_weights + + +def generate_weight_hash( + address: str, + netuid: int, + uids: List[int], + values: List[int], + version_key: int, + salt: List[int], +) -> str: + """ + Generate a valid commit hash from the provided weights. + + Args: + address (str): The account identifier. Wallet ss58_address. + netuid (int): The network unique identifier. + uids (List[int]): The list of UIDs. + salt (List[int]): The salt to add to hash. + values (List[int]): The list of weight values. + version_key (int): The version key. + + Returns: + str: The generated commit hash. + """ + # Encode data using SCALE codec + wallet_address = ScaleBytes(Keypair(ss58_address=address).public_key) + netuid = ScaleBytes(netuid.to_bytes(2, "little")) + + vec_uids = Vec(data=None, sub_type="U16") + vec_uids.value = [U16(ScaleBytes(uid.to_bytes(2, "little"))) for uid in uids] + uids = ScaleBytes(vec_uids.encode().data) + + vec_values = Vec(data=None, sub_type="U16") + vec_values.value = [ + U16(ScaleBytes(value.to_bytes(2, "little"))) for value in values + ] + values = ScaleBytes(vec_values.encode().data) + + version_key = ScaleBytes(version_key.to_bytes(8, "little")) + + vec_salt = Vec(data=None, sub_type="U16") + vec_salt.value = [U16(ScaleBytes(salts.to_bytes(2, "little"))) for salts in salt] + salt = ScaleBytes(vec_salt.encode().data) + + data = wallet_address + netuid + uids + values + salt + version_key + + # Generate Blake2b hash of the data tuple + blake2b_hash = hashlib.blake2b(data.data, digest_size=32) + + # Convert the hash to hex string and add "0x" prefix + commit_hash = "0x" + blake2b_hash.hexdigest() + + return commit_hash diff --git a/tests/e2e_tests/test_commit_weights.py b/tests/e2e_tests/test_commit_weights.py new file mode 100644 index 0000000000..3e451f9eeb --- /dev/null +++ b/tests/e2e_tests/test_commit_weights.py @@ -0,0 +1,161 @@ +import time + +import numpy as np +import pytest + +import bittensor +from bittensor import logging +from bittensor.utils.weight_utils import convert_weights_and_uids_for_emit +from tests.e2e_tests.utils.chain_interactions import ( + add_stake, + register_neuron, + register_subnet, + sudo_set_hyperparameter_bool, + sudo_set_hyperparameter_values, + wait_interval, +) +from tests.e2e_tests.utils.test_utils import setup_wallet + + +@pytest.mark.asyncio +async def test_commit_and_reveal_weights(local_chain): + """ + Tests the commit/reveal weights mechanism + + Steps: + 1. Register a subnet through Alice + 2. Register Alice's neuron and add stake + 3. Enable commit-reveal mechanism on the subnet + 4. Lower the commit_reveal interval and rate limit + 5. Commit weights and verify + 6. Wait interval & reveal weights and verify + Raises: + AssertionError: If any of the checks or verifications fail + """ + netuid = 1 + logging.info("Testing test_commit_and_reveal_weights") + # Register root as Alice + keypair, alice_wallet = setup_wallet("//Alice") + assert register_subnet(local_chain, alice_wallet), "Unable to register the subnet" + + # Verify subnet 1 created successfully + assert local_chain.query( + "SubtensorModule", "NetworksAdded", [1] + ).serialize(), "Subnet wasn't created successfully" + + assert register_neuron( + local_chain, alice_wallet, netuid + ), "Unable to register Alice as a neuron" + + # Stake to become to top neuron after the first epoch + add_stake(local_chain, alice_wallet, bittensor.Balance.from_tao(100_000)) + + # Enable commit_reveal on the subnet + assert sudo_set_hyperparameter_bool( + local_chain, + alice_wallet, + "sudo_set_commit_reveal_weights_enabled", + True, + netuid, + ), "Unable to enable commit reveal on the subnet" + + subtensor = bittensor.subtensor(network="ws://localhost:9945") + assert subtensor.get_subnet_hyperparameters( + netuid=netuid + ).commit_reveal_weights_enabled, "Failed to enable commit/reveal" + + # Lower the commit_reveal interval + assert sudo_set_hyperparameter_values( + local_chain, + alice_wallet, + call_function="sudo_set_commit_reveal_weights_interval", + call_params={"netuid": netuid, "interval": "370"}, + return_error_message=True, + ) + + subtensor = bittensor.subtensor(network="ws://localhost:9945") + assert ( + subtensor.get_subnet_hyperparameters( + netuid=netuid + ).commit_reveal_weights_interval + == 370 + ), "Failed to set commit/reveal interval" + + # Lower the rate limit + assert sudo_set_hyperparameter_values( + local_chain, + alice_wallet, + call_function="sudo_set_weights_set_rate_limit", + call_params={"netuid": netuid, "weights_set_rate_limit": "0"}, + return_error_message=True, + ) + subtensor = bittensor.subtensor(network="ws://localhost:9945") + assert ( + subtensor.get_subnet_hyperparameters(netuid=1).weights_rate_limit == 0 + ), "Failed to set weights_rate_limit" + + # Commit-reveal values + uids = np.array([0], dtype=np.int64) + weights = np.array([0.1], dtype=np.float32) + salt = [18, 179, 107, 0, 165, 211, 141, 197] + weight_uids, weight_vals = convert_weights_and_uids_for_emit( + uids=uids, weights=weights + ) + + # Commit weights + success, message = subtensor.commit_weights( + alice_wallet, + netuid, + salt=salt, + uids=weight_uids, + weights=weight_vals, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + weight_commits = subtensor.query_module( + module="SubtensorModule", + name="WeightCommits", + params=[netuid, alice_wallet.hotkey.ss58_address], + ) + # Assert that the committed weights are set correctly + assert weight_commits.value is not None, "Weight commit not found in storage" + commit_hash, commit_block = weight_commits.value + assert commit_block > 0, f"Invalid block number: {commit_block}" + + # Query the WeightCommitRevealInterval storage map + weight_commit_reveal_interval = subtensor.query_module( + module="SubtensorModule", name="WeightCommitRevealInterval", params=[netuid] + ) + interval = weight_commit_reveal_interval.value + assert interval > 0, "Invalid WeightCommitRevealInterval" + + # Wait until the reveal block range + await wait_interval(interval, subtensor) + + # Reveal weights + success, message = subtensor.reveal_weights( + alice_wallet, + netuid, + uids=weight_uids, + weights=weight_vals, + salt=salt, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + time.sleep(10) + + # Query the Weights storage map + revealed_weights = subtensor.query_module( + module="SubtensorModule", + name="Weights", + params=[netuid, 0], # netuid and uid + ) + + # Assert that the revealed weights are set correctly + assert revealed_weights.value is not None, "Weight reveal not found in storage" + + assert ( + weight_vals[0] == revealed_weights.value[0][1] + ), f"Incorrect revealed weights. Expected: {weights[0]}, Actual: {revealed_weights.value[0][1]}" + logging.info("✅ Passed test_commit_and_reveal_weights") diff --git a/tests/e2e_tests/test_liquid_alpha.py b/tests/e2e_tests/test_liquid_alpha.py new file mode 100644 index 0000000000..371a908e4a --- /dev/null +++ b/tests/e2e_tests/test_liquid_alpha.py @@ -0,0 +1,187 @@ +import bittensor +from bittensor import logging +from tests.e2e_tests.utils.chain_interactions import ( + add_stake, + register_neuron, + register_subnet, + sudo_set_hyperparameter_bool, + sudo_set_hyperparameter_values, +) +from tests.e2e_tests.utils.test_utils import setup_wallet + + +def liquid_alpha_call_params(netuid: int, alpha_values: str): + alpha_low, alpha_high = [v.strip() for v in alpha_values.split(",")] + return { + "netuid": netuid, + "alpha_low": alpha_low, + "alpha_high": alpha_high, + } + + +def test_liquid_alpha(local_chain): + """ + Test the liquid alpha mechanism + + Steps: + 1. Register a subnet through Alice + 2. Register Alice's neuron and add stake + 3. Verify we can't set alpha values without enabling liquid_alpha + 4. Test setting alpha values after enabling liquid_alpha + 5. Verify failures when setting incorrect values (upper and lower bounds) + Raises: + AssertionError: If any of the checks or verifications fail + """ + u16_max = 65535 + netuid = 1 + logging.info("Testing test_liquid_alpha_enabled") + + # Register root as Alice + keypair, alice_wallet = setup_wallet("//Alice") + register_subnet(local_chain, alice_wallet), "Unable to register the subnet" + + # Verify subnet 1 created successfully + assert local_chain.query("SubtensorModule", "NetworksAdded", [1]).serialize() + + # Register a neuron to the subnet + ( + register_neuron(local_chain, alice_wallet, netuid), + "Unable to register Alice as a neuron", + ) + + # Stake to become to top neuron after the first epoch + add_stake(local_chain, alice_wallet, bittensor.Balance.from_tao(100_000)) + + # Assert liquid alpha is disabled + subtensor = bittensor.subtensor(network="ws://localhost:9945") + assert ( + subtensor.get_subnet_hyperparameters(netuid=netuid).liquid_alpha_enabled + is False + ), "Liquid alpha is enabled by default" + + # Attempt to set alpha high/low while disabled (should fail) + alpha_values = "6553, 53083" + call_params = liquid_alpha_call_params(netuid, alpha_values) + result, error_message = sudo_set_hyperparameter_values( + local_chain, + alice_wallet, + call_function="sudo_set_alpha_values", + call_params=call_params, + return_error_message=True, + ) + assert result is False, "Alpha values set while being disabled" + assert error_message["name"] == "LiquidAlphaDisabled" + + # Enabled liquid alpha on the subnet + assert sudo_set_hyperparameter_bool( + local_chain, alice_wallet, "sudo_set_liquid_alpha_enabled", True, netuid + ), "Unable to enable liquid alpha" + + assert subtensor.get_subnet_hyperparameters( + netuid=1 + ).liquid_alpha_enabled, "Failed to enable liquid alpha" + + # Attempt to set alpha high & low after enabling the hyperparameter + alpha_values = "87, 54099" + call_params = liquid_alpha_call_params(netuid, alpha_values) + assert sudo_set_hyperparameter_values( + local_chain, + alice_wallet, + call_function="sudo_set_alpha_values", + call_params=call_params, + ), "Unable to set alpha_values" + assert ( + subtensor.get_subnet_hyperparameters(netuid=1).alpha_high == 54099 + ), "Failed to set alpha high" + assert ( + subtensor.get_subnet_hyperparameters(netuid=1).alpha_low == 87 + ), "Failed to set alpha low" + + # Testing alpha high upper and lower bounds + + # 1. Test setting Alpha_high too low + alpha_high_too_low = ( + u16_max * 4 // 5 + ) - 1 # One less than the minimum acceptable value + call_params = liquid_alpha_call_params(netuid, f"6553, {alpha_high_too_low}") + result, error_message = sudo_set_hyperparameter_values( + local_chain, + alice_wallet, + call_function="sudo_set_alpha_values", + call_params=call_params, + return_error_message=True, + ) + + assert result is False, "Able to set incorrect alpha_high value" + assert error_message["name"] == "AlphaHighTooLow" + + # 2. Test setting Alpha_high too high + alpha_high_too_high = u16_max + 1 # One more than the max acceptable value + call_params = liquid_alpha_call_params(netuid, f"6553, {alpha_high_too_high}") + try: + result, error_message = sudo_set_hyperparameter_values( + local_chain, + alice_wallet, + call_function="sudo_set_alpha_values", + call_params=call_params, + return_error_message=True, + ) + except Exception as e: + assert str(e) == "65536 out of range for u16", f"Unexpected error: {e}" + + # Testing alpha low upper and lower bounds + + # 1. Test setting Alpha_low too low + alpha_low_too_low = 0 + call_params = liquid_alpha_call_params(netuid, f"{alpha_low_too_low}, 53083") + result, error_message = sudo_set_hyperparameter_values( + local_chain, + alice_wallet, + call_function="sudo_set_alpha_values", + call_params=call_params, + return_error_message=True, + ) + assert result is False, "Able to set incorrect alpha_low value" + assert error_message["name"] == "AlphaLowOutOfRange" + + # 2. Test setting Alpha_low too high + alpha_low_too_high = ( + u16_max * 4 // 5 + ) + 1 # One more than the maximum acceptable value + call_params = liquid_alpha_call_params(netuid, f"{alpha_low_too_high}, 53083") + result, error_message = sudo_set_hyperparameter_values( + local_chain, + alice_wallet, + call_function="sudo_set_alpha_values", + call_params=call_params, + return_error_message=True, + ) + assert result is False, "Able to set incorrect alpha_low value" + assert error_message["name"] == "AlphaLowOutOfRange" + + # Setting normal alpha values + alpha_values = "6553, 53083" + call_params = liquid_alpha_call_params(netuid, alpha_values) + assert sudo_set_hyperparameter_values( + local_chain, + alice_wallet, + call_function="sudo_set_alpha_values", + call_params=call_params, + ), "Unable to set liquid alpha values" + + assert ( + subtensor.get_subnet_hyperparameters(netuid=1).alpha_high == 53083 + ), "Failed to set alpha high" + assert ( + subtensor.get_subnet_hyperparameters(netuid=1).alpha_low == 6553 + ), "Failed to set alpha low" + + # Disable Liquid Alpha + assert sudo_set_hyperparameter_bool( + local_chain, alice_wallet, "sudo_set_liquid_alpha_enabled", False, netuid + ), "Unable to disable liquid alpha" + + assert ( + subtensor.get_subnet_hyperparameters(netuid=1).liquid_alpha_enabled is False + ), "Failed to disable liquid alpha" + logging.info("✅ Passed test_liquid_alpha") diff --git a/tests/e2e_tests/utils/chain_interactions.py b/tests/e2e_tests/utils/chain_interactions.py index f0797770dc..148da7680e 100644 --- a/tests/e2e_tests/utils/chain_interactions.py +++ b/tests/e2e_tests/utils/chain_interactions.py @@ -4,6 +4,7 @@ """ import asyncio +from typing import Dict, List, Tuple, Union from substrateinterface import SubstrateInterface @@ -11,6 +12,60 @@ from bittensor import logging +def sudo_set_hyperparameter_bool( + substrate: SubstrateInterface, + wallet: bittensor.wallet, + call_function: str, + value: bool, + netuid: int, +) -> bool: + """ + Sets boolean hyperparameter value through AdminUtils. Mimics setting hyperparams + """ + call = substrate.compose_call( + call_module="AdminUtils", + call_function=call_function, + call_params={"netuid": netuid, "enabled": value}, + ) + extrinsic = substrate.create_signed_extrinsic(call=call, keypair=wallet.coldkey) + response = substrate.submit_extrinsic( + extrinsic, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + response.process_events() + return response.is_success + + +def sudo_set_hyperparameter_values( + substrate: SubstrateInterface, + wallet: bittensor.wallet, + call_function: str, + call_params: Dict, + return_error_message: bool = False, +) -> Union[bool, Tuple[bool, str]]: + """ + Sets liquid alpha values using AdminUtils. Mimics setting hyperparams + """ + call = substrate.compose_call( + call_module="AdminUtils", + call_function=call_function, + call_params=call_params, + ) + extrinsic = substrate.create_signed_extrinsic(call=call, keypair=wallet.coldkey) + response = substrate.submit_extrinsic( + extrinsic, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + response.process_events() + + if return_error_message: + return response.is_success, response.error_message + + return response.is_success + + def add_stake( substrate: SubstrateInterface, wallet: bittensor.wallet, amount: bittensor.Balance ) -> bool: From 9b7492196a37502ec03671959cdb57c6c2403051 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Tue, 13 Aug 2024 09:30:20 -0700 Subject: [PATCH 2/3] Review suggestions implemented --- bittensor/utils/deprecated/extrinsics/commit_weights.py | 9 ++++----- tests/e2e_tests/conftest.py | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/bittensor/utils/deprecated/extrinsics/commit_weights.py b/bittensor/utils/deprecated/extrinsics/commit_weights.py index 9fe6ae7ffa..cd56988c77 100644 --- a/bittensor/utils/deprecated/extrinsics/commit_weights.py +++ b/bittensor/utils/deprecated/extrinsics/commit_weights.py @@ -1,15 +1,14 @@ # The MIT License (MIT) -# Copyright © 2021 Yuma Rao -# Copyright © 2023 Opentensor Foundation - +# Copyright © 2024 Opentensor Foundation +# # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated # documentation files (the “Software”), to deal in the Software without restriction, including without limitation # the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, # and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - +# # The above copyright notice and this permission notice shall be included in all copies or substantial portions of # the Software. - +# # THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO # THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION diff --git a/tests/e2e_tests/conftest.py b/tests/e2e_tests/conftest.py index 9fc9faec68..7105991906 100644 --- a/tests/e2e_tests/conftest.py +++ b/tests/e2e_tests/conftest.py @@ -22,7 +22,7 @@ def local_chain(request): param = request.param if hasattr(request, "param") else None # Get the environment variable for the script path - script_path = os.getenv("LOCALNET_SH_PATH") + script_path = "/Users/ibraheem/Desktop/Bittensor/subtensor/scripts/localnet.sh" if not script_path: # Skip the test if the localhost.sh path is not set From b417ef1d849cd463bd968e296c061dbab7992f39 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Tue, 13 Aug 2024 09:32:07 -0700 Subject: [PATCH 3/3] Fixes logging string --- 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 3ffd92d5a7..9b35cda2ec 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -1898,7 +1898,7 @@ def commit_weights( version_key=version_key, ) - logging.info("Commit Hash: {}".format(commit_hash)) + logging.info(f"Commit Hash: {commit_hash}") while retries < max_retries: try: