From bf07b851ed99b64a859d9f3e895bfe29ff58bc43 Mon Sep 17 00:00:00 2001 From: opendansor Date: Wed, 26 Jun 2024 11:41:23 -0700 Subject: [PATCH 01/13] Merge pull request #2012 from opentensor/feature/gus/liquid-alpha-params Liquid alpha --- bittensor/chain_data.py | 9 + bittensor/commands/network.py | 52 +++- bittensor/commands/utils.py | 6 +- bittensor/extrinsics/network.py | 30 +- .../subcommands/hyperparams/__init__.py | 0 .../hyperparams/test_liquid_alpha.py | 275 ++++++++++++++++++ .../subcommands/wallet/test_faucet.py | 1 + .../integration_tests/test_cli_no_network.py | 79 +++++ 8 files changed, 443 insertions(+), 9 deletions(-) create mode 100644 tests/e2e_tests/subcommands/hyperparams/__init__.py create mode 100644 tests/e2e_tests/subcommands/hyperparams/test_liquid_alpha.py diff --git a/bittensor/chain_data.py b/bittensor/chain_data.py index 4a9f98244c..e62ad19621 100644 --- a/bittensor/chain_data.py +++ b/bittensor/chain_data.py @@ -186,6 +186,9 @@ ["difficulty", "Compact"], ["commit_reveal_weights_interval", "Compact"], ["commit_reveal_weights_enabled", "bool"], + ["alpha_high", "Compact"], + ["alpha_low", "Compact"], + ["liquid_alpha_enabled", "bool"], ], }, } @@ -981,6 +984,9 @@ class SubnetHyperparameters: difficulty: int commit_reveal_weights_interval: int commit_reveal_weights_enabled: bool + alpha_high: int + alpha_low: int + liquid_alpha_enabled: bool @classmethod def from_vec_u8(cls, vec_u8: List[int]) -> Optional["SubnetHyperparameters"]: @@ -1033,6 +1039,9 @@ def fix_decoded_values(cls, decoded: Dict) -> "SubnetHyperparameters": difficulty=decoded["difficulty"], commit_reveal_weights_interval=decoded["commit_reveal_weights_interval"], commit_reveal_weights_enabled=decoded["commit_reveal_weights_enabled"], + alpha_high=decoded["alpha_high"], + alpha_low=decoded["alpha_low"], + liquid_alpha_enabled=decoded["liquid_alpha_enabled"], ) def to_parameter_dict( diff --git a/bittensor/commands/network.py b/bittensor/commands/network.py index 0843b71c70..b5fada55a9 100644 --- a/bittensor/commands/network.py +++ b/bittensor/commands/network.py @@ -17,10 +17,10 @@ import argparse import bittensor -from . import defaults +from . import defaults # type: ignore from rich.prompt import Prompt from rich.table import Table -from typing import List, Optional, Dict +from typing import List, Optional, Dict, Union, Tuple from .utils import ( get_delegates_details, DelegatesDetails, @@ -330,6 +330,8 @@ def add_args(parser: argparse.ArgumentParser): "bonds_moving_avg": "sudo_set_bonds_moving_average", "commit_reveal_weights_interval": "sudo_set_commit_reveal_weights_interval", "commit_reveal_weights_enabled": "sudo_set_commit_reveal_weights_enabled", + "alpha_values": "sudo_set_alpha_values", + "liquid_alpha_enabled": "sudo_set_liquid_alpha_enabled", } @@ -388,6 +390,7 @@ def _run( cli.config.param == "network_registration_allowed" or cli.config.param == "network_pow_registration_allowed" or cli.config.param == "commit_reveal_weights_enabled" + or cli.config.param == "liquid_alpha_enabled" ): cli.config.value = ( True @@ -395,11 +398,17 @@ def _run( else False ) + is_allowed_value, value = allowed_value(cli.config.param, cli.config.value) + if not is_allowed_value: + raise ValueError( + f"Hyperparameter {cli.config.param} value is not within bounds. Value is {cli.config.value} but must be {value}" + ) + subtensor.set_hyperparameter( wallet, netuid=cli.config.netuid, parameter=cli.config.param, - value=cli.config.value, + value=value, prompt=not cli.config.no_prompt, ) @@ -638,3 +647,40 @@ def add_args(parser: argparse.ArgumentParser): default=False, ) bittensor.subtensor.add_args(parser) + + +def allowed_value( + param: str, value: Union[str, bool, float] +) -> Tuple[bool, Union[str, list[float], float]]: + """ + Check the allowed values on hyperparameters. Return False if value is out of bounds. + """ + # Reminder error message ends like: Value is {value} but must be {error_message}. (the second part of return statement) + # Check if value is a boolean, only allow boolean and floats + try: + if not isinstance(value, bool): + if param == "alpha_values": + # Split the string into individual values + alpha_low_str, alpha_high_str = value.split(",") + alpha_high = float(alpha_high_str) + alpha_low = float(alpha_low_str) + + # Check alpha_high value + if alpha_high <= 52428 or alpha_high >= 65535: + return ( + False, + f"between 52428 and 65535 for alpha_high (but is {alpha_high})", + ) + + # Check alpha_low value + if alpha_low < 0 or alpha_low > 52428: + return ( + False, + f"between 0 and 52428 for alpha_low (but is {alpha_low})", + ) + + return True, [alpha_low, alpha_high] + except ValueError: + return False, "a number or a boolean" + + return True, value diff --git a/bittensor/commands/utils.py b/bittensor/commands/utils.py index 1694d3bc5e..661cd818cc 100644 --- a/bittensor/commands/utils.py +++ b/bittensor/commands/utils.py @@ -186,10 +186,10 @@ def filter_netuids_by_registered_hotkeys( ) netuids_with_registered_hotkeys.extend(netuids_list) - if cli.config.netuids == None or cli.config.netuids == []: + if not cli.config.netuids: netuids = netuids_with_registered_hotkeys - elif cli.config.netuids != []: + else: netuids = [netuid for netuid in netuids if netuid in cli.config.netuids] netuids.extend(netuids_with_registered_hotkeys) @@ -216,6 +216,8 @@ def normalize_hyperparameters( "bonds_moving_avg": U64_NORMALIZED_FLOAT, "max_weight_limit": U16_NORMALIZED_FLOAT, "kappa": U16_NORMALIZED_FLOAT, + "alpha_high": U16_NORMALIZED_FLOAT, + "alpha_low": U16_NORMALIZED_FLOAT, "min_burn": Balance.from_rao, "max_burn": Balance.from_rao, } diff --git a/bittensor/extrinsics/network.py b/bittensor/extrinsics/network.py index c03e5cf77b..16cbc0ed26 100644 --- a/bittensor/extrinsics/network.py +++ b/bittensor/extrinsics/network.py @@ -183,16 +183,38 @@ def set_hyperparameter_extrinsic( extrinsic_params = substrate.get_metadata_call_function( "AdminUtils", extrinsic ) - value_argument = extrinsic_params["fields"][ - len(extrinsic_params["fields"]) - 1 - ] + call_params = {"netuid": netuid} + + # if input value is a list, iterate through the list and assign values + if isinstance(value, list): + # Create an iterator for the list of values + value_iterator = iter(value) + # Iterate over all value arguments and add them to the call_params dictionary + for value_argument in extrinsic_params["fields"]: + if "netuid" not in str(value_argument["name"]): + # Assign the next value from the iterator + try: + call_params[str(value_argument["name"])] = next( + value_iterator + ) + except StopIteration: + raise ValueError( + "Not enough values provided in the list for all parameters" + ) + + else: + value_argument = extrinsic_params["fields"][ + len(extrinsic_params["fields"]) - 1 + ] + call_params[str(value_argument["name"])] = value # create extrinsic call call = substrate.compose_call( call_module="AdminUtils", call_function=extrinsic, - call_params={"netuid": netuid, str(value_argument["name"]): value}, + call_params=call_params, ) + extrinsic = substrate.create_signed_extrinsic( call=call, keypair=wallet.coldkey ) diff --git a/tests/e2e_tests/subcommands/hyperparams/__init__.py b/tests/e2e_tests/subcommands/hyperparams/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/e2e_tests/subcommands/hyperparams/test_liquid_alpha.py b/tests/e2e_tests/subcommands/hyperparams/test_liquid_alpha.py new file mode 100644 index 0000000000..cf2522b788 --- /dev/null +++ b/tests/e2e_tests/subcommands/hyperparams/test_liquid_alpha.py @@ -0,0 +1,275 @@ +import bittensor +from bittensor.commands import ( + RegisterCommand, + StakeCommand, + RegisterSubnetworkCommand, + SubnetSudoCommand, +) +from tests.e2e_tests.utils import setup_wallet + +""" +Test the liquid alpha weights mechanism. + +Verify that: +* it can get enabled +* liquid alpha values cannot be set before the feature flag is set +* after feature flag, you can set alpha_high +* after feature flag, you can set alpha_low +""" + + +def test_liquid_alpha_enabled(local_chain, capsys): + # Register root as Alice + keypair, exec_command, wallet = setup_wallet("//Alice") + exec_command(RegisterSubnetworkCommand, ["s", "create"]) + + # hyperparameter values + alpha_values = "6553, 53083" + + # Verify subnet 1 created successfully + assert local_chain.query("SubtensorModule", "NetworksAdded", [1]).serialize() + + # Register a neuron to the subnet + exec_command( + RegisterCommand, + [ + "s", + "register", + "--netuid", + "1", + ], + ) + + # Stake to become to top neuron after the first epoch + exec_command( + StakeCommand, + [ + "stake", + "add", + "--amount", + "100000", + ], + ) + + # Assert liquid alpha disabled + subtensor = bittensor.subtensor(network="ws://localhost:9945") + assert ( + subtensor.get_subnet_hyperparameters(netuid=1).liquid_alpha_enabled is False + ), "Liquid alpha is enabled by default" + + # Attempt to set alpha high/low while disabled (should fail) + result = subtensor.set_hyperparameter( + wallet=wallet, + netuid=1, + parameter="alpha_values", + value=list(map(int, alpha_values.split(","))), + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert result is None + output = capsys.readouterr().out + assert ( + "❌ Failed: Subtensor returned `LiquidAlphaDisabled (Module)` error. This means: \n`Attempting to set alpha high/low while disabled`" + in output + ) + + # Enable Liquid Alpha + exec_command( + SubnetSudoCommand, + [ + "sudo", + "set", + "hyperparameters", + "--netuid", + "1", + "--wallet.name", + wallet.name, + "--param", + "liquid_alpha_enabled", + "--value", + "True", + "--wait_for_inclusion", + "True", + "--wait_for_finalization", + "True", + ], + ) + + assert subtensor.get_subnet_hyperparameters( + netuid=1 + ).liquid_alpha_enabled, "Failed to enable liquid alpha" + + output = capsys.readouterr().out + assert "✅ Hyper parameter liquid_alpha_enabled changed to True" in output + + exec_command( + SubnetSudoCommand, + [ + "sudo", + "set", + "hyperparameters", + "--netuid", + "1", + "--wallet.name", + wallet.name, + "--param", + "alpha_values", + "--value", + "87, 54099", + "--wait_for_inclusion", + "True", + "--wait_for_finalization", + "True", + ], + ) + 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" + + u16_max = 65535 + # Set alpha high too low + alpha_high_too_low = ( + u16_max * 4 // 5 + ) - 1 # One less than the minimum acceptable value + result = subtensor.set_hyperparameter( + wallet=wallet, + netuid=1, + parameter="alpha_values", + value=[6553, alpha_high_too_low], + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert result is None + output = capsys.readouterr().out + assert ( + "❌ Failed: Subtensor returned `AlphaHighTooLow (Module)` error. This means: \n`Alpha high is too low: alpha_high > 0.8`" + in output + ) + + alpha_high_too_high = u16_max + 1 # One more than the max acceptable value + try: + result = subtensor.set_hyperparameter( + wallet=wallet, + netuid=1, + parameter="alpha_values", + value=[6553, alpha_high_too_high], + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert result is None, "Expected not to be able to set alpha value above u16" + except Exception as e: + assert str(e) == "65536 out of range for u16", f"Unexpected error: {e}" + + # Set alpha low too low + alpha_low_too_low = 0 + result = subtensor.set_hyperparameter( + wallet=wallet, + netuid=1, + parameter="alpha_values", + value=[alpha_low_too_low, 53083], + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert result is None + output = capsys.readouterr().out + assert ( + "❌ Failed: Subtensor returned `AlphaLowOutOfRange (Module)` error. This means: \n`Alpha low is out of range: alpha_low > 0 && alpha_low < 0.8`" + in output + ) + + # Set alpha low too high + alpha_low_too_high = ( + u16_max * 4 // 5 + ) + 1 # One more than the maximum acceptable value + result = subtensor.set_hyperparameter( + wallet=wallet, + netuid=1, + parameter="alpha_values", + value=[alpha_low_too_high, 53083], + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert result is None + output = capsys.readouterr().out + assert ( + "❌ Failed: Subtensor returned `AlphaLowOutOfRange (Module)` error. This means: \n`Alpha low is out of range: alpha_low > 0 && alpha_low < 0.8`" + in output + ) + + exec_command( + SubnetSudoCommand, + [ + "sudo", + "set", + "hyperparameters", + "--netuid", + "1", + "--wallet.name", + wallet.name, + "--param", + "alpha_values", + "--value", + alpha_values, + "--wait_for_inclusion", + "True", + "--wait_for_finalization", + "True", + ], + ) + + 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" + + output = capsys.readouterr().out + assert "✅ Hyper parameter alpha_values changed to [6553.0, 53083.0]" in output + + # Disable Liquid Alpha + exec_command( + SubnetSudoCommand, + [ + "sudo", + "set", + "hyperparameters", + "--netuid", + "1", + "--wallet.name", + wallet.name, + "--param", + "liquid_alpha_enabled", + "--value", + "False", + "--wait_for_inclusion", + "True", + "--wait_for_finalization", + "True", + ], + ) + + assert ( + subtensor.get_subnet_hyperparameters(netuid=1).liquid_alpha_enabled is False + ), "Failed to disable liquid alpha" + + output = capsys.readouterr().out + assert "✅ Hyper parameter liquid_alpha_enabled changed to False" in output + + result = subtensor.set_hyperparameter( + wallet=wallet, + netuid=1, + parameter="alpha_values", + value=list(map(int, alpha_values.split(","))), + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert result is None + output = capsys.readouterr().out + assert ( + "❌ Failed: Subtensor returned `LiquidAlphaDisabled (Module)` error. This means: \n`Attempting to set alpha high/low while disabled`" + in output + ) diff --git a/tests/e2e_tests/subcommands/wallet/test_faucet.py b/tests/e2e_tests/subcommands/wallet/test_faucet.py index 0e647387b6..9f5fc6fc49 100644 --- a/tests/e2e_tests/subcommands/wallet/test_faucet.py +++ b/tests/e2e_tests/subcommands/wallet/test_faucet.py @@ -12,6 +12,7 @@ ) +@pytest.mark.skip @pytest.mark.parametrize("local_chain", [False], indirect=True) def test_faucet(local_chain): # Register root as Alice diff --git a/tests/integration_tests/test_cli_no_network.py b/tests/integration_tests/test_cli_no_network.py index cd9f89ee6a..e3a3d6a49c 100644 --- a/tests/integration_tests/test_cli_no_network.py +++ b/tests/integration_tests/test_cli_no_network.py @@ -1371,6 +1371,85 @@ def _test_value_parsing(parsed_value: bool, modified: str): _test_value_parsing(boolean_value, as_str.upper()) _test_value_parsing(boolean_value, as_str.lower()) + @patch("bittensor.wallet", new_callable=return_mock_wallet_factory) + def test_hyperparameter_allowed_values( + self, + mock_sub, + __, + ): + params = ["alpha_values"] + + def _test_value_parsing(param: str, value: str): + cli = bittensor.cli( + args=[ + "sudo", + "set", + "hyperparameters", + "--netuid", + "1", + "--param", + param, + "--value", + value, + "--wallet.name", + "mock", + ] + ) + should_raise_error = False + error_message = "" + + try: + alpha_low_str, alpha_high_str = value.strip("[]").split(",") + alpha_high = float(alpha_high_str) + alpha_low = float(alpha_low_str) + if alpha_high <= 52428 or alpha_high >= 65535: + should_raise_error = True + error_message = "between 52428 and 65535" + elif alpha_low < 0 or alpha_low > 52428: + should_raise_error = True + error_message = "between 0 and 52428" + except ValueError: + should_raise_error = True + error_message = "a number or a boolean" + except TypeError: + should_raise_error = True + error_message = "a number or a boolean" + + if isinstance(value, bool): + should_raise_error = True + error_message = "a number or a boolean" + + if should_raise_error: + with pytest.raises(ValueError) as exc_info: + cli.run() + assert ( + f"Hyperparameter {param} value is not within bounds. Value is {value} but must be {error_message}" + in str(exc_info.value) + ) + else: + cli.run() + _, kwargs = mock_sub.call_args + passed_config = kwargs["config"] + self.assertEqual(passed_config.param, param, msg="Incorrect param") + self.assertEqual( + passed_config.value, + value, + msg=f"Value argument not set correctly for {param}", + ) + + for param in params: + for value in [ + [0.8, 11], + [52429, 52428], + [52427, 53083], + [6553, 53083], + [-123, None], + [1, 0], + [True, "Some string"], + ]: + as_str = str(value).strip("[]") + _test_value_parsing(param, as_str) + @patch("bittensor.wallet", new_callable=return_mock_wallet_factory) def test_network_registration_allowed_parse_boolean_argument(self, mock_sub, __): param = "network_registration_allowed" From 659dbb49129deb9ebc985387ae414de4e7dae96f Mon Sep 17 00:00:00 2001 From: Gus Date: Thu, 11 Jul 2024 17:53:36 -0400 Subject: [PATCH 02/13] ci: adds release steps --- .circleci/config.yml | 86 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 84 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index a4d07de74b..9773b9c076 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -3,9 +3,20 @@ version: 2.1 orbs: python: circleci/python@2.1.1 python-lib: dialogue/python-lib@0.1.55 - # coveralls: coveralls/coveralls@1.0.6 jobs: + check-if-pr-is-draft: + docker: + - image: cimg/python:3.10 + steps: + - checkout + - run: + name: Install jq + command: sudo apt-get update && sudo apt-get install -y jq + - run: + name: Check if PR is a draft + command: .circleci/check_pr_status.sh + ruff: resource_class: small parameters: @@ -137,6 +148,7 @@ jobs: - store_artifacts: path: test-results + #- when: #condition: #equal: ["3.10.5", << parameters.python-version >> ] @@ -153,6 +165,7 @@ jobs: #CI_JOB_ID: $CIRCLE_NODE_INDEX #COVERALLS_PARALLEL: true + lint-and-type-check: resource_class: medium parallelism: 2 @@ -275,6 +288,59 @@ jobs: command: | ./scripts/release/release.sh --github-token ${GH_API_ACCESS_TOKEN} + publish-to-pypi: + docker: + - image: cimg/python:3.10 + steps: + - checkout + - run: + name: Install dependencies + command: | + python -m venv venv + . venv/bin/activate + pip install --upgrade pip + pip install wheel twine + - run: + name: Build package + command: | + . venv/bin/activate + python setup.py sdist bdist_wheel + - run: + name: Create .pypirc + command: | + echo -e "[distutils]" > ~/.pypirc + echo -e "index-servers =" >> ~/.pypirc + echo -e " pypi" >> ~/.pypirc + echo -e " bittensor\n" >> ~/.pypirc + echo -e "[bittensor]" >> ~/.pypirc + echo -e "repository = $BITTENSOR_REPOSITORY_URL" >> ~/.pypirc + echo -e "username = __token__" >> ~/.pypirc + echo -e "password = $PYPI_TOKEN" >> ~/.pypirc + - run: + name: Check if package version already exists + command: | + . venv/bin/activate + PACKAGE_NAME=$(python setup.py --name) + PACKAGE_VERSION=$(python setup.py --version) + if twine check dist/*; then + if pip install --index-url $BITTENSOR_REPOSITORY_URL $PACKAGE_NAME==$PACKAGE_VERSION; then + echo "!!!!!!!!!!!!!!!! Error: Version $PACKAGE_VERSION of $PACKAGE_NAME already exists on the bittensor repository !!!!!!!!!!!!!!!!" + circleci-agent step halt + else + echo "Version $PACKAGE_VERSION of $PACKAGE_NAME does not exist on the bittensor pypi. Proceeding with upload." + fi + else + echo "Error: Twine check failed." + circleci-agent step halt + fi + - run: + name: Publish to bittensor repository + command: | + . venv/bin/activate + if ! python -m twine upload --verbose --repository bittensor dist/*; then + echo "Error: Failed to upload package to bittensor repository" + exit 1 + fi workflows: compatibility_checks: @@ -291,12 +357,17 @@ workflows: pr-requirements: jobs: + - check-if-pr-is-draft - ruff: python-version: "3.9.13" + requires: + - check-if-pr-is-draft - build-and-test: matrix: parameters: python-version: ["3.9.13", "3.10.6", "3.11.4"] + requires: + - check-if-pr-is-draft - unit-tests-all-python-versions: requires: - build-and-test @@ -305,7 +376,7 @@ workflows: parameters: python-version: ["3.9.13", "3.10.6", "3.11.4"] requires: - - build-and-test + - check-if-pr-is-draft #- coveralls: #requires: #- build-and-test @@ -327,6 +398,17 @@ workflows: branches: only: - /^(release|hotfix)/.*/ + - hold-for-approval: + type: approval + filters: + branches: + only: /^release\/\d+\.\d+\.\d+$/ + - publish-to-pypi: + requires: + - hold-for-approval + filters: + branches: + only: /^release\/\d+\.\d+\.\d+$/ release-requirements: jobs: From f4de4e1e9d38e8ec93f4b9c26e034391172f20a3 Mon Sep 17 00:00:00 2001 From: Gus Date: Fri, 12 Jul 2024 12:44:24 -0400 Subject: [PATCH 03/13] ci: replace CircleCi with GH workflow --- .circleci/config.yml | 65 ------------------------------ .github/workflows/release.yaml | 72 ++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 65 deletions(-) create mode 100644 .github/workflows/release.yaml diff --git a/.circleci/config.yml b/.circleci/config.yml index 9773b9c076..90f49d54eb 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -288,60 +288,6 @@ jobs: command: | ./scripts/release/release.sh --github-token ${GH_API_ACCESS_TOKEN} - publish-to-pypi: - docker: - - image: cimg/python:3.10 - steps: - - checkout - - run: - name: Install dependencies - command: | - python -m venv venv - . venv/bin/activate - pip install --upgrade pip - pip install wheel twine - - run: - name: Build package - command: | - . venv/bin/activate - python setup.py sdist bdist_wheel - - run: - name: Create .pypirc - command: | - echo -e "[distutils]" > ~/.pypirc - echo -e "index-servers =" >> ~/.pypirc - echo -e " pypi" >> ~/.pypirc - echo -e " bittensor\n" >> ~/.pypirc - echo -e "[bittensor]" >> ~/.pypirc - echo -e "repository = $BITTENSOR_REPOSITORY_URL" >> ~/.pypirc - echo -e "username = __token__" >> ~/.pypirc - echo -e "password = $PYPI_TOKEN" >> ~/.pypirc - - run: - name: Check if package version already exists - command: | - . venv/bin/activate - PACKAGE_NAME=$(python setup.py --name) - PACKAGE_VERSION=$(python setup.py --version) - if twine check dist/*; then - if pip install --index-url $BITTENSOR_REPOSITORY_URL $PACKAGE_NAME==$PACKAGE_VERSION; then - echo "!!!!!!!!!!!!!!!! Error: Version $PACKAGE_VERSION of $PACKAGE_NAME already exists on the bittensor repository !!!!!!!!!!!!!!!!" - circleci-agent step halt - else - echo "Version $PACKAGE_VERSION of $PACKAGE_NAME does not exist on the bittensor pypi. Proceeding with upload." - fi - else - echo "Error: Twine check failed." - circleci-agent step halt - fi - - run: - name: Publish to bittensor repository - command: | - . venv/bin/activate - if ! python -m twine upload --verbose --repository bittensor dist/*; then - echo "Error: Failed to upload package to bittensor repository" - exit 1 - fi - workflows: compatibility_checks: jobs: @@ -398,17 +344,6 @@ workflows: branches: only: - /^(release|hotfix)/.*/ - - hold-for-approval: - type: approval - filters: - branches: - only: /^release\/\d+\.\d+\.\d+$/ - - publish-to-pypi: - requires: - - hold-for-approval - filters: - branches: - only: /^release\/\d+\.\d+\.\d+$/ release-requirements: jobs: diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000000..2cdfe5dfa0 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,72 @@ +name: Build and Publish Python Package + +on: + workflow_dispatch: + inputs: + version: + description: 'Version to release' + required: true + type: string + +jobs: + build: + name: Build Python distribution + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.10' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install build wheel twine + + - name: Build package + run: python setup.py sdist bdist_wheel + + - name: Check if package version already exists + run: | + PACKAGE_NAME=$(python setup.py --name) + PACKAGE_VERSION=${{ github.event.inputs.version }} + if twine check dist/*; then + if pip install $PACKAGE_NAME==$PACKAGE_VERSION; then + echo "Error: Version $PACKAGE_VERSION of $PACKAGE_NAME already exists on PyPI" + exit 1 + else + echo "Version $PACKAGE_VERSION of $PACKAGE_NAME does not exist on PyPI. Proceeding with upload." + fi + else + echo "Error: Twine check failed." + exit 1 + fi + + - name: Upload artifact + uses: actions/upload-artifact@v3 + with: + name: dist + path: dist/ + + approve-and-publish: + needs: build + runs-on: ubuntu-latest + environment: release + permissions: + contents: read + id-token: write + + steps: + - name: Download artifact + uses: actions/download-artifact@v3 + with: + name: dist + path: dist/ + + - name: Publish package distributions to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + verbose: true + print-hash: true \ No newline at end of file From f625f1fdf880e4b9c13bed1b299832c60f997cd0 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor <165814940+ibraheem-opentensor@users.noreply.github.com> Date: Fri, 12 Jul 2024 09:57:28 -0700 Subject: [PATCH 04/13] Re-adds check_coldkey_swap command (#2126) * Inital commit: Readds check-coldkey-swap * Adds check_coldkey_swap command --- bittensor/__init__.py | 31 ++++++ bittensor/chain_data.py | 60 +++++++++++ bittensor/cli.py | 2 + bittensor/commands/__init__.py | 1 + bittensor/commands/check_coldkey_swap.py | 126 +++++++++++++++++++++++ bittensor/subtensor.py | 40 +++++++ bittensor/utils/formatting.py | 14 +++ tests/unit_tests/test_subtensor.py | 38 +++++++ 8 files changed, 312 insertions(+) create mode 100644 bittensor/commands/check_coldkey_swap.py diff --git a/bittensor/__init__.py b/bittensor/__init__.py index 7cb37ef0d0..3c7ac7aa12 100644 --- a/bittensor/__init__.py +++ b/bittensor/__init__.py @@ -229,6 +229,37 @@ def debug(on: bool = True): "SubnetRegistrationRuntimeApi": { "methods": {"get_network_registration_cost": {"params": [], "type": "u64"}} }, + "ColdkeySwapRuntimeApi": { + "methods": { + "get_scheduled_coldkey_swap": { + "params": [ + { + "name": "coldkey_account_vec", + "type": "Vec", + }, + ], + "type": "Vec", + }, + "get_remaining_arbitration_period": { + "params": [ + { + "name": "coldkey_account_vec", + "type": "Vec", + }, + ], + "type": "Vec", + }, + "get_coldkey_swap_destinations": { + "params": [ + { + "name": "coldkey_account_vec", + "type": "Vec", + }, + ], + "type": "Vec", + }, + } + }, }, } diff --git a/bittensor/chain_data.py b/bittensor/chain_data.py index e62ad19621..55cdc47b6e 100644 --- a/bittensor/chain_data.py +++ b/bittensor/chain_data.py @@ -191,6 +191,14 @@ ["liquid_alpha_enabled", "bool"], ], }, + "ScheduledColdkeySwapInfo": { + "type": "struct", + "type_mapping": [ + ["old_coldkey", "AccountId"], + ["new_coldkey", "AccountId"], + ["arbitration_block", "Compact"], + ], + }, } } @@ -324,6 +332,7 @@ class ChainDataType(Enum): StakeInfo = 6 IPInfo = 7 SubnetHyperparameters = 8 + ScheduledColdkeySwapInfo = 9 def from_scale_encoding( @@ -1140,3 +1149,54 @@ class ProposalVoteData(TypedDict): ProposalCallData = GenericCall + + +@dataclass +class ScheduledColdkeySwapInfo: + """Dataclass for scheduled coldkey swap information.""" + + old_coldkey: str + new_coldkey: str + arbitration_block: int + + @classmethod + def fix_decoded_values(cls, decoded: Any) -> "ScheduledColdkeySwapInfo": + """Fixes the decoded values.""" + return cls( + old_coldkey=ss58_encode(decoded["old_coldkey"], bittensor.__ss58_format__), + new_coldkey=ss58_encode(decoded["new_coldkey"], bittensor.__ss58_format__), + arbitration_block=decoded["arbitration_block"], + ) + + @classmethod + def from_vec_u8(cls, vec_u8: List[int]) -> Optional["ScheduledColdkeySwapInfo"]: + """Returns a ScheduledColdkeySwapInfo object from a ``vec_u8``.""" + if len(vec_u8) == 0: + return None + + decoded = from_scale_encoding(vec_u8, ChainDataType.ScheduledColdkeySwapInfo) + if decoded is None: + return None + + return ScheduledColdkeySwapInfo.fix_decoded_values(decoded) + + @classmethod + def list_from_vec_u8(cls, vec_u8: List[int]) -> List["ScheduledColdkeySwapInfo"]: + """Returns a list of ScheduledColdkeySwapInfo objects from a ``vec_u8``.""" + decoded = from_scale_encoding( + vec_u8, ChainDataType.ScheduledColdkeySwapInfo, is_vec=True + ) + if decoded is None: + return [] + + return [ScheduledColdkeySwapInfo.fix_decoded_values(d) for d in decoded] + + @classmethod + def decode_account_id_list(cls, vec_u8: List[int]) -> Optional[List[str]]: + """Decodes a list of AccountIds from vec_u8.""" + decoded = from_scale_encoding(vec_u8, ChainDataType.AccountId, is_vec=True) + if decoded is None: + return None + return [ + ss58_encode(account_id, bittensor.__ss58_format__) for account_id in decoded + ] diff --git a/bittensor/cli.py b/bittensor/cli.py index 2322475734..6285241fa4 100644 --- a/bittensor/cli.py +++ b/bittensor/cli.py @@ -69,6 +69,7 @@ WalletCreateCommand, CommitWeightCommand, RevealWeightCommand, + CheckColdKeySwapCommand ) # Create a console instance for CLI display. @@ -157,6 +158,7 @@ "set_identity": SetIdentityCommand, "get_identity": GetIdentityCommand, "history": GetWalletHistoryCommand, + "check_coldkey_swap": CheckColdKeySwapCommand, }, }, "stake": { diff --git a/bittensor/commands/__init__.py b/bittensor/commands/__init__.py index 497fe4252b..514a081c41 100644 --- a/bittensor/commands/__init__.py +++ b/bittensor/commands/__init__.py @@ -121,3 +121,4 @@ RootSetSlashCommand, ) from .identity import GetIdentityCommand, SetIdentityCommand +from .check_coldkey_swap import CheckColdKeySwapCommand diff --git a/bittensor/commands/check_coldkey_swap.py b/bittensor/commands/check_coldkey_swap.py new file mode 100644 index 0000000000..dce2ca04f9 --- /dev/null +++ b/bittensor/commands/check_coldkey_swap.py @@ -0,0 +1,126 @@ +# The MIT License (MIT) +# Copyright © 2021 Yuma Rao + +# 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. + +import argparse + +from rich.prompt import Prompt + +import bittensor +from bittensor.utils.formatting import convert_blocks_to_time +from . import defaults + +console = bittensor.__console__ + + +def fetch_arbitration_stats(subtensor, wallet): + """ + Performs a check of the current arbitration data (if any), and displays it through the bittensor console. + """ + arbitration_check = len(subtensor.check_in_arbitration(wallet.coldkey.ss58_address)) + if arbitration_check == 0: + bittensor.__console__.print( + "[green]There has been no previous key swap initiated for your coldkey.[/green]" + ) + if arbitration_check == 1: + arbitration_remaining = subtensor.get_remaining_arbitration_period( + wallet.coldkey.ss58_address + ) + hours, minutes, seconds = convert_blocks_to_time(arbitration_remaining) + bittensor.__console__.print( + "[yellow]There has been 1 swap request made for this coldkey already." + " By adding another swap request, the key will enter arbitration." + f" Your key swap is scheduled for {hours} hours, {minutes} minutes, {seconds} seconds" + " from now.[/yellow]" + ) + if arbitration_check > 1: + bittensor.__console__.print( + f"[red]This coldkey is currently in arbitration with a total swaps of {arbitration_check}.[/red]" + ) + + +class CheckColdKeySwapCommand: + """ + Executes the ``check_coldkey_swap`` command to check swap status of a coldkey in the Bittensor network. + Usage: + Users need to specify the wallet they want to check the swap status of. + Example usage:: + btcli wallet check_coldkey_swap + Note: + This command is important for users who wish check if swap requests were made against their coldkey. + """ + + @staticmethod + def run(cli: "bittensor.cli"): + """ + Runs the check coldkey swap command. + Args: + cli (bittensor.cli): The CLI object containing configuration and command-line interface utilities. + """ + try: + config = cli.config.copy() + subtensor: "bittensor.subtensor" = bittensor.subtensor( + config=config, log_verbose=False + ) + CheckColdKeySwapCommand._run(cli, subtensor) + except Exception as e: + bittensor.logging.warning(f"Failed to get swap status: {e}") + finally: + if "subtensor" in locals(): + subtensor.close() + bittensor.logging.debug("closing subtensor connection") + + @staticmethod + def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): + """ + Internal method to check coldkey swap status. + Args: + cli (bittensor.cli): The CLI object containing configuration and command-line interface utilities. + subtensor (bittensor.subtensor): The subtensor object for blockchain interactions. + """ + config = cli.config.copy() + wallet = bittensor.wallet(config=config) + + fetch_arbitration_stats(subtensor, wallet) + + @classmethod + def check_config(cls, config: "bittensor.config"): + """ + Checks and prompts for necessary configuration settings. + Args: + config (bittensor.config): The configuration object. + Prompts the user for wallet name if not set in the config. + """ + if not config.is_set("wallet.name") and not config.no_prompt: + wallet_name: str = Prompt.ask( + "Enter wallet name", default=defaults.wallet.name + ) + config.wallet.name = str(wallet_name) + + @staticmethod + def add_args(command_parser: argparse.ArgumentParser): + """ + Adds arguments to the command parser. + Args: + command_parser (argparse.ArgumentParser): The command parser to add arguments to. + """ + swap_parser = command_parser.add_parser( + "check_coldkey_swap", + help="""Check the status of swap requests for a coldkey on the Bittensor network. + Adding more than one swap request will make the key go into arbitration mode.""", + ) + bittensor.wallet.add_args(swap_parser) + bittensor.subtensor.add_args(swap_parser) diff --git a/bittensor/subtensor.py b/bittensor/subtensor.py index 0fffa1cc7e..d1ebac9df4 100644 --- a/bittensor/subtensor.py +++ b/bittensor/subtensor.py @@ -2297,6 +2297,46 @@ def make_substrate_call_with_retry(): return make_substrate_call_with_retry() + ################## + # Coldkey Swap # + ################## + + def check_in_arbitration(self, ss58_address: str) -> int: + """ + Checks storage function to see if the provided coldkey is in arbitration. + If 0, `swap` has not been called on this key. If 1, swap has been called once, so + the key is not in arbitration. If >1, `swap` has been called with multiple destinations, and + the key is thus in arbitration. + """ + return self.query_module( + "SubtensorModule", "ColdkeySwapDestinations", params=[ss58_address] + ).decode() + + def get_remaining_arbitration_period( + self, coldkey_ss58: str, block: Optional[int] = None + ) -> Optional[int]: + """ + Retrieves the remaining arbitration period for a given coldkey. + Args: + coldkey_ss58 (str): The SS58 address of the coldkey. + block (Optional[int], optional): The block number to query. If None, uses the latest block. + Returns: + Optional[int]: The remaining arbitration period in blocks, or 0 if not found. + """ + arbitration_block = self.query_subtensor( + name="ColdkeyArbitrationBlock", + block=block, + params=[coldkey_ss58], + ) + + if block is None: + block = self.block + + if arbitration_block.value > block: + return arbitration_block.value - block + else: + return 0 + ########## # Senate # ########## diff --git a/bittensor/utils/formatting.py b/bittensor/utils/formatting.py index 1e93ce8340..f0a22d094d 100644 --- a/bittensor/utils/formatting.py +++ b/bittensor/utils/formatting.py @@ -20,3 +20,17 @@ def millify(n: int): ) return "{:.2f}{}".format(n / 10 ** (3 * millidx), millnames[millidx]) + + +def convert_blocks_to_time(blocks: int, block_time: int = 12) -> tuple[int, int, int]: + """ + Converts number of blocks into number of hours, minutes, seconds. + :param blocks: number of blocks + :param block_time: time per block, by default this is 12 + :return: tuple containing number of hours, number of minutes, number of seconds + """ + seconds = blocks * block_time + hours = seconds // 3600 + minutes = (seconds % 3600) // 60 + remaining_seconds = seconds % 60 + return hours, minutes, remaining_seconds diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index c3a295d078..731285c225 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -2277,3 +2277,41 @@ def test_get_delegate_take_no_data(mocker, subtensor): subtensor.query_subtensor.assert_called_once_with("Delegates", block, [hotkey_ss58]) spy_u16_normalized_float.assert_not_called() assert result is None + + +def test_get_remaining_arbitration_period(subtensor, mocker): + """Tests successful retrieval of total stake for hotkey.""" + # Prep + subtensor.query_subtensor = mocker.MagicMock(return_value=mocker.MagicMock(value=0)) + fake_ss58_address = "12bzRJfh7arnnfPPUZHeJUaE62QLEwhK48QnH9LXeK2m1iZU" + + # Call + result = subtensor.get_remaining_arbitration_period(coldkey_ss58=fake_ss58_address) + + # Assertions + subtensor.query_subtensor.assert_called_once_with( + name="ColdkeyArbitrationBlock", block=None, params=[fake_ss58_address] + ) + # if we change the methods logic in the future we have to be make sure the returned type is correct + assert result == 0 + + +def test_get_remaining_arbitration_period_happy(subtensor, mocker): + """Tests successful retrieval of total stake for hotkey.""" + # Prep + subtensor.query_subtensor = mocker.MagicMock( + return_value=mocker.MagicMock(value=2000) + ) + fake_ss58_address = "12bzRJfh7arnnfPPUZHeJUaE62QLEwhK48QnH9LXeK2m1iZU" + + # Call + result = subtensor.get_remaining_arbitration_period( + coldkey_ss58=fake_ss58_address, block=200 + ) + + # Assertions + subtensor.query_subtensor.assert_called_once_with( + name="ColdkeyArbitrationBlock", block=200, params=[fake_ss58_address] + ) + # if we change the methods logic in the future we have to be make sure the returned type is correct + assert result == 1800 # 2000 - 200 From 52b810a222defde68f97f1235eaf4b2a0245309e Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Fri, 12 Jul 2024 10:23:15 -0700 Subject: [PATCH 05/13] Bumps version to 7.3.0 --- VERSION | 2 +- bittensor/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/VERSION b/VERSION index 4b49d9bb63..8b23b8d47c 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -7.2.0 \ No newline at end of file +7.3.0 \ No newline at end of file diff --git a/bittensor/__init__.py b/bittensor/__init__.py index 3c7ac7aa12..c872fdf973 100644 --- a/bittensor/__init__.py +++ b/bittensor/__init__.py @@ -40,7 +40,7 @@ # Bittensor code and protocol version. -__version__ = "7.2.0" +__version__ = "7.3.0" _version_split = __version__.split(".") __version_info__ = tuple(int(part) for part in _version_split) From 50857b14bea6378655b2b3376739dd4867292d8a Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Fri, 12 Jul 2024 10:35:15 -0700 Subject: [PATCH 06/13] Adds check_pr_status --- .circleci/check_pr_status.sh | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 .circleci/check_pr_status.sh diff --git a/.circleci/check_pr_status.sh b/.circleci/check_pr_status.sh new file mode 100644 index 0000000000..4b31a29698 --- /dev/null +++ b/.circleci/check_pr_status.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +# Extract the repository owner +REPO_OWNER=$(echo $CIRCLE_PULL_REQUEST | awk -F'/' '{print $(NF-3)}') + +# Extract the repository name +REPO_NAME=$(echo $CIRCLE_PULL_REQUEST | awk -F'/' '{print $(NF-2)}') + +# Extract the pull request number +PR_NUMBER=$(echo $CIRCLE_PULL_REQUEST | awk -F'/' '{print $NF}') + + +PR_DETAILS=$(curl -s \ + "https://github.com/gitapi/repos/$REPO_OWNER/$REPO_NAME/pulls/$PR_NUMBER") + + +IS_DRAFT=$(echo "$PR_DETAILS" | jq -r .draft) +echo $IS_DRAFT + +if [ "$IS_DRAFT" == "true" ]; then + echo "This PR is a draft. Skipping the workflow." + exit 1 +else + echo "This PR is not a draft. Proceeding with the workflow." + exit 0 +fi From dfd723ba0a2eae29754ba9c44de5b65262fedc04 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Fri, 12 Jul 2024 20:04:15 +0200 Subject: [PATCH 07/13] Make check_pr_status.sh executable --- .circleci/check_pr_status.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 .circleci/check_pr_status.sh diff --git a/.circleci/check_pr_status.sh b/.circleci/check_pr_status.sh old mode 100644 new mode 100755 From e4e9cafb49e54f5872489d27b3df160ac38e9e01 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Fri, 12 Jul 2024 11:12:47 -0700 Subject: [PATCH 08/13] Fixes ruff checks --- bittensor/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor/cli.py b/bittensor/cli.py index 6285241fa4..4a7a47775e 100644 --- a/bittensor/cli.py +++ b/bittensor/cli.py @@ -69,7 +69,7 @@ WalletCreateCommand, CommitWeightCommand, RevealWeightCommand, - CheckColdKeySwapCommand + CheckColdKeySwapCommand, ) # Create a console instance for CLI display. From abaabd0e9145c674778079a7371ce8e5b052d29e Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Fri, 12 Jul 2024 12:02:53 -0700 Subject: [PATCH 09/13] Updated e2e yaml file --- .github/workflows/e2e-subtensor-tests.yaml | 46 ++++++++++++++++------ 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/.github/workflows/e2e-subtensor-tests.yaml b/.github/workflows/e2e-subtensor-tests.yaml index 1d3d6bb5ce..969423db01 100644 --- a/.github/workflows/e2e-subtensor-tests.yaml +++ b/.github/workflows/e2e-subtensor-tests.yaml @@ -5,18 +5,13 @@ concurrency: cancel-in-progress: true on: - ## Run automatically for all PRs against main, regardless of what the changes are - ## to be safe and so we can more easily force re-run the CI when github is being - ## weird by using a blank commit push: branches: [main, development, staging] - ## - # Run automatically for PRs against default/main branch if Rust files change pull_request: branches: [main, development, staging] + types: [ opened, synchronize, reopened, ready_for_review ] - ## Allow running workflow manually from the Actions tab workflow_dispatch: inputs: verbose: @@ -26,24 +21,42 @@ on: env: CARGO_TERM_COLOR: always - VERBOSE: ${{ github.events.input.verbose }} + VERBOSE: ${{ github.event.inputs.verbose }} +# job to run tests in parallel jobs: + # Job to find all test files + find-tests: + runs-on: ubuntu-latest + if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }} + outputs: + test-files: ${{ steps.get-tests.outputs.test-files }} + steps: + - name: Check-out repository under $GITHUB_WORKSPACE + uses: actions/checkout@v2 + + - name: Find test files + id: get-tests + run: | + test_files=$(find tests/e2e_tests -name "test*.py" | jq -R -s -c 'split("\n") | map(select(. != ""))') + echo "::set-output name=test-files::$test_files" + shell: bash + + # Job to run tests in parallel run: + needs: find-tests runs-on: SubtensorCI strategy: + fail-fast: false # Allow other matrix jobs to run even if this job fails + max-parallel: 8 # Set the maximum number of parallel jobs matrix: rust-branch: - nightly-2024-03-05 rust-target: - x86_64-unknown-linux-gnu - # - x86_64-apple-darwin os: - ubuntu-latest - # - macos-latest - include: - - os: ubuntu-latest - # - os: macos-latest + test-file: ${{ fromJson(needs.find-tests.outputs.test-files) }} env: RELEASE_NAME: development RUSTV: ${{ matrix.rust-branch }} @@ -81,4 +94,11 @@ jobs: - name: Run tests run: | python3 -m pip install -e .[dev] pytest - LOCALNET_SH_PATH="${{ github.workspace }}/subtensor/scripts/localnet.sh" pytest tests/e2e_tests/ -s + LOCALNET_SH_PATH="${{ github.workspace }}/subtensor/scripts/localnet.sh" pytest ${{ matrix.test-file }} -s + + - name: Retry failed tests + if: failure() + run: | + sleep 10 + python3 -m pip install -e .[dev] pytest + LOCALNET_SH_PATH="${{ github.workspace }}/subtensor/scripts/localnet.sh" pytest ${{ matrix.test-file }} -s From 09ea2d71cca3bfb9f56c8b9c216ae373b0424213 Mon Sep 17 00:00:00 2001 From: Gus Date: Fri, 12 Jul 2024 15:50:36 -0400 Subject: [PATCH 10/13] chore: update changelog --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7cb2964f6d..e4a0ba068b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 7.3.0 / 2024-07-12 + +## What's Changed +* Liquid Alpha by @opendansor & @gus-opentensor in https://github.com/opentensor/bittensor/pull/2012 +* check_coldkey_swap by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2126 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v7.2.0...v7.3.0 + + ## 7.2.0 / 2024-06-12 ## What's Changed From ff967e14aee69a579b11b31d36696b2b63c9055e Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 12 Jul 2024 13:39:00 -0700 Subject: [PATCH 11/13] update e2e test --- tests/e2e_tests/multistep/test_axon.py | 20 ++--- tests/e2e_tests/multistep/test_dendrite.py | 47 ++--------- tests/e2e_tests/multistep/test_incentive.py | 79 ++----------------- .../e2e_tests/multistep/test_last_tx_block.py | 4 +- .../delegation/test_set_delegate_take.py | 4 +- .../subcommands/wallet/test_transfer.py | 2 +- .../weights/test_commit_weights.py | 8 +- tests/e2e_tests/utils.py | 23 +++++- 8 files changed, 45 insertions(+), 142 deletions(-) diff --git a/tests/e2e_tests/multistep/test_axon.py b/tests/e2e_tests/multistep/test_axon.py index f23fb4da7b..35514a680a 100644 --- a/tests/e2e_tests/multistep/test_axon.py +++ b/tests/e2e_tests/multistep/test_axon.py @@ -32,7 +32,7 @@ @pytest.mark.asyncio async def test_axon(local_chain): # Register root as Alice - alice_keypair, exec_command, wallet_path = setup_wallet("//Alice") + alice_keypair, exec_command, wallet = setup_wallet("//Alice") exec_command(RegisterSubnetworkCommand, ["s", "create"]) # Verify subnet 1 created successfully @@ -46,17 +46,6 @@ async def test_axon(local_chain): "register", "--netuid", "1", - "--wallet.name", - "default", - "--wallet.hotkey", - "default", - "--wallet.path", - wallet_path, - "--subtensor.network", - "local", - "--subtensor.chain_endpoint", - "ws://localhost:9945", - "--no_prompt", ], ) @@ -86,19 +75,20 @@ async def test_axon(local_chain): "--subtensor.chain_endpoint", "ws://localhost:9945", "--wallet.path", - wallet_path, + wallet.path, "--wallet.name", - "default", + wallet.name, "--wallet.hotkey", "default", ] ) - await asyncio.create_subprocess_shell( + axon_process = await asyncio.create_subprocess_shell( cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) + await asyncio.sleep( 5 ) # wait for 5 seconds for the metagraph to refresh with latest data diff --git a/tests/e2e_tests/multistep/test_dendrite.py b/tests/e2e_tests/multistep/test_dendrite.py index 6abde7464d..deb37eb133 100644 --- a/tests/e2e_tests/multistep/test_dendrite.py +++ b/tests/e2e_tests/multistep/test_dendrite.py @@ -1,7 +1,6 @@ import asyncio import logging import sys -import time import pytest @@ -17,8 +16,10 @@ setup_wallet, template_path, repo_name, + wait_epoch, ) + logging.basicConfig(level=logging.INFO) """ @@ -35,7 +36,7 @@ @pytest.mark.asyncio async def test_dendrite(local_chain): # Register root as Alice - the subnet owner - alice_keypair, exec_command, wallet_path = setup_wallet("//Alice") + alice_keypair, exec_command, wallet = setup_wallet("//Alice") exec_command(RegisterSubnetworkCommand, ["s", "create"]) # Verify subnet 1 created successfully @@ -51,15 +52,6 @@ async def test_dendrite(local_chain): "register", "--netuid", "1", - "--wallet.name", - "default", - "--wallet.hotkey", - "default", - "--subtensor.network", - "local", - "--subtensor.chain_endpoint", - "ws://localhost:9945", - "--no_prompt", ], ) @@ -111,20 +103,21 @@ async def test_dendrite(local_chain): "--subtensor.chain_endpoint", "ws://localhost:9945", "--wallet.path", - wallet_path, + wallet.path, "--wallet.name", - "default", + wallet.name, "--wallet.hotkey", "default", ] ) # run validator in the background - await asyncio.create_subprocess_shell( + dendrite_process = await asyncio.create_subprocess_shell( cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) + await asyncio.sleep( 5 ) # wait for 5 seconds for the metagraph and subtensor to refresh with latest data @@ -137,12 +130,6 @@ async def test_dendrite(local_chain): "register", "--netuid", "1", - "--wallet.name", - "default", - "--wallet.hotkey", - "default", - "--subtensor.chain_endpoint", - "ws://localhost:9945", ], ) @@ -155,28 +142,10 @@ async def test_dendrite(local_chain): "1", "--increase", "1", - "--wallet.name", - "default", - "--wallet.hotkey", - "default", - "--subtensor.chain_endpoint", - "ws://localhost:9945", ], ) # get current block, wait until 360 blocks pass (subnet tempo) - interval = 360 - current_block = subtensor.get_current_block() - next_tempo_block_start = (current_block - (current_block % interval)) + interval - while current_block < next_tempo_block_start: - time.sleep(1) # Wait for 1 second before checking the block number again - current_block = subtensor.get_current_block() - if current_block % 10 == 0: - print( - f"Current Block: {current_block} Next tempo at: {next_tempo_block_start}" - ) - logging.info( - f"Current Block: {current_block} Next tempo at: {next_tempo_block_start}" - ) + wait_epoch(360, subtensor) # refresh metagraph metagraph = bittensor.metagraph(netuid=1, network="ws://localhost:9945") diff --git a/tests/e2e_tests/multistep/test_incentive.py b/tests/e2e_tests/multistep/test_incentive.py index ea5809dd7f..d1b4634653 100644 --- a/tests/e2e_tests/multistep/test_incentive.py +++ b/tests/e2e_tests/multistep/test_incentive.py @@ -1,7 +1,6 @@ import asyncio import logging import sys -import time import pytest @@ -17,6 +16,7 @@ setup_wallet, template_path, repo_name, + wait_epoch, ) logging.basicConfig(level=logging.INFO) @@ -44,13 +44,13 @@ @pytest.mark.asyncio async def test_incentive(local_chain): # Register root as Alice - the subnet owner and validator - alice_keypair, alice_exec_command, alice_wallet_path = setup_wallet("//Alice") + alice_keypair, alice_exec_command, alice_wallet = setup_wallet("//Alice") alice_exec_command(RegisterSubnetworkCommand, ["s", "create"]) # Verify subnet 1 created successfully assert local_chain.query("SubtensorModule", "NetworksAdded", [1]).serialize() # Register Bob as miner - bob_keypair, bob_exec_command, bob_wallet_path = setup_wallet("//Bob") + bob_keypair, bob_exec_command, bob_wallet = setup_wallet("//Bob") # Register Alice as neuron to the subnet alice_exec_command( @@ -60,17 +60,6 @@ async def test_incentive(local_chain): "register", "--netuid", "1", - "--wallet.name", - "default", - "--wallet.hotkey", - "default", - "--wallet.path", - alice_wallet_path, - "--subtensor.network", - "local", - "--subtensor.chain_endpoint", - "ws://localhost:9945", - "--no_prompt", ], ) @@ -82,15 +71,6 @@ async def test_incentive(local_chain): "register", "--netuid", "1", - "--wallet.name", - "default", - "--wallet.hotkey", - "default", - "--subtensor.network", - "local", - "--subtensor.chain_endpoint", - "ws://localhost:9945", - "--no_prompt", ], ) @@ -122,9 +102,9 @@ async def test_incentive(local_chain): "--subtensor.chain_endpoint", "ws://localhost:9945", "--wallet.path", - bob_wallet_path, + bob_wallet.path, "--wallet.name", - "default", + bob_wallet.name, "--wallet.hotkey", "default", "--logging.trace", @@ -137,21 +117,6 @@ async def test_incentive(local_chain): stderr=asyncio.subprocess.PIPE, ) - # Function to write output to the log file - async def miner_write_output(stream): - log_file = "miner.log" - with open(log_file, "a") as f: - while True: - line = await stream.readline() - if not line: - break - f.write(line.decode()) - f.flush() - - # Create tasks to read stdout and stderr concurrently - asyncio.create_task(miner_write_output(miner_process.stdout)) - asyncio.create_task(miner_write_output(miner_process.stderr)) - await asyncio.sleep( 5 ) # wait for 5 seconds for the metagraph to refresh with latest data @@ -169,9 +134,9 @@ async def miner_write_output(stream): "--subtensor.chain_endpoint", "ws://localhost:9945", "--wallet.path", - alice_wallet_path, + alice_wallet.path, "--wallet.name", - "default", + alice_wallet.name, "--wallet.hotkey", "default", "--logging.trace", @@ -185,21 +150,6 @@ async def miner_write_output(stream): stderr=asyncio.subprocess.PIPE, ) - # Function to write output to the log file - async def validator_write_output(stream): - log_file = "validator.log" - with open(log_file, "a") as f: - while True: - line = await stream.readline() - if not line: - break - f.write(line.decode()) - f.flush() - - # Create tasks to read stdout and stderr concurrently - asyncio.create_task(validator_write_output(validator_process.stdout)) - asyncio.create_task(validator_write_output(validator_process.stderr)) - await asyncio.sleep( 5 ) # wait for 5 seconds for the metagraph and subtensor to refresh with latest data @@ -289,18 +239,3 @@ async def validator_write_output(stream): assert alice_neuron.dividends == 1 assert alice_neuron.stake.tao == 10_000.0 assert alice_neuron.validator_trust == 1 - - -def wait_epoch(interval, subtensor): - current_block = subtensor.get_current_block() - next_tempo_block_start = (current_block - (current_block % interval)) + interval - while current_block < next_tempo_block_start: - time.sleep(1) # Wait for 1 second before checking the block number again - current_block = subtensor.get_current_block() - if current_block % 10 == 0: - print( - f"Current Block: {current_block} Next tempo at: {next_tempo_block_start}" - ) - logging.info( - f"Current Block: {current_block} Next tempo at: {next_tempo_block_start}" - ) diff --git a/tests/e2e_tests/multistep/test_last_tx_block.py b/tests/e2e_tests/multistep/test_last_tx_block.py index b97d54f8fa..5bc4759212 100644 --- a/tests/e2e_tests/multistep/test_last_tx_block.py +++ b/tests/e2e_tests/multistep/test_last_tx_block.py @@ -9,7 +9,7 @@ # https://discord.com/channels/799672011265015819/1176889736636407808/1236057424134144152 def test_takes(local_chain): # Register root as Alice - keypair, exec_command, wallet_path = setup_wallet("//Alice") + keypair, exec_command, wallet = setup_wallet("//Alice") exec_command(RootRegisterCommand, ["root", "register"]) # Create subnet 1 and verify created successfully @@ -21,7 +21,7 @@ def test_takes(local_chain): assert local_chain.query("SubtensorModule", "NetworksAdded", [1]).serialize() # Register and nominate Bob - keypair, exec_command, wallet_path = setup_wallet("//Bob") + keypair, exec_command, wallet = setup_wallet("//Bob") assert ( local_chain.query( "SubtensorModule", "LastTxBlock", [keypair.ss58_address] diff --git a/tests/e2e_tests/subcommands/delegation/test_set_delegate_take.py b/tests/e2e_tests/subcommands/delegation/test_set_delegate_take.py index cc7b1b5744..cefb150f70 100644 --- a/tests/e2e_tests/subcommands/delegation/test_set_delegate_take.py +++ b/tests/e2e_tests/subcommands/delegation/test_set_delegate_take.py @@ -8,7 +8,7 @@ def test_set_delegate_increase_take(local_chain): # Register root as Alice - keypair, exec_command, wallet_path = setup_wallet("//Alice") + keypair, exec_command, wallet = setup_wallet("//Alice") exec_command(RootRegisterCommand, ["root", "register"]) # Create subnet 1 and verify created successfully @@ -20,7 +20,7 @@ def test_set_delegate_increase_take(local_chain): assert local_chain.query("SubtensorModule", "NetworksAdded", [1]).serialize() # Register and nominate Bob - keypair, exec_command, wallet_path = setup_wallet("//Bob") + keypair, exec_command, wallet = setup_wallet("//Bob") assert ( local_chain.query( "SubtensorModule", "LastTxBlock", [keypair.ss58_address] diff --git a/tests/e2e_tests/subcommands/wallet/test_transfer.py b/tests/e2e_tests/subcommands/wallet/test_transfer.py index 5b491b3f0d..5b22b4e778 100644 --- a/tests/e2e_tests/subcommands/wallet/test_transfer.py +++ b/tests/e2e_tests/subcommands/wallet/test_transfer.py @@ -5,7 +5,7 @@ # Example test using the local_chain fixture def test_transfer(local_chain): - keypair, exec_command, wallet_path = setup_wallet("//Alice") + keypair, exec_command, wallet = setup_wallet("//Alice") acc_before = local_chain.query("System", "Account", [keypair.ss58_address]) exec_command( diff --git a/tests/e2e_tests/subcommands/weights/test_commit_weights.py b/tests/e2e_tests/subcommands/weights/test_commit_weights.py index 4c719b0ebd..f04f4f7ab3 100644 --- a/tests/e2e_tests/subcommands/weights/test_commit_weights.py +++ b/tests/e2e_tests/subcommands/weights/test_commit_weights.py @@ -30,7 +30,7 @@ def test_commit_and_reveal_weights(local_chain): # Register root as Alice - keypair, exec_command, wallet_path = setup_wallet("//Alice") + keypair, exec_command, wallet = setup_wallet("//Alice") exec_command(RegisterSubnetworkCommand, ["s", "create"]) @@ -48,12 +48,6 @@ def test_commit_and_reveal_weights(local_chain): ["s", "register", "--netuid", "1", "--wallet.path", "/tmp/btcli-wallet"], ) - # Create a test wallet and set the coldkey, coldkeypub, and hotkey - wallet = bittensor.wallet(path="/tmp/btcli-wallet") - wallet.set_coldkey(keypair=keypair, encrypt=False, overwrite=True) - wallet.set_coldkeypub(keypair=keypair, encrypt=False, overwrite=True) - wallet.set_hotkey(keypair=keypair, encrypt=False, overwrite=True) - # Stake to become to top neuron after the first epoch exec_command( StakeCommand, diff --git a/tests/e2e_tests/utils.py b/tests/e2e_tests/utils.py index 4b485e3bd9..d2af4e3a52 100644 --- a/tests/e2e_tests/utils.py +++ b/tests/e2e_tests/utils.py @@ -1,13 +1,13 @@ +import logging import os import shutil import subprocess import sys - +import time from typing import List -from bittensor import Keypair - import bittensor +from bittensor import Keypair template_path = os.getcwd() + "/neurons/" repo_name = "templates repository" @@ -39,7 +39,7 @@ def exec_command(command, extra_args: List[str]): cli_instance = bittensor.cli(config) command.run(cli_instance) - return keypair, exec_command, wallet_path + return keypair, exec_command, wallet def clone_or_update_templates(): @@ -74,3 +74,18 @@ def uninstall_templates(install_dir): ) # delete everything in directory shutil.rmtree(install_dir) + + +def wait_epoch(interval, subtensor): + current_block = subtensor.get_current_block() + next_tempo_block_start = (current_block - (current_block % interval)) + interval + while current_block < next_tempo_block_start: + time.sleep(1) # Wait for 1 second before checking the block number again + current_block = subtensor.get_current_block() + if current_block % 10 == 0: + print( + f"Current Block: {current_block} Next tempo at: {next_tempo_block_start}" + ) + logging.info( + f"Current Block: {current_block} Next tempo at: {next_tempo_block_start}" + ) \ No newline at end of file From 23397db89ae827155c56e1e63ce1c03c48ba4568 Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 12 Jul 2024 13:40:51 -0700 Subject: [PATCH 12/13] ruff --- tests/e2e_tests/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e_tests/utils.py b/tests/e2e_tests/utils.py index d2af4e3a52..ce7b0bc092 100644 --- a/tests/e2e_tests/utils.py +++ b/tests/e2e_tests/utils.py @@ -88,4 +88,4 @@ def wait_epoch(interval, subtensor): ) logging.info( f"Current Block: {current_block} Next tempo at: {next_tempo_block_start}" - ) \ No newline at end of file + ) From 53510860969b1909957d46631e5e318d0ce0456e Mon Sep 17 00:00:00 2001 From: Gus Date: Fri, 12 Jul 2024 16:59:16 -0400 Subject: [PATCH 13/13] ci: adding release.yml --- .github/workflows/release.yml | 72 +++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000000..2cdfe5dfa0 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,72 @@ +name: Build and Publish Python Package + +on: + workflow_dispatch: + inputs: + version: + description: 'Version to release' + required: true + type: string + +jobs: + build: + name: Build Python distribution + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.10' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install build wheel twine + + - name: Build package + run: python setup.py sdist bdist_wheel + + - name: Check if package version already exists + run: | + PACKAGE_NAME=$(python setup.py --name) + PACKAGE_VERSION=${{ github.event.inputs.version }} + if twine check dist/*; then + if pip install $PACKAGE_NAME==$PACKAGE_VERSION; then + echo "Error: Version $PACKAGE_VERSION of $PACKAGE_NAME already exists on PyPI" + exit 1 + else + echo "Version $PACKAGE_VERSION of $PACKAGE_NAME does not exist on PyPI. Proceeding with upload." + fi + else + echo "Error: Twine check failed." + exit 1 + fi + + - name: Upload artifact + uses: actions/upload-artifact@v3 + with: + name: dist + path: dist/ + + approve-and-publish: + needs: build + runs-on: ubuntu-latest + environment: release + permissions: + contents: read + id-token: write + + steps: + - name: Download artifact + uses: actions/download-artifact@v3 + with: + name: dist + path: dist/ + + - name: Publish package distributions to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + verbose: true + print-hash: true \ No newline at end of file