diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 6b2eaa0333..0000000000 --- a/.flake8 +++ /dev/null @@ -1,4 +0,0 @@ -[flake8] -max-line-length = 120 -exclude = .git,__pycache__, __init__.py, docs/source/conf.py,old,build,dist,venv,.venv,.tox -select = E9,F63,F7,F82,F401 diff --git a/.github/workflows/_run-e2e-single.yaml b/.github/workflows/_run-e2e-single.yaml index d86da50d2d..96a14441cc 100644 --- a/.github/workflows/_run-e2e-single.yaml +++ b/.github/workflows/_run-e2e-single.yaml @@ -40,22 +40,22 @@ jobs: steps: - name: Check-out repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: ref: ${{ inputs.ref != '' && inputs.ref || github.ref }} - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Install uv - uses: astral-sh/setup-uv@v4 + uses: astral-sh/setup-uv@v8.0.0 with: enable-cache: false - name: Cache uv and venv - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: | ~/.cache/uv @@ -70,7 +70,7 @@ jobs: run: uv sync --extra dev --dev - name: Download Cached Docker Image - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: ${{ inputs.artifact-name }} diff --git a/.github/workflows/changelog-checker.yml b/.github/workflows/changelog-checker.yml index aae9580609..a5038e860b 100644 --- a/.github/workflows/changelog-checker.yml +++ b/.github/workflows/changelog-checker.yml @@ -14,11 +14,11 @@ jobs: if: startsWith(github.head_ref, 'release/') || startsWith(github.head_ref, 'hotfix/') runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: tj-actions/changed-files@v46 + - uses: actions/checkout@v6 + - uses: tj-actions/changed-files@v47.0.5 id: changed - name: Ensure CHANGELOG.md updated if: contains(steps.changed.outputs.all_changed_files, 'CHANGELOG.md') == false - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: script: core.setFailed('CHANGELOG.md must be updated.') diff --git a/.github/workflows/compatibility.yml b/.github/workflows/compatibility.yml index eb333b4126..b192f7834d 100644 --- a/.github/workflows/compatibility.yml +++ b/.github/workflows/compatibility.yml @@ -13,7 +13,7 @@ jobs: outputs: python-versions: ${{ steps.read-versions.outputs.versions }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - id: read-versions run: | versions=$(cat .github/supported-python-versions.json) @@ -29,8 +29,8 @@ jobs: python-version: ${{ fromJson(needs.read-python-versions.outputs.python-versions) }} steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} diff --git a/.github/workflows/docker_release.yml b/.github/workflows/docker_release.yml index 58f94c0571..0f0962a714 100644 --- a/.github/workflows/docker_release.yml +++ b/.github/workflows/docker_release.yml @@ -18,7 +18,7 @@ jobs: steps: - name: Check out code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Install cosign uses: sigstore/cosign-installer@v3 diff --git a/.github/workflows/e2e-subtensor-tests.yaml b/.github/workflows/e2e-subtensor-tests.yaml index f19bc2d454..c904cd18d5 100644 --- a/.github/workflows/e2e-subtensor-tests.yaml +++ b/.github/workflows/e2e-subtensor-tests.yaml @@ -42,22 +42,22 @@ jobs: test-files: ${{ steps.get-tests.outputs.test-files }} steps: - name: Check-out repository under $GITHUB_WORKSPACE - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.10' - name: Install uv - uses: astral-sh/setup-uv@v4 + uses: astral-sh/setup-uv@v8.0.0 with: enable-cache: false cache-dependency-glob: '**/pyproject.toml' ignore-nothing-to-cache: true - name: Cache uv and venv - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: | ~/.cache/uv @@ -92,7 +92,7 @@ jobs: outputs: python-versions: ${{ steps.read-versions.outputs.versions }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - id: read-versions run: | versions=$(cat .github/supported-python-versions.json) @@ -201,7 +201,7 @@ jobs: run: docker save -o subtensor-localnet.tar ${{ steps.set-image.outputs.image }} - name: Upload Docker Image as Artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: subtensor-localnet path: subtensor-localnet.tar diff --git a/.github/workflows/monitor_requirements_size_master.yml b/.github/workflows/monitor_requirements_size_master.yml index af87292257..bb879689c1 100644 --- a/.github/workflows/monitor_requirements_size_master.yml +++ b/.github/workflows/monitor_requirements_size_master.yml @@ -20,7 +20,7 @@ jobs: outputs: python-versions: ${{ steps.read-versions.outputs.versions }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - id: read-versions run: | versions=$(cat .github/supported-python-versions.json) @@ -40,8 +40,8 @@ jobs: py313: ${{ steps.set-output.outputs.py313 }} steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} @@ -70,7 +70,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Post venv size summary to PR - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | diff --git a/.github/workflows/flake8-and-mypy.yml b/.github/workflows/mypy.yml similarity index 83% rename from .github/workflows/flake8-and-mypy.yml rename to .github/workflows/mypy.yml index 9638d08ff8..ee436db4ca 100644 --- a/.github/workflows/flake8-and-mypy.yml +++ b/.github/workflows/mypy.yml @@ -1,4 +1,4 @@ -name: Flake8 and Mypy - linters check +name: Mypy check permissions: contents: read @@ -12,7 +12,7 @@ jobs: outputs: python-versions: ${{ steps.read-versions.outputs.versions }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - id: read-versions run: | versions=$(cat .github/supported-python-versions.json) @@ -31,20 +31,20 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Install uv - uses: astral-sh/setup-uv@v4 + uses: astral-sh/setup-uv@v8.0.0 with: enable-cache: false - name: Cache uv and .venv - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: | ~/.cache/uv @@ -55,8 +55,5 @@ jobs: - name: Sync dev deps run: uv sync --extra dev --dev - - name: Flake8 - run: uv run flake8 bittensor/ --count - - name: Mypy run: uv run mypy --ignore-missing-imports bittensor/ diff --git a/.github/workflows/nightly-e2e-tests-subtensor-main.yml b/.github/workflows/nightly-e2e-tests-subtensor-main.yml index d8c8d44419..1cbd66e528 100644 --- a/.github/workflows/nightly-e2e-tests-subtensor-main.yml +++ b/.github/workflows/nightly-e2e-tests-subtensor-main.yml @@ -33,22 +33,22 @@ jobs: test-files: ${{ steps.get-tests.outputs.test-files }} steps: - name: Check-out repository under $GITHUB_WORKSPACE - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.10' - name: Install uv - uses: astral-sh/setup-uv@v4 + uses: astral-sh/setup-uv@v8.0.0 with: enable-cache: false cache-dependency-glob: '**/pyproject.toml' ignore-nothing-to-cache: true - name: Cache uv and venv - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: | ~/.cache/uv @@ -83,7 +83,7 @@ jobs: outputs: python-versions: ${{ steps.read-versions.outputs.versions }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - id: read-versions run: | versions=$(cat .github/supported-python-versions.json) @@ -110,13 +110,13 @@ jobs: docker save -o subtensor-localnet-devnet-ready.tar ghcr.io/opentensor/subtensor-localnet:devnet-ready - name: Upload main Docker Image as Artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: subtensor-localnet-main path: subtensor-localnet-main.tar - name: Upload devnet-ready Docker Image as Artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: subtensor-localnet-devnet-ready path: subtensor-localnet-devnet-ready.tar diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f5a988b42a..72b92a53e0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,11 +12,13 @@ jobs: build: name: Build Python distribution runs-on: ubuntu-latest + permissions: + contents: read steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v6 with: python-version: '3.11' @@ -45,7 +47,7 @@ jobs: fi - name: Upload artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: dist path: dist/ @@ -60,11 +62,19 @@ jobs: steps: - name: Download artifact - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: dist path: dist/ + - name: Verify artifact checksums + run: | + echo "Artifacts to be published:" + ls -la dist/ + echo "" + echo "SHA256 checksums:" + sha256sum dist/* + - name: Publish package distributions to PyPI uses: pypa/gh-action-pypi-publish@release/v1 with: diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml index 7dfaf2eda5..d990f61cd2 100644 --- a/.github/workflows/ruff.yml +++ b/.github/workflows/ruff.yml @@ -1,4 +1,4 @@ -name: Ruff - formatter check +name: Ruff - formatter/linter check permissions: contents: read @@ -8,26 +8,22 @@ on: jobs: ruff: - if: github.event.pull_request.draft == false runs-on: ubuntu-latest - + timeout-minutes: 10 steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - - name: Set up Python - uses: actions/setup-python@v5 + - name: Ruff format check + uses: astral-sh/ruff-action@v4.0.0 with: - python-version: "3.11" - - - name: Install Ruff in virtual environment - run: | - python -m venv venv - source venv/bin/activate - python -m pip install --upgrade pip - python -m pip install ruff==0.11.5 + version: "0.15.12" + args: "format --diff" + src: "bittensor tests" - - name: Ruff format check - run: | - source venv/bin/activate - python -m ruff format --diff bittensor + - name: Ruff linter check + uses: astral-sh/ruff-action@v4.0.0 + with: + version: "0.15.12" + args: "check" + src: "bittensor" diff --git a/.github/workflows/subtensor-consistency-tests.yaml b/.github/workflows/subtensor-consistency-tests.yaml index b7e02abf4d..3d762ff660 100644 --- a/.github/workflows/subtensor-consistency-tests.yaml +++ b/.github/workflows/subtensor-consistency-tests.yaml @@ -27,22 +27,22 @@ jobs: test-files: ${{ steps.get-tests.outputs.test-files }} steps: - name: Check-out repository under $GITHUB_WORKSPACE - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.10' - name: Install uv - uses: astral-sh/setup-uv@v4 + uses: astral-sh/setup-uv@v8.0.0 with: enable-cache: false cache-dependency-glob: '**/pyproject.toml' ignore-nothing-to-cache: true - name: Cache uv and venv - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: | ~/.cache/uv @@ -132,7 +132,7 @@ jobs: run: docker save -o subtensor-localnet.tar ${{ steps.set-image.outputs.image }} - name: Upload Docker Image as Artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: subtensor-localnet path: subtensor-localnet.tar @@ -161,18 +161,18 @@ jobs: test-file: ${{ fromJson(needs.find-tests.outputs.test-files) }} steps: - name: Check-out repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.10' - name: Install uv - uses: astral-sh/setup-uv@v4 + uses: astral-sh/setup-uv@v8.0.0 - name: Cache uv and venv - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: | ~/.cache/uv @@ -184,7 +184,7 @@ jobs: run: uv sync --extra dev --dev - name: Download Cached Docker Image - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: subtensor-localnet diff --git a/.github/workflows/unit-and-integration-tests.yml b/.github/workflows/unit-and-integration-tests.yml index a6a525981e..51b9a5eb2b 100644 --- a/.github/workflows/unit-and-integration-tests.yml +++ b/.github/workflows/unit-and-integration-tests.yml @@ -12,7 +12,7 @@ jobs: outputs: python-versions: ${{ steps.read-versions.outputs.versions }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - id: read-versions run: | versions=$(cat .github/supported-python-versions.json) @@ -31,20 +31,20 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Install uv - uses: astral-sh/setup-uv@v4 + uses: astral-sh/setup-uv@v8.0.0 with: enable-cache: false - name: Cache uv and .venv - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: | ~/.cache/uv diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d716cdd8a..e95ce43bb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,36 @@ # Changelog +## 10.3.0 /2026-0-21 + +## What's Changed +* Fix logging.info and state transitions in LoggingMachine by @ionodeionode in https://github.com/latent-to/bittensor/pull/3270 +* Feat/coldkey swap clear by @ibraheem-abe in https://github.com/latent-to/bittensor/pull/3296 +* Removes munch by @thewhaleking in https://github.com/latent-to/bittensor/pull/3298 +* Ensures we don't accidentally install ASI 2.0 on this by @thewhaleking in https://github.com/latent-to/bittensor/pull/3302 +* There is not Balances.transfer by @thewhaleking in https://github.com/latent-to/bittensor/pull/3300 +* Bumps all workflows versions by @thewhaleking in https://github.com/latent-to/bittensor/pull/3301 +* Fix: Updates exception check for Commitment Pallet extension by @ibraheem-abe in https://github.com/latent-to/bittensor/pull/3303 +* Fix and extend incentive e2e test by @basfroman in https://github.com/latent-to/bittensor/pull/3304 +* Fix/e2e tests for stake lock owner alpha by @ibraheem-abe in https://github.com/latent-to/bittensor/pull/3305 +* Update/staking hotkeys limitation for coldkey swap by @ibraheem-abe in https://github.com/latent-to/bittensor/pull/3306 +* Update: Log SHA & update perms of workflow by @ibraheem-abe in https://github.com/latent-to/bittensor/pull/3307 +* Fix e2e tests (metagraph_info) by @basfroman in https://github.com/latent-to/bittensor/pull/3310 +* Removes flake8, uses ruff for linter check by @thewhaleking in https://github.com/latent-to/bittensor/pull/3313 +* Adds note to httpx in dev reqs by @thewhaleking in https://github.com/latent-to/bittensor/pull/3315 +* Add `register_limit` extrinsic by @basfroman in https://github.com/latent-to/bittensor/pull/3316 +* ASI 2.0 & cyscale by @thewhaleking in https://github.com/latent-to/bittensor/pull/3314 +* Bump ASI req by @thewhaleking in https://github.com/latent-to/bittensor/pull/3324 +* Remove PoW registration, refactor `register()` to use `register_limit` by @basfroman in https://github.com/latent-to/bittensor/pull/3325 +* hoist get_required_fields out of to_headers by @okradze in https://github.com/latent-to/bittensor/pull/3323 +* Adds CODEOWNERS file by @thewhaleking in https://github.com/latent-to/bittensor/pull/3327 +* Bump ASI + cyscale by @thewhaleking in https://github.com/latent-to/bittensor/pull/3326 + +## New Contributors +* @ionodeionode made their first contribution in https://github.com/latent-to/bittensor/pull/3270 +* @okradze made their first contribution in https://github.com/latent-to/bittensor/pull/3323 + +**Full Changelog**: https://github.com/latent-to/bittensor/compare/v10.2.1...v10.3.0 + ## 10.2.1 /2026-04-22 ## What's Changed diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000000..b752bf1c02 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1 @@ +* @basfroman @thewhaleking @ibraheem-abe \ No newline at end of file diff --git a/Makefile b/Makefile index f71c2c2a2b..b776718f25 100644 --- a/Makefile +++ b/Makefile @@ -35,4 +35,4 @@ check: ruff @mypy --ignore-missing-imports bittensor/ --python-version=3.12 @mypy --ignore-missing-imports bittensor/ --python-version=3.13 @mypy --ignore-missing-imports bittensor/ --python-version=3.14 - @flake8 bittensor/ --count + @python -m ruff check bittensor/ diff --git a/bittensor/__init__.py b/bittensor/__init__.py index e290570af2..a9d99b0c76 100644 --- a/bittensor/__init__.py +++ b/bittensor/__init__.py @@ -1,3 +1,7 @@ -from .core.settings import __version__, DEFAULTS, DEFAULT_NETWORK -from .utils.btlogging import logging -from .utils.easy_imports import * +from .core.settings import ( + __version__ as __version__, + DEFAULTS as DEFAULTS, + DEFAULT_NETWORK as DEFAULT_NETWORK, +) +from .utils.btlogging import logging # noqa: F401 +from .utils.easy_imports import * # noqa: F403 diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index d199c029fc..669d2fc326 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -8,11 +8,12 @@ import scalecodec from async_substrate_interface import AsyncSubstrateInterface from async_substrate_interface.substrate_addons import RetryAsyncSubstrate -from async_substrate_interface.types import ScaleObj from async_substrate_interface.utils.storage import StorageKey from bittensor_drand import get_encrypted_commitment from bittensor_wallet.utils import SS58_FORMAT -from scalecodec import GenericCall +from scalecodec import GenericCall, ScaleValue +from scalecodec.base import ScaleType +from scalecodec.utils.math import FixedPoint, fixed_to_decimal from bittensor.core.chain_data import ( ColdkeySwapAnnouncementInfo, @@ -38,12 +39,10 @@ SubnetIdentity, SubnetInfo, WeightCommitInfo, - decode_account_id, ) from bittensor.core.chain_data.chain_identity import ChainIdentity from bittensor.core.chain_data.delegate_info import DelegatedInfo from bittensor.core.chain_data.utils import ( - decode_block, decode_metadata, decode_revealed_commitment, decode_revealed_commitment_with_hotkey, @@ -56,6 +55,7 @@ ) from bittensor.core.extrinsics.asyncex.coldkey_swap import ( announce_coldkey_swap_extrinsic, + clear_coldkey_swap_announcement_extrinsic, dispute_coldkey_swap_extrinsic, swap_coldkey_announced_extrinsic, ) @@ -97,7 +97,7 @@ ) from bittensor.core.extrinsics.asyncex.registration import ( burned_register_extrinsic, - register_extrinsic, + register_limit_extrinsic, register_subnet_extrinsic, set_subnet_identity_extrinsic, ) @@ -147,6 +147,11 @@ SubtensorMixin, UIDs, Weights, + PositionResponse, + NeuronCertificateResponse, + CommitmentOfResponse, + CrowdloansResponse, + DynamicInfoResponse, ) from bittensor.utils import ( Certificate, @@ -161,7 +166,6 @@ ) from bittensor.utils.balance import ( Balance, - FixedPoint, check_balance_amount, fixed_to_float, ) @@ -345,9 +349,9 @@ async def __aexit__(self, exc_type, exc_val, exc_tb): async def _decode_crowdloan_entry( self, crowdloan_id: int, - data: dict, + data: CrowdloansResponse, block_hash: Optional[str] = None, - ) -> "CrowdloanInfo": + ) -> CrowdloanInfo: """ Internal helper to parse and decode a single Crowdloan record. @@ -356,14 +360,15 @@ async def _decode_crowdloan_entry( call_data = data.get("call") if call_data and "Inline" in call_data: try: - inline_bytes = bytes(call_data["Inline"][0][0]) - scale_object = await self.substrate.create_scale_object( - type_string="Call", - data=scalecodec.ScaleBytes(inline_bytes), + runtime = await self.substrate.init_runtime(block_hash=block_hash) + call_obj = await self.substrate.create_scale_object( + "Call", + data=scalecodec.ScaleBytes(call_data["Inline"]), block_hash=block_hash, + runtime=runtime, ) - decoded_call = scale_object.decode() - data["call"] = decoded_call + call_value = call_obj.decode() + data["call"] = call_value except Exception as e: data["call"] = {"decode_error": str(e), "raw": call_data} @@ -485,6 +490,8 @@ async def determine_block_hash( return block_hash if block is not None: return await self.get_block_hash(block) + if reuse_block: + return self.substrate.last_block_hash return None async def _runtime_method_exists( @@ -502,12 +509,8 @@ async def _runtime_method_exists( """ runtime = await self.substrate.init_runtime(block_hash=block_hash) if runtime.metadata_v15 is not None: - metadata_v15_value = runtime.metadata_v15.value() - apis = {entry["name"]: entry for entry in metadata_v15_value["apis"]} try: - api_entry = apis[api] - methods = {entry["name"]: entry for entry in api_entry["methods"]} - _ = methods[method] + _ = runtime.runtime_api_map[api][method] return True except KeyError: return False @@ -527,7 +530,7 @@ async def _query_with_fallback( *args: tuple[str, str, Optional[list[Any]]], block_hash: Optional[str] = None, default_value: Any = ValueError, - ): + ) -> ScaleType[ScaleValue] | Any: """ Queries the subtensor node with a given set of args, falling back to the next group if the method does not exist at the given block. This method exists to support backwards compatibility for blocks. @@ -640,10 +643,8 @@ async def get_hyperparameter( storage_function=param_name, params=[netuid], block_hash=block_hash, - reuse_block_hash=reuse_block, ) - - return getattr(result, "value", result) + return result.value async def sim_swap( self, @@ -754,7 +755,7 @@ async def query_constant( block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> Optional["ScaleObj"]: + ) -> Optional[ScaleType[ScaleValue]]: """Retrieves a constant from the specified module on the Bittensor blockchain. Use this function for nonstandard queries to constants defined within the Bittensor blockchain, if these cannot @@ -778,7 +779,6 @@ async def query_constant( module_name=module_name, constant_name=constant_name, block_hash=block_hash, - reuse_block_hash=reuse_block, ) async def query_map( @@ -813,7 +813,6 @@ async def query_map( storage_function=name, params=params, block_hash=block_hash, - reuse_block_hash=reuse_block, ) return result @@ -847,7 +846,6 @@ async def query_map_subtensor( storage_function=name, params=params, block_hash=block_hash, - reuse_block_hash=reuse_block, ) async def query_module( @@ -858,7 +856,7 @@ async def query_module( block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> Optional[Union["ScaleObj", Any]]: + ) -> ScaleType[ScaleValue]: """Queries any module storage on the Bittensor blockchain with the specified parameters and block number. This function is a generic query interface that allows for flexible and diverse data retrieval from various blockchain modules. Use this function for nonstandard queries to storage defined within the Bittensor @@ -883,7 +881,6 @@ async def query_module( storage_function=name, params=params, block_hash=block_hash, - reuse_block_hash=reuse_block, ) async def query_runtime_api( @@ -894,7 +891,7 @@ async def query_runtime_api( block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> Optional[Any]: + ) -> Any: """Queries the runtime API of the Bittensor blockchain, providing a way to interact with the underlying runtime and retrieve data encoded in Scale Bytes format. Use this function for nonstandard queries to the runtime environment, if these cannot be accessed through other, standard getter methods. @@ -918,7 +915,7 @@ async def query_runtime_api( result = await self.substrate.runtime_call( runtime_api, method, params, block_hash ) - return result.value + return result async def query_subtensor( self, @@ -927,7 +924,7 @@ async def query_subtensor( block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> Optional[Union["ScaleObj", Any]]: + ) -> ScaleType[ScaleValue]: """Queries named storage from the Subtensor module on the Bittensor blockchain. Use this function for nonstandard queries to storage defined within the Bittensor blockchain, if these cannot @@ -950,7 +947,6 @@ async def query_subtensor( storage_function=name, params=params, block_hash=block_hash, - reuse_block_hash=reuse_block, ) async def state_call( @@ -985,7 +981,6 @@ async def state_call( method="state_call", params=[method, data], block_hash=block_hash, - reuse_block_hash=reuse_block, ) # Common subtensor methods ========================================================================================= @@ -1015,14 +1010,18 @@ async def all_subnets( if not block_hash and reuse_block: block_hash = self.substrate.last_block_hash - query = await self.substrate.runtime_call( - api="SubnetInfoRuntimeApi", - method="get_all_dynamic_info", - block_hash=block_hash, - ) - subnet_prices = await self.get_subnet_prices(block_hash=block_hash) + decoded: list[DynamicInfoResponse] + subnet_prices: dict[int, Balance] - decoded = query.decode() + decoded, subnet_prices = await asyncio.gather( + self.substrate.runtime_call( + api="SubnetInfoRuntimeApi", + method="get_all_dynamic_info", + block_hash=block_hash, + ), + self.get_subnet_prices(block_hash=block_hash), + return_exceptions=True, + ) if not isinstance(subnet_prices, (SubstrateRequestException, ValueError)): for sn in decoded: @@ -1058,14 +1057,14 @@ async def blocks_since_last_step( Notes: - """ - query = await self.query_subtensor( + query: ScaleType[int] = await self.query_subtensor( name="BlocksSinceLastStep", block=block, block_hash=block_hash, reuse_block=reuse_block, params=[netuid], ) - return cast(Optional[int], getattr(query, "value", query)) + return query.value async def blocks_since_last_update( self, @@ -1089,14 +1088,14 @@ async def blocks_since_last_update( """ block_hash = await self.determine_block_hash(block, block_hash, reuse_block) block = block or await self.substrate.get_block_number(block_hash) - call = await self.get_hyperparameter( + call: list[int] = await self.get_hyperparameter( param_name="LastUpdate", netuid=netuid, block=block, block_hash=block_hash, reuse_block=reuse_block, ) - return None if call is None else (block - int(call[uid])) + return None if len(call) == 0 else (block - int(call[uid])) async def blocks_until_next_epoch( self, @@ -1178,14 +1177,14 @@ async def bonds( storage_function="Bonds", params=[storage_index], block_hash=block_hash, - reuse_block_hash=reuse_block, ) - b_map = [] - async for uid, b in b_map_encoded: - if b.value is not None: - b_map.append((uid, b.value)) + bond_map = [] + uid: int + bond: list[tuple[int, int]] + async for uid, bond in b_map_encoded: + bond_map.append((uid, bond)) - return b_map + return bond_map async def commit_reveal_enabled( self, @@ -1211,13 +1210,14 @@ async def commit_reveal_enabled( - """ block_hash = await self.determine_block_hash(block, block_hash, reuse_block) - call = await self.get_hyperparameter( + call: Optional[bool] = await self.get_hyperparameter( param_name="CommitRevealWeightsEnabled", block_hash=block_hash, netuid=netuid, reuse_block=reuse_block, ) - return True if call is True else False + assert call is not None + return call async def difficulty( self, @@ -1225,7 +1225,7 @@ async def difficulty( block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> Optional[int]: + ) -> int: """Retrieves the 'Difficulty' hyperparameter for a specified subnet in the Bittensor network. This parameter determines the computational challenge required for neurons to participate in consensus and @@ -1250,14 +1250,13 @@ async def difficulty( - """ block_hash = await self.determine_block_hash(block, block_hash, reuse_block) - call = await self.get_hyperparameter( + call: Optional[int] = await self.get_hyperparameter( param_name="Difficulty", netuid=netuid, block_hash=block_hash, reuse_block=reuse_block, ) - if call is None: - return None + assert call is not None return int(call) async def does_hotkey_exist( @@ -1289,20 +1288,13 @@ async def does_hotkey_exist( - """ block_hash = await self.determine_block_hash(block, block_hash, reuse_block) - result = await self.substrate.query( + result: ScaleType[str] = await self.substrate.query( module="SubtensorModule", storage_function="Owner", params=[hotkey_ss58], block_hash=block_hash, - reuse_block_hash=reuse_block, ) - return_val = ( - False - if result is None - # not the default key (0x0) - else result != "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM" - ) - return return_val + return result.value != "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM" async def get_admin_freeze_window( self, @@ -1329,19 +1321,19 @@ async def get_admin_freeze_window( - """ block_hash = await self.determine_block_hash(block, block_hash, reuse_block) - query = await self.substrate.query( + query: ScaleType[int] = await self.substrate.query( module="SubtensorModule", storage_function="AdminFreezeWindow", block_hash=block_hash, ) - return cast(int, getattr(query, "value", query)) + return query.value async def get_all_subnets_info( self, block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> list["SubnetInfo"]: + ) -> list[SubnetInfo]: """Retrieves detailed information about all subnets within the Bittensor network. Parameters: @@ -1416,9 +1408,11 @@ async def get_all_commitments( reuse_block=reuse_block, ) result = {} + id_: str + value: CommitmentOfResponse async for id_, value in query: try: - result[decode_account_id(id_[0])] = decode_metadata(value) + result[id_] = decode_metadata(value) except Exception as error: logging.error( f"Error decoding [red]{id_}[/red] and [red]{value}[/red]: {error}" @@ -1499,10 +1493,10 @@ async def get_all_metagraphs_info( method=method, block_hash=block_hash, ) - if query is None or not hasattr(query, "value"): + if query is None: return None - return MetagraphInfo.list_from_dicts(query.value) + return MetagraphInfo.list_from_dicts(query) async def get_all_neuron_certificates( self, @@ -1539,7 +1533,7 @@ async def get_all_neuron_certificates( ) output = {} async for key, item in query_certificates: - output[decode_account_id(key)] = Certificate(item.value) + output[key] = Certificate(item) return output async def get_all_revealed_commitments( @@ -1620,7 +1614,6 @@ async def get_all_subnets_netuid( module="SubtensorModule", storage_function="NetworksAdded", block_hash=block_hash, - reuse_block_hash=reuse_block, ) subnets = [] if result.records: @@ -1662,8 +1655,7 @@ async def get_auto_stakes( ) pairs = {} - async for netuid, destination in query: - hotkey_ss58 = decode_account_id(destination.value[0]) + async for netuid, hotkey_ss58 in query: if hotkey_ss58: pairs[int(netuid)] = hotkey_ss58 @@ -1692,14 +1684,13 @@ async def get_balance( Balance: The balance object containing the account's TAO balance. """ block_hash = await self.determine_block_hash(block, block_hash, reuse_block) - balance = await self.substrate.query( + balance: ScaleType[dict[str, Any]] = await self.substrate.query( module="System", storage_function="Account", params=[address], block_hash=block_hash, - reuse_block_hash=reuse_block, ) - return Balance(balance["data"]["free"]) + return Balance(balance.value["data"]["free"]) async def get_balances( self, @@ -1739,7 +1730,9 @@ async def get_balances( ) for address in addresses ] - batch_call = await self.substrate.query_multi(calls, block_hash=block_hash) + batch_call: list[tuple[StorageKey, dict]] = await self.substrate.query_multi( + calls, block_hash=block_hash + ) # type: ignore[assignment] results = {} for item in batch_call: value = item[1] or {"data": {"free": 0}} @@ -1871,13 +1864,11 @@ async def get_children( storage_function="ChildKeys", params=[hotkey_ss58, netuid], block_hash=block_hash, - reuse_block_hash=reuse_block, ) if children: formatted_children = [] - for proportion, child in children.value: + for proportion, formatted_child in children.value: # Convert U64 to int - formatted_child = decode_account_id(child[0]) normalized_proportion = u64_normalized_float(proportion) formatted_children.append((normalized_proportion, formatted_child)) return True, formatted_children, "" @@ -1927,7 +1918,6 @@ async def get_children_pending( block_hash, reuse_block, ), - reuse_block_hash=reuse_block, ) pending_value = getattr(response, "value", response) children, cooldown = cast( @@ -1939,7 +1929,7 @@ async def get_children_pending( [ ( u64_normalized_float(proportion), - decode_account_id(child[0]), + child, ) for proportion, child in children ], @@ -1952,7 +1942,7 @@ async def get_coldkey_swap_announcement( block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> Optional["ColdkeySwapAnnouncementInfo"]: + ) -> Optional[ColdkeySwapAnnouncementInfo]: """ Retrieves coldkey swap announcement for a specific coldkey. @@ -1975,17 +1965,16 @@ async def get_coldkey_swap_announcement( - See: """ block_hash = await self.determine_block_hash(block, block_hash, reuse_block) - query = await self.substrate.query( + query: ScaleType[Optional[tuple[int, str]]] = await self.substrate.query( module="SubtensorModule", storage_function="ColdkeySwapAnnouncements", params=[coldkey_ss58], block_hash=block_hash, - reuse_block_hash=reuse_block, ) - if query is None: + if query.value is None: return None return ColdkeySwapAnnouncementInfo.from_query( - coldkey_ss58=coldkey_ss58, query=cast(ScaleObj, query) + coldkey_ss58=coldkey_ss58, query=query ) async def get_coldkey_swap_announcements( @@ -2018,7 +2007,6 @@ async def get_coldkey_swap_announcements( module="SubtensorModule", storage_function="ColdkeySwapAnnouncements", block_hash=block_hash, - reuse_block_hash=reuse_block, ) return [ ColdkeySwapAnnouncementInfo.from_record(record) @@ -2054,7 +2042,6 @@ async def get_coldkey_swap_announcement_delay( module="SubtensorModule", storage_function="ColdkeySwapAnnouncementDelay", block_hash=block_hash, - reuse_block_hash=reuse_block, ) value = getattr(query, "value", query) return cast(int, value) if value is not None else 0 @@ -2088,7 +2075,6 @@ async def get_coldkey_swap_reannouncement_delay( module="SubtensorModule", storage_function="ColdkeySwapReannouncementDelay", block_hash=block_hash, - reuse_block_hash=reuse_block, ) value = getattr(query, "value", query) return cast(int, value) if value is not None else 0 @@ -2125,13 +2111,10 @@ async def get_coldkey_swap_dispute( storage_function="ColdkeySwapDisputes", params=[coldkey_ss58], block_hash=block_hash, - reuse_block_hash=reuse_block, ) if query is None: return None - return ColdkeySwapDisputeInfo.from_query( - coldkey_ss58=coldkey_ss58, query=cast(ScaleObj, query) - ) + return ColdkeySwapDisputeInfo.from_query(coldkey_ss58=coldkey_ss58, query=query) async def get_coldkey_swap_disputes( self, @@ -2163,7 +2146,6 @@ async def get_coldkey_swap_disputes( module="SubtensorModule", storage_function="ColdkeySwapDisputes", block_hash=block_hash, - reuse_block_hash=reuse_block, ) return [ ColdkeySwapDisputeInfo.from_record(record) async for record in query_map @@ -2262,13 +2244,11 @@ async def get_commitment( ) return "" - metadata = cast( - dict, - await self.get_commitment_metadata( - netuid, hotkey, block, block_hash, reuse_block - ), + metadata = await self.get_commitment_metadata( + netuid, hotkey, block, block_hash, reuse_block ) try: + assert not isinstance(metadata, str) return decode_metadata(metadata) except Exception as error: logging.error(error) @@ -2281,7 +2261,7 @@ async def get_commitment_metadata( block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> Union[str, dict]: + ) -> str | CommitmentOfResponse: # TODO: how to handle return data? need good example @roman """Fetches raw commitment metadata from specific subnet for given hotkey. @@ -2300,16 +2280,17 @@ async def get_commitment_metadata( - """ block_hash = await self.determine_block_hash(block, block_hash, reuse_block) - commit_data = await self.substrate.query( + commit_data: ScaleType[ + Optional[CommitmentOfResponse] + ] = await self.substrate.query( module="Commitments", storage_function="CommitmentOf", params=[netuid, hotkey_ss58], block_hash=block_hash, - reuse_block_hash=reuse_block, ) - if commit_data is None: + if commit_data.value is None: return "" - return cast(Union[str, dict], getattr(commit_data, "value", commit_data)) + return commit_data.value async def get_crowdloan_constants( self, @@ -2317,7 +2298,7 @@ async def get_crowdloan_constants( block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> "CrowdloanConstants": + ) -> CrowdloanConstants: """Retrieves runtime configuration constants governing crowdloan behavior and limits on the Bittensor blockchain. If a list of constant names is provided, only those constants will be queried. @@ -2371,7 +2352,7 @@ async def get_crowdloan_contributions( block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> dict[str, "Balance"]: + ) -> dict[str, Balance]: """Retrieves all contributions made to a specific crowdloan campaign. Returns a mapping of contributor coldkey addresses to their contribution amounts in Rao. @@ -2401,13 +2382,11 @@ async def get_crowdloan_contributions( ) result = {} - - if query.records: - async for record in query: - if record[1].value: - result[decode_account_id(record[0])] = Balance.from_rao( - record[1].value - ) + contributor: str + amount: int + async for contributor, amount in query: + if amount: + result[contributor] = Balance.from_rao(amount) return result @@ -2417,7 +2396,7 @@ async def get_crowdloan_by_id( block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> Optional["CrowdloanInfo"]: + ) -> Optional[CrowdloanInfo]: """Retrieves detailed information about a specific crowdloan campaign. Parameters: @@ -2437,13 +2416,13 @@ async def get_crowdloan_by_id( - Crowdloans Overview: """ block_hash = await self.determine_block_hash(block, block_hash, reuse_block) - query = await self.substrate.query( + query: ScaleType[Optional[CrowdloansResponse]] = await self.substrate.query( module="Crowdloan", storage_function="Crowdloans", params=[crowdloan_id], block_hash=block_hash, ) - if not query: + if not query.value: return None return await self._decode_crowdloan_entry( crowdloan_id=crowdloan_id, data=query.value, block_hash=block_hash @@ -2473,13 +2452,12 @@ async def get_crowdloan_next_id( - Crowdloan Tutorial: """ block_hash = await self.determine_block_hash(block, block_hash, reuse_block) - result = await self.substrate.query( + result: ScaleType[int] = await self.substrate.query( module="Crowdloan", storage_function="NextCrowdloanId", block_hash=block_hash, ) - value = getattr(result, "value", result) - return int(value or 0) + return result.value async def get_crowdloans( self, @@ -2515,17 +2493,14 @@ async def get_crowdloans( ) crowdloans = [] - - if query.records: - async for c_id, value_obj in query: - data = value_obj.value - if not data: - continue - crowdloans.append( - await self._decode_crowdloan_entry( - crowdloan_id=c_id, data=data, block_hash=block_hash - ) + c_id: int + data: CrowdloansResponse + async for c_id, data in query: + crowdloans.append( + await self._decode_crowdloan_entry( + crowdloan_id=c_id, data=data, block_hash=block_hash ) + ) return crowdloans @@ -2599,12 +2574,11 @@ async def get_delegate_identities( module="SubtensorModule", storage_function="IdentitiesV2", block_hash=block_hash, - reuse_block_hash=reuse_block, ) return { - decode_account_id(ss58_address[0]): ChainIdentity.from_dict( - decode_hex_identity_dict(identity.value), + ss58_address: ChainIdentity.from_dict( + decode_hex_identity_dict(identity), ) async for ss58_address, identity in identities } @@ -2634,14 +2608,14 @@ async def get_delegate_take( - """ block_hash = await self.determine_block_hash(block, block_hash, reuse_block) - result = await self.query_subtensor( + result: ScaleType[int] = await self.query_subtensor( name="Delegates", block_hash=block_hash, reuse_block=reuse_block, params=[hotkey_ss58], ) - return u16_normalized_float(result.value) # type: ignore + return u16_normalized_float(result.value) async def get_delegated( self, @@ -2747,17 +2721,16 @@ async def get_existential_deposit( - """ block_hash = await self.determine_block_hash(block, block_hash, reuse_block) - result = await self.substrate.get_constant( + result: Optional[ScaleType[int]] = await self.substrate.get_constant( module_name="Balances", constant_name="ExistentialDeposit", block_hash=block_hash, - reuse_block_hash=reuse_block, ) if result is None: raise Exception("Unable to retrieve existential deposit amount.") - return Balance.from_rao(getattr(result, "value", 0)) + return Balance.from_rao(result.value) async def get_ema_tao_inflow( self, @@ -2788,7 +2761,7 @@ async def get_ema_tao_inflow( - EMA smoothing: """ block_hash = await self.determine_block_hash(block) - query = await self.substrate.query( + query: ScaleType[Optional[tuple[int, FixedPoint]]] = await self.substrate.query( module="SubtensorModule", storage_function="SubnetEmaTaoFlow", params=[netuid], @@ -2796,11 +2769,12 @@ async def get_ema_tao_inflow( ) # sn0 doesn't have EmaTaoInflow - if query is None: + if query.value is None: return None block_updated, tao_bits = query.value ema_value = int(fixed_to_float(tao_bits)) + # TODO verify this from rao, seems like we're just rounding down return block_updated, Balance.from_rao(ema_value) async def get_hotkey_owner( @@ -2830,18 +2804,18 @@ async def get_hotkey_owner( - """ block_hash = await self.determine_block_hash(block, block_hash, reuse_block) - hk_owner_query = await self.substrate.query( + hk_owner: ScaleType[str] = await self.substrate.query( module="SubtensorModule", storage_function="Owner", params=[hotkey_ss58], block_hash=block_hash, - reuse_block_hash=reuse_block, ) - exists = False - if hk_owner_query: + if hk_owner.value != "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM": exists = await self.does_hotkey_exist(hotkey_ss58, block_hash=block_hash) - hotkey_owner = hk_owner_query if exists else None - return cast(Optional[str], getattr(hotkey_owner, "value", hotkey_owner)) + else: + exists = False + hotkey_owner = hk_owner.value if exists else None + return hotkey_owner async def get_last_bonds_reset( self, @@ -2850,7 +2824,7 @@ async def get_last_bonds_reset( block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ): + ) -> ScaleType[Optional[int]]: """Retrieves the block number when bonds were last reset for a specific hotkey on a subnet. Parameters: @@ -2861,19 +2835,19 @@ async def get_last_bonds_reset( reuse_block: Whether to use the last-used block. Do not set if using `block_hash` or `block`. Returns: - The block number when bonds were last reset, or `None` if no bonds reset has occurred. + A ScaleType object containing the block number when bonds were last reset, or `None` if no bonds reset + has occurred. Notes: - - """ block_hash = await self.determine_block_hash(block, block_hash, reuse_block) - block = await self.substrate.query( + block: ScaleType[Optional[int]] = await self.substrate.query( module="Commitments", storage_function="LastBondsReset", params=[netuid, hotkey_ss58], block_hash=block_hash, - reuse_block_hash=reuse_block, ) return block @@ -2910,10 +2884,7 @@ async def get_last_commitment_bonds_reset_block( block_data = await self.get_last_bonds_reset( netuid, hotkey, block, block_hash, reuse_block ) - try: - return decode_block(block_data) - except TypeError: - return None + return block_data.value async def get_liquidity_list( self, @@ -3005,13 +2976,12 @@ async def get_liquidity_list( current_tick = price_to_tick(sqrt_price**2) # Fetch positions - positions_values: list[tuple[dict, int, int]] = [] + positions_values: list[tuple[PositionResponse, int, int]] = [] positions_storage_keys: list[StorageKey] = [] - async for _, p in positions_response: - position = p.value - - tick_low_idx = position.get("tick_low")[0] - tick_high_idx = position.get("tick_high")[0] + position: PositionResponse + async for _, position in positions_response: + tick_low_idx = position.get("tick_low") + tick_high_idx = position.get("tick_high") positions_values.append((position, tick_low_idx, tick_high_idx)) tick_low_sk = await self.substrate.create_storage_key( pallet="Swap", @@ -3033,7 +3003,7 @@ async def get_liquidity_list( ) # iterator with just the values ticks = iter([x[1] for x in ticks_query]) - positions = [] + positions: list[LiquidityPosition] = [] for position, tick_low_idx, tick_high_idx in positions_values: tick_low = next(ticks) tick_high = next(ticks) @@ -3089,19 +3059,15 @@ async def get_liquidity_list( positions.append( LiquidityPosition( - **{ - "id": position.get("id")[0], - "price_low": Balance.from_tao( - tick_to_price(position.get("tick_low")[0]) - ), - "price_high": Balance.from_tao( - tick_to_price(position.get("tick_high")[0]) - ), - "liquidity": Balance.from_rao(position.get("liquidity")), - "fees_tao": fees_tao, - "fees_alpha": fees_alpha, - "netuid": position.get("netuid"), - } + id=position.get("id"), + price_low=Balance.from_tao(tick_to_price(position.get("tick_low"))), + price_high=Balance.from_tao( + tick_to_price(position.get("tick_high")) + ), + liquidity=Balance.from_rao(position.get("liquidity")), + fees_tao=fees_tao, + fees_alpha=fees_alpha, + netuid=position.get("netuid"), ) ) @@ -3130,15 +3096,17 @@ async def get_mechanism_emission_split( - """ block_hash = await self.determine_block_hash(block, block_hash, reuse_block) - result = await self._query_with_fallback( + result: Optional[ + ScaleType[Optional[list[int]]] + ] = await self._query_with_fallback( ("SubtensorModule", "MechanismEmissionSplit", [netuid]), block_hash=block_hash, default_value=None, ) - if result is None or not hasattr(result, "value"): + if result is None or result.value is None: return None - - return [round(i / sum(result.value) * 100) for i in result.value] + total = sum(result.value) + return [round(i / total * 100) for i in result.value] async def get_mechanism_count( self, @@ -3162,12 +3130,12 @@ async def get_mechanism_count( - """ block_hash = await self.determine_block_hash(block, block_hash, reuse_block) - query = await self._query_with_fallback( + query: ScaleType[Optional[int]] = await self._query_with_fallback( ("SubtensorModule", "MechanismCountCurrent", [netuid]), block_hash=block_hash, default_value=None, ) - return getattr(query, "value", 1) + return query.value or 1 async def get_metagraph_info( self, @@ -3263,13 +3231,13 @@ async def get_metagraph_info( default_value=None, ) - if getattr(query, "value", None) is None: + if query is None: logging.error( f"Subnet mechanism {netuid}.{mechid if mechid else 0} does not exist." ) return None - return MetagraphInfo.from_dict(query.value) + return MetagraphInfo.from_dict(query) async def get_mev_shield_current_key( self, @@ -3296,16 +3264,18 @@ async def get_mev_shield_current_key( announced a key yet. """ block_hash = await self.determine_block_hash(block, block_hash, reuse_block) - query = await self.substrate.query( + query: ScaleType[Optional[bytearray]] = await self.substrate.query( module="MevShield", storage_function="CurrentKey", block_hash=block_hash, ) - if query is None: + if query.value_object is None: return None - public_key_bytes = bytes(next(iter(query))) + value: bytearray = query.value_object + + public_key_bytes = bytes(value) # Validate public_key size for ML-KEM-768 (must be exactly 1184 bytes) if len(public_key_bytes) != MLKEM768_PUBLIC_KEY_SIZE: @@ -3341,16 +3311,17 @@ async def get_mev_shield_next_key( announced the next key yet. """ block_hash = await self.determine_block_hash(block, block_hash, reuse_block) - query = await self.substrate.query( + query: ScaleType[Optional[bytearray]] = await self.substrate.query( module="MevShield", storage_function="NextKey", block_hash=block_hash, ) - if query is None: + if query.value_object is None: return None - public_key_bytes = bytes(next(iter(query))) + value: bytearray = query.value_object + public_key_bytes = bytes(value) # Validate public_key size for ML-KEM-768 (must be exactly 1184 bytes) if len(public_key_bytes) != MLKEM768_PUBLIC_KEY_SIZE: @@ -3361,7 +3332,7 @@ async def get_mev_shield_next_key( return public_key_bytes - async def get_minimum_required_stake(self): + async def get_minimum_required_stake(self) -> Balance: """Returns the minimum required stake threshold for nominator cleanup operations. This threshold is used ONLY for cleanup after unstaking operations. If a nominator's remaining stake @@ -3382,7 +3353,7 @@ async def get_minimum_required_stake(self): module="SubtensorModule", storage_function="NominatorMinRequiredStake" ) - return Balance.from_rao(getattr(result, "value", 0)) + return Balance.from_rao(result.value or 0) async def get_netuids_for_hotkey( self, @@ -3412,13 +3383,14 @@ async def get_netuids_for_hotkey( storage_function="IsNetworkMember", params=[hotkey_ss58], block_hash=block_hash, - reuse_block_hash=reuse_block, ) netuids = [] + netuid: int + is_member: bool if result.records: - async for record in result: - if record[1].value: - netuids.append(record[0]) + async for netuid, is_member in result: + if is_member: + netuids.append(netuid) return netuids async def get_neuron_certificate( @@ -3448,22 +3420,18 @@ async def get_neuron_certificate( This function is used for certificate discovery for setting up mutual tls communication between neurons. """ block_hash = await self.determine_block_hash(block, block_hash, reuse_block) - certificate = cast( - Union[str, dict], - await self.query_module( - module="SubtensorModule", - name="NeuronCertificates", - block_hash=block_hash, - reuse_block=reuse_block, - params=[netuid, hotkey_ss58], - ), + certificate_query: ScaleType[ + Optional[str | NeuronCertificateResponse] + ] = await self.query_module( + module="SubtensorModule", + name="NeuronCertificates", + block_hash=block_hash, + reuse_block=reuse_block, + params=[netuid, hotkey_ss58], ) - try: - if certificate: - return Certificate(certificate) - - except AttributeError: - return None + certificate: Optional[NeuronCertificateResponse] = certificate_query.value + if certificate is not None: + return Certificate(certificate) return None async def get_neuron_for_pubkey_and_subnet( @@ -3473,7 +3441,7 @@ async def get_neuron_for_pubkey_and_subnet( block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> "NeuronInfo": + ) -> NeuronInfo: """ Retrieves information about a neuron based on its public key (hotkey SS58 address) and the specific subnet UID (netuid). This function provides detailed neuron information for a particular subnet within the Bittensor @@ -3498,9 +3466,8 @@ async def get_neuron_for_pubkey_and_subnet( storage_function="Uids", params=[netuid, hotkey_ss58], block_hash=block_hash, - reuse_block_hash=reuse_block, ) - if (uid := getattr(uid_query, "value", None)) is None: + if (uid := uid_query.value) is None: return NeuronInfo.get_null_neuron() else: return await self.neuron_for_uid( @@ -3576,10 +3543,9 @@ async def get_owned_hotkeys( storage_function="OwnedHotkeys", params=[coldkey_ss58], block_hash=block_hash, - reuse_block_hash=reuse_block, ) - return [decode_account_id(hotkey[0]) for hotkey in owned_hotkeys or []] + return owned_hotkeys.value or [] async def get_parents( self, @@ -3613,13 +3579,11 @@ async def get_parents( storage_function="ParentKeys", params=[hotkey_ss58, netuid], block_hash=block_hash, - reuse_block_hash=reuse_block, ) - if parents: + if parents.value: formatted_parents = [] - for proportion, parent in parents.value: + for proportion, formatted_child in parents.value: # Convert U64 to int - formatted_child = decode_account_id(parent[0]) normalized_proportion = u64_normalized_float(proportion) formatted_parents.append((normalized_proportion, formatted_child)) return formatted_parents @@ -3657,7 +3621,6 @@ async def get_proxies( module="Proxy", storage_function="Proxies", block_hash=block_hash, - reuse_block_hash=reuse_block, ) proxies = {} @@ -3702,7 +3665,6 @@ async def get_proxies_for_real_account( storage_function="Proxies", params=[real_account_ss58], block_hash=block_hash, - reuse_block_hash=reuse_block, ) return ProxyInfo.from_query(query) @@ -3735,15 +3697,13 @@ async def get_proxy_announcement( - See: """ block_hash = await self.determine_block_hash(block, block_hash, reuse_block) - query = await self.substrate.query( + query: ScaleType[tuple[list[dict], int]] = await self.substrate.query( # type: ignore[assignment] module="Proxy", storage_function="Announcements", params=[delegate_account_ss58], block_hash=block_hash, - reuse_block_hash=reuse_block, ) - query_value = getattr(query, "value", query) - return ProxyAnnouncementInfo.from_dict(cast(list[Any], query_value)[0]) + return ProxyAnnouncementInfo.from_dict(query.value) async def get_proxy_announcements( self, @@ -3776,7 +3736,6 @@ async def get_proxy_announcements( module="Proxy", storage_function="Announcements", block_hash=block_hash, - reuse_block_hash=reuse_block, ) announcements = {} async for record in query_map: @@ -3914,9 +3873,11 @@ async def get_revealed_commitment_by_hotkey( block_hash=block_hash, reuse_block=reuse_block, ) - if query is None: + if query.value_serialized is None: return None - return tuple(decode_revealed_commitment(pair) for pair in query) + return tuple( + decode_revealed_commitment(pair) for pair in query.value_serialized + ) async def get_root_claim_type( self, @@ -3924,7 +3885,7 @@ async def get_root_claim_type( block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> Union[str, dict]: + ) -> str | dict[str, dict[str, list[int]]]: """Return the configured root claim type for a given coldkey. The root claim type controls how dividends from staking to the Root Subnet (subnet 0) are processed when they @@ -3952,31 +3913,15 @@ async def get_root_claim_type( - See also: """ block_hash = await self.determine_block_hash(block, block_hash, reuse_block) - query = await self.substrate.query( + query: ScaleType[ + str | dict[str, dict[str, list[int]]] + ] = await self.substrate.query( module="SubtensorModule", storage_function="RootClaimType", params=[coldkey_ss58], block_hash=block_hash, - reuse_block_hash=reuse_block, ) - query_value = getattr(query, "value", query) - claim_type = cast(dict[str, Any], query_value) - # Query returns enum as dict: {"Swap": ()} or {"Keep": ()} or {"KeepSubnets": {"subnets": [1, 2, 3]}} - variant_name = next(iter(claim_type.keys())) - variant_value = claim_type[variant_name] - - # For simple variants (Swap, Keep), value is empty tuple, return string - if not variant_value or variant_value == (): - return variant_name - - # For KeepSubnets, value contains the data, return full dict structure - if isinstance(variant_value, dict) and "subnets" in variant_value: - subnets_raw = variant_value["subnets"] - subnets = list(subnets_raw[0]) - - return {variant_name: {"subnets": subnets}} - - return {variant_name: variant_value} + return query.value async def get_root_alpha_dividends_per_subnet( self, @@ -4002,15 +3947,13 @@ async def get_root_alpha_dividends_per_subnet( Balance: The root alpha dividends for this hotkey on this subnet in Rao, with unit set to netuid. """ block_hash = await self.determine_block_hash(block, block_hash, reuse_block) - query = await self.substrate.query( + query: ScaleType[int] = await self.substrate.query( module="SubtensorModule", storage_function="RootAlphaDividendsPerSubnet", params=[netuid, hotkey_ss58], block_hash=block_hash, - reuse_block_hash=reuse_block, ) - value = getattr(query, "value", query) - return Balance.from_rao(cast(int, value)).set_unit(netuid=netuid) + return Balance.from_rao(query.value, netuid=netuid) async def get_root_claimable_rate( self, @@ -4071,16 +4014,15 @@ async def get_root_claimable_all_rates( - See: """ block_hash = await self.determine_block_hash(block, block_hash, reuse_block) - query = await self.substrate.query( + query: ScaleType[list[tuple[int, FixedPoint]]] = await self.substrate.query( module="SubtensorModule", storage_function="RootClaimable", params=[hotkey_ss58], block_hash=block_hash, - reuse_block_hash=reuse_block, ) - query_value = getattr(query, "value", query) - bits_list = next(iter(cast(list[list[tuple[int, FixedPoint]]], query_value))) - return {bits[0]: fixed_to_float(bits[1], frac_bits=32) for bits in bits_list} + return { + netuid: fixed_to_float(bits, frac_bits=32) for (netuid, bits) in query.value + } async def get_root_claimable_stake( self, @@ -4171,15 +4113,13 @@ async def get_root_claimed( - See: """ block_hash = await self.determine_block_hash(block, block_hash, reuse_block) - query = await self.substrate.query( + query: ScaleType[int] = await self.substrate.query( module="SubtensorModule", storage_function="RootClaimed", params=[netuid, hotkey_ss58, coldkey_ss58], block_hash=block_hash, - reuse_block_hash=reuse_block, ) - value = getattr(query, "value", query) - return Balance.from_rao(cast(int, value)).set_unit(netuid=netuid) + return Balance.from_rao(query.value, netuid=netuid) async def get_stake( self, @@ -4340,7 +4280,7 @@ async def get_stake_info_for_coldkey( block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> list["StakeInfo"]: + ) -> list[StakeInfo]: """ Retrieves the stake information for a given coldkey. @@ -4374,7 +4314,7 @@ async def get_stake_info_for_coldkeys( block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> dict[str, list["StakeInfo"]]: + ) -> dict[str, list[StakeInfo]]: """ Retrieves the stake information for multiple coldkeys. @@ -4399,10 +4339,7 @@ async def get_stake_info_for_coldkeys( if query is None: return {} - return { - decode_account_id(ck): StakeInfo.list_from_dicts(st_info) - for ck, st_info in query - } + return {ck: StakeInfo.list_from_dicts(st_info) for ck, st_info in query} async def get_stake_for_hotkey( self, @@ -4431,9 +4368,7 @@ async def get_stake_for_hotkey( block_hash=block_hash, reuse_block=reuse_block, ) - balance = Balance.from_rao(hotkey_alpha_query.value) - balance.set_unit(netuid=netuid) - return balance + return Balance.from_rao(hotkey_alpha_query.value, netuid=netuid) get_hotkey_stake = get_stake_for_hotkey @@ -4467,6 +4402,36 @@ async def get_stake_weight( ) return [u16_normalized_float(w) for w in cast(list[int], result or [])] + async def get_staking_hotkeys( + self, + coldkey_ss58: str, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> list[str]: + """ + Retrieves the hotkeys that have staked for a given coldkey. + + Parameters: + coldkey_ss58: The SS58 address of the coldkey. + block: The block number at which to query the stake information. + block_hash: The hash of the blockchain block number for the query. + reuse_block: Whether to reuse the last-used block hash. + + Returns: + A list of hotkey SS58 addresses that have staked for the given coldkey. + """ + block_hash = await self.determine_block_hash( + block=block, block_hash=block_hash, reuse_block=reuse_block + ) + result = await self.substrate.query( + module="SubtensorModule", + storage_function="StakingHotkeys", + params=[coldkey_ss58], + block_hash=block_hash, + ) + return result or [] + async def get_start_call_delay( self, block: Optional[int] = None, @@ -4484,17 +4449,14 @@ async def get_start_call_delay( Return: Amount of blocks after the start call can be executed. """ - return cast( - int, - ( - await self.query_subtensor( - name="StartCallDelay", - block=block, - block_hash=block_hash, - reuse_block=reuse_block, - ) - ), - ) + return ( + await self.query_subtensor( + name="StartCallDelay", + block=block, + block_hash=block_hash, + reuse_block=reuse_block, + ) + ).value async def get_subnet_burn_cost( self, @@ -4665,7 +4627,7 @@ async def get_subnet_price( params=[netuid], block_hash=block_hash, ) - price_rao = call.value + price_rao = call return Balance.from_rao(price_rao) async def get_subnet_prices( @@ -4699,10 +4661,10 @@ async def get_subnet_prices( ) prices = {} - async for id_, current_sqrt_price in current_sqrt_prices: - current_sqrt_price = fixed_to_float(current_sqrt_price) + async for id_, current_sqrt_price_bits in current_sqrt_prices: + current_sqrt_price = fixed_to_decimal(current_sqrt_price_bits) current_price = current_sqrt_price * current_sqrt_price - current_price_in_tao = Balance.from_rao(int(current_price * 1e9)) + current_price_in_tao = Balance.from_tao(float(current_price)) prices.update({id_: current_price_in_tao}) # SN0 price is always 1 TAO @@ -4869,7 +4831,6 @@ async def get_total_subnets( storage_function="TotalNetworks", params=[], block_hash=block_hash, - reuse_block_hash=reuse_block, ) return getattr(result, "value", None) @@ -4981,21 +4942,17 @@ async def get_vote_data( network, particularly how proposals are received and acted upon by the governing body. """ block_hash = await self.determine_block_hash(block, block_hash, reuse_block) - vote_data = cast( - Optional[dict[str, Any]], - await self.substrate.query( - module="Triumvirate", - storage_function="Voting", - params=[proposal_hash], - block_hash=block_hash, - reuse_block_hash=reuse_block, - ), + vote_data: ScaleType[Optional[dict[str, Any]]] = await self.substrate.query( + module="Triumvirate", + storage_function="Voting", + params=[proposal_hash], + block_hash=block_hash, ) - if vote_data is None: + if vote_data.value is None: return None - return ProposalVoteData.from_dict(vote_data) + return ProposalVoteData.from_dict(vote_data.value) async def get_uid_for_hotkey_on_subnet( self, @@ -5027,7 +4984,6 @@ async def get_uid_for_hotkey_on_subnet( storage_function="Uids", params=[netuid, hotkey_ss58], block_hash=block_hash, - reuse_block_hash=reuse_block, ) return cast(Optional[int], getattr(result, "value", result)) @@ -5189,9 +5145,8 @@ async def is_fast_blocks(self) -> bool: - """ - slot_duration_obj = cast( - ScaleObj, await self.query_constant("Aura", "SlotDuration") - ) + slot_duration_obj = await self.query_constant("Aura", "SlotDuration") + assert slot_duration_obj is not None return slot_duration_obj.value == 250 async def is_hotkey_delegate( @@ -5355,7 +5310,11 @@ async def is_subnet_active( reuse_block=reuse_block, params=[netuid], ) - return True if query and query.value > 0 else False + qv: Optional[int] = query.value + if qv is None or qv <= 0: + return False + else: + return True async def last_drand_round(self) -> Optional[int]: """Retrieves the last drand round emitted in Bittensor. @@ -5633,19 +5592,12 @@ async def query_identity( storage_function="IdentitiesV2", params=[coldkey_ss58], block_hash=block_hash, - reuse_block_hash=reuse_block, ) - if not identity_info: - return None - - try: - identity_data = getattr(identity_info, "value", identity_info) - return ChainIdentity.from_dict( - decode_hex_identity_dict(cast(dict[str, Any], identity_data)), - ) - except TypeError: + identity_data: Optional[dict[str, Any]] = identity_info.value + if identity_data is None: return None + return ChainIdentity.from_dict(decode_hex_identity_dict(identity_data)) async def recycle( self, @@ -5707,20 +5659,25 @@ async def subnet( if not block_hash and reuse_block: block_hash = self.substrate.last_block_hash - query = await self.substrate.runtime_call( - "SubnetInfoRuntimeApi", - "get_dynamic_info", - params=[netuid], - block_hash=block_hash, - ) - price = await self.get_subnet_price( - netuid=netuid, - block=block, - block_hash=block_hash, - reuse_block=reuse_block, + decoded: Optional[DynamicInfoResponse] + price: Optional[Balance] + decoded, price = await asyncio.gather( + self.substrate.runtime_call( + "SubnetInfoRuntimeApi", + "get_dynamic_info", + params=[netuid], + block_hash=block_hash, + ), + self.get_subnet_price( + netuid=netuid, + block=block, + block_hash=block_hash, + reuse_block=reuse_block, + ), + return_exceptions=True, ) - if isinstance(decoded := query.decode(), dict): + if isinstance(decoded, dict): if isinstance(price, (SubstrateRequestException, ValueError)): price = None return DynamicInfo.from_dict({**decoded, "price": price}) @@ -5754,7 +5711,6 @@ async def subnet_exists( storage_function="NetworksAdded", params=[netuid], block_hash=block_hash, - reuse_block_hash=reuse_block, ) return getattr(result, "value", False) @@ -5921,11 +5877,10 @@ async def weights( storage_function="Weights", params=[storage_index], block_hash=block_hash, - reuse_block_hash=reuse_block, ) w_map = [] async for uid, w in w_map_encoded: - w_map.append((uid, w.value)) + w_map.append((uid, w)) return w_map @@ -6220,7 +6175,7 @@ async def get_extrinsic_fee( # Estimate fee before sending a transfer call = await subtensor.compose_call( call_module="Balances", - call_function="transfer", + call_function="transfer_allow_death", call_params={"dest": destination_ss58, "value": amount.rao} ) fee = await subtensor.get_extrinsic_fee(call=call, keypair=wallet.coldkey) @@ -7115,6 +7070,50 @@ async def dispute_coldkey_swap( wait_for_revealed_execution=wait_for_revealed_execution, ) + async def clear_coldkey_swap_announcement( + self, + wallet: "Wallet", + *, + mev_protection: bool = DEFAULT_MEV_PROTECTION, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + wait_for_revealed_execution: bool = True, + ) -> ExtrinsicResponse: + """ + Clears (withdraws) a pending coldkey swap announcement. + + Callable by the coldkey that has an active, undisputed swap announcement. The reannouncement delay must have + elapsed past the execution block before the announcement can be cleared. + + Parameters: + wallet: Bittensor wallet object (should be the current coldkey with an active announcement). + mev_protection: If ``True``, encrypts and submits the transaction through the MEV Shield pallet. + period: The number of blocks during which the transaction will remain valid. + raise_error: Raises a relevant exception rather than returning ``False`` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + wait_for_revealed_execution: Whether to wait for the revealed execution if mev_protection used. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + Notes: + - The coldkey must have an active, undisputed swap announcement. + - The reannouncement delay must have elapsed past the execution block. + """ + return await clear_coldkey_swap_announcement_extrinsic( + subtensor=self, + wallet=wallet, + mev_protection=mev_protection, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + async def dissolve_crowdloan( self, wallet: "Wallet", @@ -7818,14 +7817,7 @@ async def register( self: "AsyncSubtensor", wallet: "Wallet", netuid: int, - max_allowed_attempts: int = 3, - output_in_place: bool = False, - cuda: bool = False, - dev_id: Union[list[int], int] = 0, - tpb: int = 256, - num_processes: Optional[int] = None, - update_interval: Optional[int] = None, - log_verbose: bool = False, + limit_price: Optional[Balance] = None, *, mev_protection: bool = DEFAULT_MEV_PROTECTION, period: Optional[int] = DEFAULT_PERIOD, @@ -7834,66 +7826,131 @@ async def register( wait_for_finalization: bool = True, wait_for_revealed_execution: bool = True, ) -> ExtrinsicResponse: - """ - Registers a neuron on the Bittensor subnet with provided netuid using the provided wallet. + """Registers a neuron on the Bittensor network by recycling TAO, with automatic price protection. - Registration is a critical step for a neuron to become an active participant in the network, enabling it to - stake, set weights, and receive incentives. + Uses ``register_limit`` under the hood. If ``limit_price`` is not provided, it is automatically + calculated as the current recycle (burn) cost plus a 0.5% tolerance to protect against price fluctuations. + + For root subnet (``netuid == 0``), delegates to ``root_register_extrinsic``. Parameters: wallet: The wallet associated with the neuron to be registered. netuid: The unique identifier of the subnet. - max_allowed_attempts: Maximum number of attempts to register the wallet. - output_in_place: If `True`, prints the progress of the proof of work to the console in-place. Meaning the - progress is printed on the same lines. - cuda: If `true`, the wallet should be registered using CUDA device(s). - dev_id: The CUDA device id to use, or a list of device ids. - tpb: The number of threads per block (CUDA). - num_processes: The number of processes to use to register. - update_interval: The number of nonces to solve between updates. - log_verbose: If `true`, the registration process will log more information. - mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect + limit_price: Maximum acceptable burn price as a Balance instance. If ``None``, automatically calculated + as ``recycle * 1.005`` (0.5% tolerance). If the on-chain burn price exceeds this value, the + transaction will fail with RegistrationPriceLimitExceeded. + mev_protection: If ``True``, encrypts and submits the transaction through the MEV Shield pallet to protect against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators - decrypt and execute it. If `False`, submits the transaction directly without encryption. + decrypt and execute it. If ``False``, submits the transaction directly without encryption. period: The number of blocks during which the transaction will remain valid after it's submitted. If the transaction is not included in a block within that number of blocks, it will expire and be rejected. You can think of it as an expiration date for the transaction. - raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. - wait_for_inclusion: Whether to wait for the inclusion of the transaction. - wait_for_finalization: Whether to wait for the finalization of the transaction. + raise_error: Raises a relevant exception rather than returning ``False`` if unsuccessful. + wait_for_inclusion: Waits for the transaction to be included in a block. + wait_for_finalization: Waits for the transaction to be finalized on the blockchain. wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. Returns: ExtrinsicResponse: The result object of the extrinsic execution. - This function facilitates the entry of new neurons into the network, supporting the decentralized growth and - scalability of the Bittensor ecosystem. + Notes: + - Rate Limits: + """ + async with self: + if netuid == 0: + return await root_register_extrinsic( + subtensor=self, + wallet=wallet, + mev_protection=mev_protection, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + + if limit_price is not None: + check_balance_amount(limit_price) + else: + recycle = await self.recycle(netuid=netuid) + if recycle is None: + return ExtrinsicResponse( + False, f"Subnet {netuid} does not exist." + ).with_log() + limit_price = Balance.from_rao(recycle.rao * 1005 // 1000) + + return await register_limit_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + limit_price=limit_price, + mev_protection=mev_protection, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + + async def register_limit( + self, + wallet: "Wallet", + netuid: int, + limit_price: Balance, + *, + mev_protection: bool = DEFAULT_MEV_PROTECTION, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + wait_for_revealed_execution: bool = True, + ) -> ExtrinsicResponse: + """ + Registers a neuron on the Bittensor network by recycling TAO, with a maximum burn price limit. + + Unlike ``burned_register``, this method includes a ``limit_price`` parameter that ensures the registration + will only proceed if the current on-chain burn price does not exceed the specified maximum. This protects + against unexpected price spikes between reading the price and submitting the transaction. + + Parameters: + wallet: The wallet associated with the neuron to be registered. + netuid: The unique identifier of the subnet. + limit_price: Maximum acceptable burn price as a Balance instance. If the on-chain burn price exceeds + this value, the transaction will fail with RegistrationPriceLimitExceeded. + mev_protection: If ``True``, encrypts and submits the transaction through the MEV Shield pallet to protect + against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators + decrypt and execute it. If ``False``, submits the transaction directly without encryption. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You + can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning ``False`` if unsuccessful. + wait_for_inclusion: Waits for the transaction to be included in a block. + wait_for_finalization: Waits for the transaction to be finalized on the blockchain. + wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. Notes: - Rate Limits: """ - return await register_extrinsic( - subtensor=self, - wallet=wallet, - netuid=netuid, - max_allowed_attempts=max_allowed_attempts, - tpb=tpb, - update_interval=update_interval, - num_processes=num_processes, - cuda=cuda, - dev_id=dev_id, - output_in_place=output_in_place, - log_verbose=log_verbose, - mev_protection=mev_protection, - period=period, - raise_error=raise_error, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - wait_for_revealed_execution=wait_for_revealed_execution, - ) + check_balance_amount(limit_price) + async with self: + return await register_limit_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + limit_price=limit_price, + mev_protection=mev_protection, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) async def register_subnet( - self: "AsyncSubtensor", + self, wallet: "Wallet", *, mev_protection: bool = DEFAULT_MEV_PROTECTION, diff --git a/bittensor/core/axon.py b/bittensor/core/axon.py index f7227d6233..0895cf708a 100644 --- a/bittensor/core/axon.py +++ b/bittensor/core/axon.py @@ -21,6 +21,7 @@ from fastapi.responses import JSONResponse from fastapi.routing import serialize_response from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint +from starlette.types import ASGIApp from starlette.requests import Request from starlette.responses import Response @@ -1097,7 +1098,7 @@ class AxonMiddleware(BaseHTTPMiddleware): such as response header updating and logging. """ - def __init__(self, app: "AxonMiddleware", axon: "Axon"): + def __init__(self, app: ASGIApp, axon: "Axon"): """ Initialize the AxonMiddleware class. diff --git a/bittensor/core/chain_data/__init__.py b/bittensor/core/chain_data/__init__.py index 0982c5cde2..f66c3bd0a6 100644 --- a/bittensor/core/chain_data/__init__.py +++ b/bittensor/core/chain_data/__init__.py @@ -37,7 +37,7 @@ from .subnet_identity import SubnetIdentity from .subnet_info import SubnetInfo from .subnet_state import SubnetState -from .utils import decode_account_id, process_stake_data +from .utils import process_stake_data from .weight_commit_info import WeightCommitInfo ProposalCallData = GenericCall @@ -78,6 +78,5 @@ "SubnetInfo", "SubnetState", "WeightCommitInfo", - "decode_account_id", "process_stake_data", ] diff --git a/bittensor/core/chain_data/coldkey_swap.py b/bittensor/core/chain_data/coldkey_swap.py index e638569e53..7407a3abb9 100644 --- a/bittensor/core/chain_data/coldkey_swap.py +++ b/bittensor/core/chain_data/coldkey_swap.py @@ -1,9 +1,6 @@ from dataclasses import asdict, dataclass, fields from typing import Optional - -from async_substrate_interface.types import ScaleObj - -from bittensor.core.chain_data.utils import decode_account_id +from scalecodec.base import ScaleType @dataclass @@ -34,7 +31,7 @@ class ColdkeySwapAnnouncementInfo: @classmethod def from_query( - cls, coldkey_ss58: str, query: "ScaleObj" + cls, coldkey_ss58: str, query: ScaleType ) -> Optional["ColdkeySwapAnnouncementInfo"]: """ Creates a ColdkeySwapAnnouncementInfo object from a Substrate query result. @@ -46,11 +43,10 @@ def from_query( Returns: ColdkeySwapAnnouncementInfo if announcement exists, None otherwise. """ - if not getattr(query, "value", None): + if query.value is None: return None - - execution_block = query.value[0] - new_coldkey_hash = "0x" + bytes(query.value[1][0]).hex() + qv: tuple[int, str] = query.value + execution_block, new_coldkey_hash = qv return cls( coldkey=coldkey_ss58, execution_block=execution_block, @@ -69,7 +65,7 @@ def from_record(cls, record: tuple) -> "ColdkeySwapAnnouncementInfo": Returns: ColdkeySwapAnnouncementInfo object with announcement details for the coldkey from the record. """ - coldkey_ss58 = decode_account_id(record[0]) + coldkey_ss58 = record[0] announcement_data = record[1] return cls.from_query(coldkey_ss58, announcement_data) @@ -98,7 +94,7 @@ class ColdkeySwapDisputeInfo: @classmethod def from_query( - cls, coldkey_ss58: str, query: "ScaleObj" + cls, coldkey_ss58: str, query: ScaleType ) -> Optional["ColdkeySwapDisputeInfo"]: """ Creates a ColdkeySwapDisputeInfo object from a Substrate query result. @@ -115,7 +111,7 @@ def from_query( return cls(coldkey=coldkey_ss58, disputed_block=int(query.value)) @classmethod - def from_record(cls, record: tuple) -> "ColdkeySwapDisputeInfo": + def from_record(cls, record: tuple[str, int]) -> "ColdkeySwapDisputeInfo": """ Creates a ColdkeySwapDisputeInfo object from a query_map record. @@ -126,12 +122,7 @@ def from_record(cls, record: tuple) -> "ColdkeySwapDisputeInfo": Returns: ColdkeySwapDisputeInfo object with dispute details for the coldkey from the record. """ - coldkey_ss58 = decode_account_id(record[0]) - val = record[1] - disputed_block = ( - int(val.value) if getattr(val, "value", None) is not None else int(val) - ) - return cls(coldkey=coldkey_ss58, disputed_block=disputed_block) + return cls(coldkey=record[0], disputed_block=int(record[1])) @dataclass diff --git a/bittensor/core/chain_data/crowdloan_info.py b/bittensor/core/chain_data/crowdloan_info.py index 83e119ffbd..701972bad2 100644 --- a/bittensor/core/chain_data/crowdloan_info.py +++ b/bittensor/core/chain_data/crowdloan_info.py @@ -1,9 +1,11 @@ from dataclasses import dataclass -from typing import Optional +from typing import Optional, TYPE_CHECKING -from bittensor.core.chain_data.utils import decode_account_id from bittensor.utils.balance import Balance +if TYPE_CHECKING: + from bittensor.core.types import CrowdloansResponse + @dataclass class CrowdloanInfo: @@ -38,27 +40,23 @@ class CrowdloanInfo: funds_account: str raised: Balance target_address: Optional[str] - call: Optional[str] + call: Optional[dict] finalized: bool contributors_count: int @classmethod - def from_dict(cls, idx: int, data: dict) -> "CrowdloanInfo": + def from_dict(cls, idx: int, data: "CrowdloansResponse") -> "CrowdloanInfo": """Returns a CrowdloanInfo object from decoded chain data.""" return cls( id=idx, - creator=decode_account_id(data["creator"]), + creator=data["creator"], deposit=Balance.from_rao(data["deposit"]), min_contribution=Balance.from_rao(data["min_contribution"]), end=data["end"], cap=Balance.from_rao(data["cap"]), - funds_account=decode_account_id(data["funds_account"]) - if data.get("funds_account") - else None, + funds_account=data["funds_account"], raised=Balance.from_rao(data["raised"]), - target_address=decode_account_id(data.get("target_address")) - if data.get("target_address") - else None, + target_address=data.get("target_address"), call=data.get("call") if data.get("call") else None, finalized=data["finalized"], contributors_count=data["contributors_count"], diff --git a/bittensor/core/chain_data/delegate_info.py b/bittensor/core/chain_data/delegate_info.py index 91301b6034..d12696deb8 100644 --- a/bittensor/core/chain_data/delegate_info.py +++ b/bittensor/core/chain_data/delegate_info.py @@ -2,7 +2,6 @@ from typing import Optional from bittensor.core.chain_data.info_base import InfoBase -from bittensor.core.chain_data.utils import decode_account_id from bittensor.utils import u16_normalized_float from bittensor.utils.balance import Balance @@ -47,14 +46,14 @@ class DelegateInfo(DelegateInfoBase): @classmethod def _from_dict(cls, decoded: dict) -> Optional["DelegateInfo"]: - hotkey = decode_account_id(decoded.get("delegate_ss58")) - owner = decode_account_id(decoded.get("owner_ss58")) + hotkey = decoded.get("delegate_ss58") + owner = decoded.get("owner_ss58") nominators = {} total_stake_by_netuid = {} for raw_nominator, raw_stakes in decoded.get("nominators", []): - nominator_ss58 = decode_account_id(raw_nominator) + nominator_ss58 = raw_nominator stakes = { int(netuid): Balance.from_rao(stake_amt).set_unit(int(netuid)) for (netuid, stake_amt) in raw_stakes @@ -96,8 +95,8 @@ def _from_dict( cls, decoded: tuple[dict, tuple[int, int]] ) -> Optional["DelegatedInfo"]: delegate_info, (netuid, stake) = decoded - hotkey = decode_account_id(delegate_info.get("delegate_ss58")) - owner = decode_account_id(delegate_info.get("owner_ss58")) + hotkey = delegate_info.get("delegate_ss58") + owner = delegate_info.get("owner_ss58") return cls( hotkey_ss58=hotkey, owner_ss58=owner, diff --git a/bittensor/core/chain_data/delegate_info_lite.py b/bittensor/core/chain_data/delegate_info_lite.py index 06666769dc..262c708619 100644 --- a/bittensor/core/chain_data/delegate_info_lite.py +++ b/bittensor/core/chain_data/delegate_info_lite.py @@ -1,7 +1,6 @@ from dataclasses import dataclass from bittensor.core.chain_data.info_base import InfoBase -from bittensor.core.chain_data.utils import decode_account_id from bittensor.utils import u16_normalized_float from bittensor.utils.balance import Balance @@ -34,10 +33,10 @@ class DelegateInfoLite(InfoBase): @classmethod def _from_dict(cls, decoded: dict) -> "DelegateInfoLite": return DelegateInfoLite( - delegate_ss58=decode_account_id(decoded["delegate_ss58"]), + delegate_ss58=decoded["delegate_ss58"], take=u16_normalized_float(decoded["take"]), nominators=decoded["nominators"], - owner_ss58=decode_account_id(decoded["owner_ss58"]), + owner_ss58=decoded["owner_ss58"], registrations=decoded["registrations"], validator_permits=decoded["validator_permits"], return_per_1000=Balance.from_rao(decoded["return_per_1000"]), diff --git a/bittensor/core/chain_data/dynamic_info.py b/bittensor/core/chain_data/dynamic_info.py index 4d80d79dce..92373ecf1b 100644 --- a/bittensor/core/chain_data/dynamic_info.py +++ b/bittensor/core/chain_data/dynamic_info.py @@ -7,7 +7,6 @@ from typing import Optional, Union from bittensor.core.chain_data.info_base import InfoBase -from bittensor.core.chain_data.utils import decode_account_id from bittensor.core.chain_data.subnet_identity import SubnetIdentity from bittensor.utils.balance import Balance, fixed_to_float @@ -52,8 +51,8 @@ def _from_dict(cls, decoded: dict) -> "DynamicInfo": True if int(decoded["netuid"]) > 0 else False ) # Root is not dynamic - owner_hotkey = decode_account_id(decoded["owner_hotkey"]) - owner_coldkey = decode_account_id(decoded["owner_coldkey"]) + owner_hotkey = decoded["owner_hotkey"] + owner_coldkey = decoded["owner_coldkey"] emission = Balance.from_rao(decoded["emission"]).set_unit(0) alpha_in = Balance.from_rao(decoded["alpha_in"]).set_unit(netuid) @@ -77,18 +76,15 @@ def _from_dict(cls, decoded: dict) -> "DynamicInfo": if subnet_identity := decoded.get("subnet_identity"): # we need to check it for keep backwards compatibility - logo_bytes = subnet_identity.get("logo_url") - si_logo_url = bytes(logo_bytes).decode() if logo_bytes else None - subnet_identity = SubnetIdentity( - subnet_name=bytes(subnet_identity["subnet_name"]).decode(), - github_repo=bytes(subnet_identity["github_repo"]).decode(), - subnet_contact=bytes(subnet_identity["subnet_contact"]).decode(), - subnet_url=bytes(subnet_identity["subnet_url"]).decode(), - logo_url=si_logo_url, - discord=bytes(subnet_identity["discord"]).decode(), - description=bytes(subnet_identity["description"]).decode(), - additional=bytes(subnet_identity["additional"]).decode(), + subnet_name=subnet_identity["subnet_name"], + github_repo=subnet_identity["github_repo"], + subnet_contact=subnet_identity["subnet_contact"], + subnet_url=subnet_identity["subnet_url"], + logo_url=subnet_identity.get("logo_url", ""), + discord=subnet_identity["discord"], + description=subnet_identity["description"], + additional=subnet_identity["additional"], ) else: subnet_identity = None diff --git a/bittensor/core/chain_data/metagraph_info.py b/bittensor/core/chain_data/metagraph_info.py index d82292fb1e..31107310d6 100644 --- a/bittensor/core/chain_data/metagraph_info.py +++ b/bittensor/core/chain_data/metagraph_info.py @@ -6,11 +6,11 @@ from bittensor.core.chain_data.chain_identity import ChainIdentity from bittensor.core.chain_data.info_base import InfoBase from bittensor.core.chain_data.subnet_identity import SubnetIdentity -from bittensor.core.chain_data.utils import decode_account_id from bittensor.utils import ( get_netuid_and_mechid_by_storage_index, u64_normalized_float as u64tf, u16_normalized_float as u16tf, + deprecated_message, ) from bittensor.utils.balance import Balance, fixed_to_float @@ -25,8 +25,7 @@ def get_selective_metagraph_commitments( if commitments := decoded.get("commitments"): result = [] for commitment in commitments: - account_id_bytes, commitment_bytes = commitment - hotkey = decode_account_id(account_id_bytes) + hotkey, commitment_bytes = commitment commitment = bytes( commitment_bytes[SELECTIVE_METAGRAPH_COMMITMENTS_OFFSET:] ).decode("utf-8", errors="ignore") @@ -52,6 +51,9 @@ def process_nested( data: Union[tuple, dict], chr_transform ) -> Optional[Union[list, dict]]: """Processes nested data structures by applying a transformation function to their elements.""" + deprecated_message( + "This function is deprecated as it is no longer needed with the new decoding." + ) if isinstance(data, (list, tuple)): if len(data) > 0: return [ @@ -194,11 +196,6 @@ def _from_dict(cls, decoded: dict) -> "MetagraphInfo": if decoded.get("identities") is not None: ii_list.append("identities") - for key in ii_list: - raw_data = decoded.get(key) - processed = process_nested(raw_data, _chr_str) - decoded.update({key: processed}) - return cls( # Subnet index netuid=_netuid, @@ -209,16 +206,8 @@ def _from_dict(cls, decoded: dict) -> "MetagraphInfo": identity=decoded["identity"], network_registered_at=decoded["network_registered_at"], # Keys for owner. - owner_hotkey=( - decode_account_id(decoded["owner_hotkey"][0]) - if decoded.get("owner_hotkey") is not None - else None - ), - owner_coldkey=( - decode_account_id(decoded["owner_coldkey"][0]) - if decoded.get("owner_coldkey") is not None - else None - ), + owner_hotkey=decoded.get("owner_hotkey"), + owner_coldkey=decoded.get("owner_coldkey"), # Tempo terms. block=decoded["block"], tempo=decoded["tempo"], @@ -312,16 +301,8 @@ def _from_dict(cls, decoded: dict) -> "MetagraphInfo": else None ), # Metagraph info. - hotkeys=( - [decode_account_id(ck) for ck in decoded.get("hotkeys", [])] - if decoded.get("hotkeys") is not None - else None - ), - coldkeys=( - [decode_account_id(hk) for hk in decoded.get("coldkeys", [])] - if decoded.get("coldkeys") is not None - else None - ), + hotkeys=decoded.get("hotkeys"), + coldkeys=decoded.get("coldkeys"), identities=decoded["identities"], axons=decoded.get("axons", []), active=decoded["active"], @@ -383,19 +364,13 @@ def _from_dict(cls, decoded: dict) -> "MetagraphInfo": ), # Dividend break down tao_dividends_per_hotkey=( - [ - (decode_account_id(alpha[0]), _tbwu(alpha[1])) - for alpha in decoded["tao_dividends_per_hotkey"] - ] - if decoded.get("tao_dividends_per_hotkey") is not None + [(ss58, _tbwu(alpha)) for (ss58, alpha) in tdph] + if (tdph := decoded.get("tao_dividends_per_hotkey")) is not None else None ), alpha_dividends_per_hotkey=( - [ - (decode_account_id(adphk[0]), _tbwu(adphk[1], _netuid)) - for adphk in decoded["alpha_dividends_per_hotkey"] - ] - if decoded.get("alpha_dividends_per_hotkey") is not None + [(ss58, _tbwu(adphk, _netuid)) for (ss58, adphk) in adph] + if (adph := decoded.get("alpha_dividends_per_hotkey")) is not None else None ), validators=[v for v in decoded["validators"]] diff --git a/bittensor/core/chain_data/neuron_info.py b/bittensor/core/chain_data/neuron_info.py index 6c3b89293d..b9bb43fd94 100644 --- a/bittensor/core/chain_data/neuron_info.py +++ b/bittensor/core/chain_data/neuron_info.py @@ -4,7 +4,7 @@ from bittensor.core.chain_data.axon_info import AxonInfo from bittensor.core.chain_data.info_base import InfoBase from bittensor.core.chain_data.prometheus_info import PrometheusInfo -from bittensor.core.chain_data.utils import decode_account_id, process_stake_data +from bittensor.core.chain_data.utils import process_stake_data from bittensor.utils import u16_normalized_float from bittensor.utils.balance import Balance @@ -120,8 +120,8 @@ def _from_dict(cls, decoded: Any) -> "NeuronInfo": """Returns a NeuronInfo object from decoded chain data.""" stake_dict = process_stake_data(decoded["stake"]) total_stake = sum(stake_dict.values()) if stake_dict else Balance(0) - coldkey = decode_account_id(decoded["coldkey"]) - hotkey = decode_account_id(decoded["hotkey"]) + coldkey = decoded["coldkey"] + hotkey = decoded["hotkey"] return NeuronInfo( active=decoded["active"], axon_info=AxonInfo.from_dict( diff --git a/bittensor/core/chain_data/neuron_info_lite.py b/bittensor/core/chain_data/neuron_info_lite.py index e1d8a33048..7ab5e29f44 100644 --- a/bittensor/core/chain_data/neuron_info_lite.py +++ b/bittensor/core/chain_data/neuron_info_lite.py @@ -4,7 +4,7 @@ from bittensor.core.chain_data.axon_info import AxonInfo from bittensor.core.chain_data.info_base import InfoBase from bittensor.core.chain_data.prometheus_info import PrometheusInfo -from bittensor.core.chain_data.utils import decode_account_id, process_stake_data +from bittensor.core.chain_data.utils import process_stake_data from bittensor.utils import u16_normalized_float from bittensor.utils.balance import Balance @@ -87,8 +87,8 @@ def get_null_neuron() -> "NeuronInfoLite": @classmethod def _from_dict(cls, decoded: Any) -> "NeuronInfoLite": """Returns a NeuronInfoLite object from decoded chain data.""" - coldkey = decode_account_id(decoded["coldkey"]) - hotkey = decode_account_id(decoded["hotkey"]) + coldkey = decoded["coldkey"] + hotkey = decoded["hotkey"] stake_dict = process_stake_data(decoded["stake"]) stake = sum(stake_dict.values()) if stake_dict else Balance(0) diff --git a/bittensor/core/chain_data/proposal_vote_data.py b/bittensor/core/chain_data/proposal_vote_data.py index 3cf5439955..98e221baa5 100644 --- a/bittensor/core/chain_data/proposal_vote_data.py +++ b/bittensor/core/chain_data/proposal_vote_data.py @@ -1,7 +1,6 @@ from dataclasses import dataclass from bittensor.core.chain_data.info_base import InfoBase -from bittensor.core.chain_data.utils import decode_account_id @dataclass @@ -19,9 +18,9 @@ class ProposalVoteData(InfoBase): @classmethod def from_dict(cls, proposal_dict: dict) -> "ProposalVoteData": return cls( - ayes=[decode_account_id(key) for key in proposal_dict["ayes"]], + ayes=proposal_dict["ayes"], end=proposal_dict["end"], index=proposal_dict["index"], - nays=[decode_account_id(key) for key in proposal_dict["nays"]], + nays=proposal_dict["nays"], threshold=proposal_dict["threshold"], ) diff --git a/bittensor/core/chain_data/proxy.py b/bittensor/core/chain_data/proxy.py index 8e663d26b3..662aad1e6b 100644 --- a/bittensor/core/chain_data/proxy.py +++ b/bittensor/core/chain_data/proxy.py @@ -1,8 +1,7 @@ from dataclasses import dataclass from enum import Enum -from typing import Any, Optional, Union +from typing import Any, Optional, Union, Sequence -from bittensor.core.chain_data.utils import decode_account_id from bittensor.utils.balance import Balance @@ -193,7 +192,7 @@ class ProxyInfo: delay: int @classmethod - def from_tuple(cls, data: tuple) -> list["ProxyInfo"]: + def from_tuple(cls, data: Sequence[dict[str, str | int]]) -> list["ProxyInfo"]: """Creates a list of ProxyInfo objects from chain proxy data. This method decodes the raw proxy data returned from the Proxy.Proxies storage function and creates @@ -210,8 +209,8 @@ def from_tuple(cls, data: tuple) -> list["ProxyInfo"]: """ return [ cls( - delegate=decode_account_id(proxy["delegate"]), - proxy_type=next(iter(proxy["proxy_type"].keys())), + delegate=proxy["delegate"], + proxy_type=proxy["proxy_type"], delay=proxy["delay"], ) for proxy in data @@ -239,13 +238,15 @@ def from_query(cls, query: Any) -> tuple[list["ProxyInfo"], Balance]: See: """ # proxies data is always in that path - proxies = query.value[0][0] + proxies = query.value[0] # balance data is always in that path balance = query.value[1] return cls.from_tuple(proxies), Balance.from_rao(balance) @classmethod - def from_query_map_record(cls, record: list) -> tuple[str, list["ProxyInfo"]]: + def from_query_map_record( + cls, record: tuple[str, tuple[list[dict[str, str | int]], int]] + ) -> tuple[str, list["ProxyInfo"]]: """Creates a dictionary mapping delegate addresses to their ProxyInfo lists from a query_map record. Processes a single record from a query_map call to the Proxy.Proxies storage function. Each record represents @@ -262,9 +263,9 @@ def from_query_map_record(cls, record: list) -> tuple[str, list["ProxyInfo"]]: """ # record[0] is the real account (key from storage) # record[1] is the value containing proxies data - real_account_ss58 = decode_account_id(record[0]) + real_account_ss58 = record[0] # list with proxies data is always in that path - proxy_data = cls.from_tuple(record[1].value[0][0]) + proxy_data = cls.from_tuple(record[1][0]) return real_account_ss58, proxy_data @@ -295,7 +296,9 @@ class ProxyAnnouncementInfo: height: int @classmethod - def from_dict(cls, data: tuple) -> list["ProxyAnnouncementInfo"]: + def from_dict( + cls, data: tuple[list[dict[str, str | int]], int] + ) -> list["ProxyAnnouncementInfo"]: """Creates a list of ProxyAnnouncementInfo objects from chain announcement data. This method decodes the raw announcement data returned from the Proxy.Announcements storage function. @@ -311,8 +314,8 @@ def from_dict(cls, data: tuple) -> list["ProxyAnnouncementInfo"]: """ return [ cls( - real=decode_account_id(next(iter(annt["real"]))), - call_hash="0x" + bytes(next(iter(annt["call_hash"]))).hex(), + real=annt["real"], + call_hash=annt["call_hash"], height=annt["height"], ) for annt in data[0] @@ -320,7 +323,7 @@ def from_dict(cls, data: tuple) -> list["ProxyAnnouncementInfo"]: @classmethod def from_query_map_record( - cls, record: tuple + cls, record: tuple[str, tuple[list[dict[str, str | int]], int]] ) -> tuple[str, list["ProxyAnnouncementInfo"]]: """Returns a list of ProxyAnnouncementInfo objects from a tuple of announcements data. @@ -335,9 +338,9 @@ def from_query_map_record( """ # record[0] is the real account (key from storage) # record[1] is the value containing announcements data - delegate = decode_account_id(record[0]) + delegate = record[0] # list with proxies data is always in that path - announcements_data = cls.from_dict(record[1].value[0]) + announcements_data = cls.from_dict(record[1]) return delegate, announcements_data diff --git a/bittensor/core/chain_data/stake_info.py b/bittensor/core/chain_data/stake_info.py index 4f52ddfe2f..42d397c53f 100644 --- a/bittensor/core/chain_data/stake_info.py +++ b/bittensor/core/chain_data/stake_info.py @@ -1,7 +1,6 @@ from dataclasses import dataclass from bittensor.core.chain_data.info_base import InfoBase -from bittensor.core.chain_data.utils import decode_account_id from bittensor.utils.balance import Balance @@ -30,8 +29,8 @@ def from_dict(cls, decoded: dict) -> "StakeInfo": """Returns a StakeInfo object from decoded chain data.""" netuid = decoded["netuid"] return cls( - hotkey_ss58=decode_account_id(decoded["hotkey"]), - coldkey_ss58=decode_account_id(decoded["coldkey"]), + hotkey_ss58=decoded["hotkey"], + coldkey_ss58=decoded["coldkey"], netuid=int(netuid), stake=Balance.from_rao(decoded["stake"]).set_unit(netuid), locked=Balance.from_rao(decoded["locked"]).set_unit(netuid), diff --git a/bittensor/core/chain_data/subnet_info.py b/bittensor/core/chain_data/subnet_info.py index 978dab29f7..765dae6bb6 100644 --- a/bittensor/core/chain_data/subnet_info.py +++ b/bittensor/core/chain_data/subnet_info.py @@ -2,7 +2,6 @@ from typing import Any from bittensor.core.chain_data.info_base import InfoBase -from bittensor.core.chain_data.utils import decode_account_id from bittensor.utils import u16_normalized_float from bittensor.utils.balance import Balance @@ -50,7 +49,7 @@ def _from_dict(cls, decoded: Any) -> "SubnetInfo": min_allowed_weights=decoded["min_allowed_weights"], modality=decoded["network_modality"], netuid=decoded["netuid"], - owner_ss58=decode_account_id(decoded["owner"]), + owner_ss58=decoded["owner"], rho=decoded["rho"], scaling_law_power=decoded["scaling_law_power"], subnetwork_n=decoded["subnetwork_n"], diff --git a/bittensor/core/chain_data/subnet_state.py b/bittensor/core/chain_data/subnet_state.py index 95a38536e3..b554dae6f4 100644 --- a/bittensor/core/chain_data/subnet_state.py +++ b/bittensor/core/chain_data/subnet_state.py @@ -6,7 +6,6 @@ from dataclasses import dataclass from bittensor.core.chain_data.info_base import InfoBase -from bittensor.core.chain_data.utils import decode_account_id from bittensor.utils import u16_normalized_float from bittensor.utils.balance import Balance @@ -38,8 +37,8 @@ def _from_dict(cls, decoded: dict) -> "SubnetState": netuid = decoded["netuid"] return SubnetState( netuid=netuid, - hotkeys=[decode_account_id(hk) for hk in decoded.get("hotkeys", [])], - coldkeys=[decode_account_id(ck) for ck in decoded.get("coldkeys", [])], + hotkeys=decoded.get("hotkeys", []), + coldkeys=decoded.get("coldkeys", []), active=decoded["active"], validator_permit=decoded["validator_permit"], pruning_score=[ diff --git a/bittensor/core/chain_data/utils.py b/bittensor/core/chain_data/utils.py index 5374652d8c..0f142d597e 100644 --- a/bittensor/core/chain_data/utils.py +++ b/bittensor/core/chain_data/utils.py @@ -3,16 +3,15 @@ from enum import Enum from typing import Optional, Union, TYPE_CHECKING -from async_substrate_interface.types import ScaleObj -from bittensor_wallet.utils import SS58_FORMAT -from scalecodec.base import RuntimeConfiguration, ScaleBytes +from scalecodec import ScaleBytes +from scalecodec.base import RuntimeConfiguration from scalecodec.type_registry import load_type_registry_preset -from scalecodec.utils.ss58 import ss58_encode from bittensor.utils.balance import Balance if TYPE_CHECKING: from async_substrate_interface.sync_substrate import QueryMapResult + from bittensor.core.types import CommitmentOfResponse class ChainDataType(Enum): @@ -101,23 +100,6 @@ def from_scale_encoding_using_type_string( return obj.decode() -def decode_account_id(account_id_bytes: Union[bytes, str]) -> str: - """ - Decodes an AccountId from bytes to a Base64 string using SS58 encoding. - - Parameters: - account_id_bytes: The AccountId in bytes that needs to be decoded. - - Returns: - str: The decoded AccountId as a Base64 string. - """ - if isinstance(account_id_bytes, tuple) and isinstance(account_id_bytes[0], tuple): - account_id_bytes = account_id_bytes[0] - - # Convert the AccountId bytes to a Base64 string - return ss58_encode(bytes(account_id_bytes).hex(), SS58_FORMAT) - - def process_stake_data(stake_data: list) -> dict: """ Processes stake data to decode account IDs and convert stakes from rao to Balance objects. @@ -129,17 +111,17 @@ def process_stake_data(stake_data: list) -> dict: dict: A dictionary with account IDs as keys and their corresponding Balance objects as values. """ decoded_stake_data = {} - for account_id_bytes, stake_ in stake_data: - account_id = decode_account_id(account_id_bytes) + for account_id, stake_ in stake_data: decoded_stake_data.update({account_id: Balance.from_rao(stake_)}) return decoded_stake_data -def decode_metadata(metadata: dict) -> str: - commitment = metadata["info"]["fields"][0][0] - raw_bytes = next(iter(commitment.values())) - byte_tuple = raw_bytes[0] if raw_bytes else raw_bytes - return bytes(byte_tuple).decode("utf-8", errors="ignore") +def decode_metadata(metadata: "CommitmentOfResponse") -> str: + commitment = metadata["info"]["fields"][0] + if isinstance(commitment, str): + return "" + hex_: str = next(iter(commitment.values())) + return bytes.fromhex(hex_.removeprefix("0x")).decode("utf-8", errors="ignore") def decode_block(data: bytes) -> int: @@ -152,7 +134,7 @@ def decode_block(data: bytes) -> int: Returns: int: The decoded block. """ - return int(data.value) if isinstance(data, ScaleObj) else data + return int.from_bytes(data, byteorder="little") # TODO verify this is little endian def decode_revealed_commitment(encoded_data) -> tuple[int, str]: @@ -177,7 +159,10 @@ def scale_decode_offset(data: bytes) -> int: else: return 4 - com_bytes, revealed_block = encoded_data + com_hex: str + revealed_block: int + com_hex, revealed_block = encoded_data + com_bytes = bytes.fromhex(com_hex.removeprefix("0x")) offset = scale_decode_offset(com_bytes) revealed_commitment = bytes(com_bytes[offset:]).decode("utf-8", errors="ignore") @@ -196,6 +181,6 @@ def decode_revealed_commitment_with_hotkey( """ key, data = encoded_data - ss58_address = decode_account_id(next(iter(key))) - block_data = tuple(decode_revealed_commitment(p) for p in data.value) + ss58_address = key + block_data = tuple(decode_revealed_commitment(p) for p in data) return ss58_address, block_data diff --git a/bittensor/core/chain_data/weight_commit_info.py b/bittensor/core/chain_data/weight_commit_info.py index 814ef54aca..0c917c740e 100644 --- a/bittensor/core/chain_data/weight_commit_info.py +++ b/bittensor/core/chain_data/weight_commit_info.py @@ -1,5 +1,4 @@ from dataclasses import dataclass -from bittensor.core.chain_data.utils import decode_account_id from typing import Optional @@ -35,17 +34,17 @@ def from_vec_u8(cls, data: tuple) -> tuple[str, str, int]: This method is used when querying a block or block hash where storage functions `CRV3WeightCommitsV2` does not exist in Subtensor module. """ - account_id, commit_block, commit_data, round_number = data + account_id: str + commit_hex: str + round_number: int + account_id, commit_hex, round_number = data - account_id_ = account_id[0] if isinstance(account_id, tuple) else account_id - commit_data = commit_data[0] if isinstance(commit_data, tuple) else commit_data - commit_hex = "0x" + "".join(format(x, "02x") for x in commit_data) - - return decode_account_id(account_id_), commit_hex, round_number + return account_id, commit_hex, round_number @classmethod def from_vec_u8_v2(cls, data: tuple) -> tuple[str, int, str, int]: """ + # TODO no it does not Creates a WeightCommitInfo instance Parameters: @@ -54,13 +53,10 @@ def from_vec_u8_v2(cls, data: tuple) -> tuple[str, int, str, int]: Returns: WeightCommitInfo: A new instance with the decoded data """ - account_id, commit_block, commit_data, round_number = data - - account_id_ = account_id[0] if isinstance(account_id, tuple) else account_id - commit_block = ( - commit_block[0] if isinstance(commit_block, tuple) else commit_block - ) - commit_data = commit_data[0] if isinstance(commit_data, tuple) else commit_data - commit_hex = "0x" + "".join(format(x, "02x") for x in commit_data) + account_id: str + commit_block: int + commit_hex: str + round_number: int + account_id, commit_block, commit_hex, round_number = data - return decode_account_id(account_id_), commit_block, commit_hex, round_number + return account_id, commit_block, commit_hex, round_number diff --git a/bittensor/core/config.py b/bittensor/core/config.py index 2f979e2b65..483e15828e 100644 --- a/bittensor/core/config.py +++ b/bittensor/core/config.py @@ -23,7 +23,84 @@ from typing import Any, Optional from bittensor.core.settings import DEFAULTS import yaml -from munch import DefaultMunch + + +class DefaultMunch(dict): + """ + Dict with attribute-style access and a configurable default value. + + Drop-in replacement for munch.DefaultMunch using only the stdlib. + The default value (returned for missing keys) is stored on the instance + via ``object.__setattr__`` so it never collides with dict entries. + """ + + def __init__(self, default=None, *args, **kwargs): + object.__setattr__(self, "_munch_default", default) + super().__init__() + if args or kwargs: + source = dict(*args, **kwargs) + for k, v in source.items(): + self[k] = DefaultMunch.fromDict(v) if isinstance(v, dict) else v + + def __getattr__(self, key): + try: + return self[key] + except KeyError: + return object.__getattribute__(self, "_munch_default") + + def __setattr__(self, key, value): + self[key] = value + + def __delattr__(self, key): + try: + del self[key] + except KeyError: + raise AttributeError(key) + + def toDict(self): + """Recursively converts this object to a plain dict.""" + + def _convert(v): + return v.toDict() if isinstance(v, DefaultMunch) else v + + return {k: _convert(v) for k, v in self.items()} + + def __deepcopy__(self, memo): + """ + Explicit deepcopy so Python 3.10 never falls through to __reduce_ex__. + + Without this, ``copy.deepcopy`` on a dict subclass tries + ``getattr(x, "__reduce_ex__", None)``, which hits our ``__getattr__`` + and returns ``None`` (the default value) instead of the real method. + """ + new = DefaultMunch(object.__getattribute__(self, "_munch_default")) + memo[id(self)] = new + for k, v in self.items(): + new[deepcopy(k, memo)] = deepcopy(v, memo) + return new + + @classmethod + def fromDict(cls, d, _default=None): + """Recursively creates a DefaultMunch from a plain dict.""" + result = cls(_default) + for k, v in d.items(): + result[k] = cls.fromDict(v, _default) if isinstance(v, dict) else v + return result + + +def _class_to_dict(cls) -> dict: + """ + Recursively converts a class with class-level attributes to a plain dict. + + Skips private/dunder attributes. Nested classes become nested dicts. + Used to convert the `DEFAULTS` class from settings into a plain dict. + """ + result = {} + for k, v in vars(cls).items(): + if k.startswith("_"): + continue + result[k] = _class_to_dict(v) if isinstance(v, type) else v + return result def _filter_keys(obj): @@ -64,11 +141,14 @@ def __init__( default = deepcopy(default or DEFAULTS) if isinstance(default, DefaultMunch): - # Initialize Munch with defaults (dict-safe) super().__init__(None, default.toDict()) - else: - # if defaults passed as dict + elif isinstance(default, dict): super().__init__(None, default) + elif isinstance(default, type): + # DEFAULTS is a class with nested classes; convert to plain dict + super().__init__(None, _class_to_dict(default)) + else: + super().__init__(None) self.__is_set = {} diff --git a/bittensor/core/extrinsics/asyncex/coldkey_swap.py b/bittensor/core/extrinsics/asyncex/coldkey_swap.py index db44be4253..202c41fa6a 100644 --- a/bittensor/core/extrinsics/asyncex/coldkey_swap.py +++ b/bittensor/core/extrinsics/asyncex/coldkey_swap.py @@ -60,6 +60,7 @@ async def announce_coldkey_swap_extrinsic( - A swap cost is charged when making the first announcement (not when reannouncing). - After making an announcement, all transactions from the coldkey are blocked except for `swap_coldkey_announced`. - The swap can only be executed after the delay period has passed (check via `get_coldkey_swap_announcement`). + - The destination coldkey cannot have any staking hotkeys. It must be completely new without any staking activity. - See: """ try: @@ -68,6 +69,17 @@ async def announce_coldkey_swap_extrinsic( ).success: return unlocked + staking_hotkeys = await subtensor.get_staking_hotkeys(new_coldkey_ss58) + if staking_hotkeys: + error_msg = "Destination coldkey cannot have any staking hotkeys. Please use a new coldkey for the swap." + if raise_error: + raise ValueError(error_msg) + return ExtrinsicResponse( + success=False, + message=error_msg, + extrinsic_receipt=None, + ) + # Compute hash of new coldkey new_coldkey = Keypair(ss58_address=new_coldkey_ss58) new_coldkey_hash = compute_coldkey_hash(new_coldkey) @@ -184,6 +196,75 @@ async def dispute_coldkey_swap_extrinsic( return ExtrinsicResponse.from_exception(raise_error=raise_error, error=error) +async def clear_coldkey_swap_announcement_extrinsic( + subtensor: "AsyncSubtensor", + wallet: "Wallet", + *, + mev_protection: bool = DEFAULT_MEV_PROTECTION, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + wait_for_revealed_execution: bool = True, +) -> ExtrinsicResponse: + """ + Clears (withdraws) a pending coldkey swap announcement. + + Callable by the coldkey that has an active, undisputed swap announcement. The reannouncement delay must have + elapsed past the execution block before the announcement can be cleared. + + Parameters: + subtensor: AsyncSubtensor instance with the connection to the chain. + wallet: Bittensor wallet object (should be the current coldkey with an active announcement). + mev_protection: If ``True``, encrypts and submits the transaction through the MEV Shield pallet. + period: The number of blocks during which the transaction will remain valid. + raise_error: Raises a relevant exception rather than returning ``False`` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + wait_for_revealed_execution: Whether to wait for the revealed execution if mev_protection used. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + Notes: + - The coldkey must have an active, undisputed swap announcement. + - The reannouncement delay must have elapsed past the execution block. + """ + try: + if not ( + unlocked := ExtrinsicResponse.unlock_wallet(wallet, raise_error) + ).success: + return unlocked + + call = await SubtensorModule(subtensor).clear_coldkey_swap_announcement() + + if mev_protection: + response = await submit_encrypted_extrinsic( + subtensor=subtensor, + wallet=wallet, + call=call, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + else: + response = await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + period=period, + raise_error=raise_error, + ) + + return response + + except Exception as error: + return ExtrinsicResponse.from_exception(raise_error=raise_error, error=error) + + async def swap_coldkey_announced_extrinsic( subtensor: "AsyncSubtensor", wallet: "Wallet", diff --git a/bittensor/core/extrinsics/asyncex/registration.py b/bittensor/core/extrinsics/asyncex/registration.py index adcd78c992..3457fd915a 100644 --- a/bittensor/core/extrinsics/asyncex/registration.py +++ b/bittensor/core/extrinsics/asyncex/registration.py @@ -1,17 +1,17 @@ """ -This module provides async functionalities for registering a wallet with the subtensor network using Proof-of-Work (PoW). +This module provides async functionalities for registering a wallet with the subtensor network. """ import asyncio -from typing import Optional, Union, TYPE_CHECKING +from typing import Optional, TYPE_CHECKING -from bittensor.core.errors import RegistrationError +from bittensor.core.errors import BalanceTypeError, RegistrationError from bittensor.core.extrinsics.asyncex.mev_shield import submit_encrypted_extrinsic from bittensor.core.extrinsics.pallets import SubtensorModule from bittensor.core.settings import DEFAULT_MEV_PROTECTION from bittensor.core.types import ExtrinsicResponse +from bittensor.utils.balance import Balance from bittensor.utils.btlogging import logging -from bittensor.utils.registration import create_pow_async, log_no_torch_error, torch if TYPE_CHECKING: from bittensor_wallet import Wallet @@ -158,9 +158,11 @@ async def burned_register_extrinsic( return ExtrinsicResponse.from_exception(raise_error=raise_error, error=error) -async def register_subnet_extrinsic( +async def register_limit_extrinsic( subtensor: "AsyncSubtensor", wallet: "Wallet", + netuid: int, + limit_price: Balance, *, mev_protection: bool = DEFAULT_MEV_PROTECTION, period: Optional[int] = None, @@ -169,12 +171,14 @@ async def register_subnet_extrinsic( wait_for_finalization: bool = True, wait_for_revealed_execution: bool = True, ) -> ExtrinsicResponse: - """ - Registers a new subnetwork on the Bittensor blockchain asynchronously. + """Registers the wallet to chain by recycling TAO, with a maximum burn price limit. Parameters: - subtensor: The subtensor interface to send the extrinsic. - wallet: The wallet to be used for subnet registration. + subtensor: Subtensor instance. + wallet: Bittensor wallet object. + netuid: The ``netuid`` of the subnet to register on. + limit_price: Maximum acceptable burn price as a Balance instance. If the on-chain burn price exceeds + this value, the transaction will fail with RegistrationPriceLimitExceeded. mev_protection: If True, encrypts and submits the transaction through the MEV Shield pallet to protect against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators decrypt and execute it. If False, submits the transaction directly without encryption. @@ -197,17 +201,44 @@ async def register_subnet_extrinsic( ).success: return unlocked - balance = await subtensor.get_balance(wallet.coldkeypub.ss58_address) - burn_cost = await subtensor.get_subnet_burn_cost() + if not isinstance(limit_price, Balance): + raise BalanceTypeError("`limit_price` must be an instance of Balance.") - if burn_cost > balance: + block_hash = await subtensor.substrate.get_chain_head() + if not await subtensor.subnet_exists(netuid=netuid, block_hash=block_hash): return ExtrinsicResponse( - False, - f"Insufficient balance {balance} to register subnet. Current burn cost is {burn_cost} TAO.", + False, f"Subnet {netuid} does not exist." ).with_log() - call = await SubtensorModule(subtensor).register_network( - hotkey=wallet.hotkey.ss58_address + neuron, old_balance, recycle_amount = await asyncio.gather( + subtensor.get_neuron_for_pubkey_and_subnet( + netuid=netuid, + hotkey_ss58=wallet.hotkey.ss58_address, + block_hash=block_hash, + ), + subtensor.get_balance( + address=wallet.coldkeypub.ss58_address, block_hash=block_hash + ), + subtensor.recycle(netuid=netuid, block_hash=block_hash), + ) + + if not neuron.is_null: + message = "Already registered." + logging.debug(f"[green]{message}[/green]") + logging.debug(f"\t\tuid: [blue]{neuron.uid}[/blue]") + logging.debug(f"\t\tnetuid: [blue]{neuron.netuid}[/blue]") + logging.debug(f"\t\thotkey: [blue]{neuron.hotkey}[/blue]") + logging.debug(f"\t\tcoldkey: [blue]{neuron.coldkey}[/blue]") + return ExtrinsicResponse( + message=message, data={"neuron": neuron, "old_balance": old_balance} + ) + + logging.debug(f"Recycling {recycle_amount} to register on subnet:{netuid}") + + call = await SubtensorModule(subtensor).register_limit( + netuid=netuid, + hotkey=wallet.hotkey.ss58_address, + limit_price=limit_price.rao, ) if mev_protection: @@ -230,33 +261,52 @@ async def register_subnet_extrinsic( wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, ) - - if not wait_for_finalization and not wait_for_inclusion: + extrinsic_fee = response.extrinsic_fee + logging.debug( + f"The registration fee for SN #[blue]{netuid}[/blue] is [blue]{extrinsic_fee}[/blue]." + ) + if not response.success: + logging.error(f"[red]{response.message}[/red]") + await asyncio.sleep(0.5) return response - if response.success: - logging.debug("[green]Successfully registered subnet.[/green]") + new_balance = await subtensor.get_balance( + address=wallet.coldkeypub.ss58_address + ) + + logging.debug( + f"Balance: [blue]{old_balance}[/blue] :arrow_right: [green]{new_balance}[/green]" + ) + is_registered = await subtensor.is_hotkey_registered( + netuid=netuid, hotkey_ss58=wallet.hotkey.ss58_address + ) + + response.data = { + "neuron": neuron, + "balance_before": old_balance, + "balance_after": new_balance, + "recycle_amount": recycle_amount, + } + + if is_registered: + logging.debug("[green]Registered.[/green]") return response - logging.error(f"Failed to register subnet: {response.message}") - return response + message = f"Neuron with hotkey {wallet.hotkey.ss58_address} not found in subnet {netuid} after registration." + return ExtrinsicResponse( + success=False, + message=message, + extrinsic=response.extrinsic, + error=RegistrationError(message), + ).with_log() except Exception as error: return ExtrinsicResponse.from_exception(raise_error=raise_error, error=error) -async def register_extrinsic( +async def register_subnet_extrinsic( subtensor: "AsyncSubtensor", wallet: "Wallet", - netuid: int, - max_allowed_attempts: int = 3, - output_in_place: bool = True, - cuda: bool = False, - dev_id: Union[list[int], int] = 0, - tpb: int = 256, - num_processes: Optional[int] = None, - update_interval: Optional[int] = None, - log_verbose: bool = False, *, mev_protection: bool = DEFAULT_MEV_PROTECTION, period: Optional[int] = None, @@ -265,23 +315,12 @@ async def register_extrinsic( wait_for_finalization: bool = True, wait_for_revealed_execution: bool = True, ) -> ExtrinsicResponse: - """Registers a neuron on the Bittensor subnet with provided netuid using the provided wallet. - - Registration is a critical step for a neuron to become an active participant in the network, enabling it to stake, - set weights, and receive incentives. + """ + Registers a new subnetwork on the Bittensor blockchain asynchronously. Parameters: - subtensor: Subtensor object to use for chain interactions - wallet: Bittensor wallet object. - netuid: The ``netuid`` of the subnet to register on. - max_allowed_attempts: Maximum number of attempts to register the wallet. - output_in_place: Whether the POW solving should be outputted to the console as it goes along. - cuda: If `True`, the wallet should be registered using CUDA device(s). - dev_id: The CUDA device id to use, or a list of device ids. - tpb: The number of threads per block (CUDA). - num_processes: The number of processes to use to register. - update_interval: The number of nonces to solve between updates. - log_verbose: If `True`, the registration process will log more information. + subtensor: The subtensor interface to send the extrinsic. + wallet: The wallet to be used for subnet registration. mev_protection: If True, encrypts and submits the transaction through the MEV Shield pallet to protect against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators decrypt and execute it. If False, submits the transaction directly without encryption. @@ -304,148 +343,49 @@ async def register_extrinsic( ).success: return unlocked - block_hash = await subtensor.substrate.get_chain_head() - if not await subtensor.subnet_exists(netuid, block_hash=block_hash): + balance = await subtensor.get_balance(wallet.coldkeypub.ss58_address) + burn_cost = await subtensor.get_subnet_burn_cost() + + if burn_cost > balance: return ExtrinsicResponse( - False, f"Subnet {netuid} does not exist." + False, + f"Insufficient balance {balance} to register subnet. Current burn cost is {burn_cost} TAO.", ).with_log() - neuron = await subtensor.get_neuron_for_pubkey_and_subnet( - hotkey_ss58=wallet.hotkey.ss58_address, netuid=netuid, block_hash=block_hash + call = await SubtensorModule(subtensor).register_network( + hotkey=wallet.hotkey.ss58_address ) - if not neuron.is_null: - message = "Already registered." - logging.debug(f"[green]{message}[/green]") - logging.debug(f"\t\tuid: [blue]{neuron.uid}[/blue]") - logging.debug(f"\t\tnetuid: [blue]{neuron.netuid}[/blue]") - logging.debug(f"\t\thotkey: [blue]{neuron.hotkey}[/blue]") - logging.debug(f"\t\tcoldkey: [blue]{neuron.coldkey}[/blue]") - return ExtrinsicResponse(message=message, data={"neuron": neuron}) + if mev_protection: + response = await submit_encrypted_extrinsic( + subtensor=subtensor, + wallet=wallet, + call=call, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + else: + response = await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) - logging.debug( - f"Registration hotkey: [blue]{wallet.hotkey.ss58_address}[/blue], Public coldkey: " - f"[blue]{wallet.coldkey.ss58_address}[/blue] in the network: [blue]{subtensor.network}[/blue]." - ) + if not wait_for_finalization and not wait_for_inclusion: + return response + + if response.success: + logging.debug("[green]Successfully registered subnet.[/green]") + return response - if not torch: - log_no_torch_error() - return ExtrinsicResponse(False, "Torch is not installed.").with_log() - - # Attempt rolling registration. - attempts = 1 - - while True: - # Solve latest POW. - if cuda: - if not torch.cuda.is_available(): - return ExtrinsicResponse(False, "CUDA not available.").with_log() - - logging.debug(f"Creating a POW with CUDA.") - pow_result = await create_pow_async( - subtensor=subtensor, - wallet=wallet, - netuid=netuid, - output_in_place=output_in_place, - cuda=cuda, - dev_id=dev_id, - tpb=tpb, - num_processes=num_processes, - update_interval=update_interval, - log_verbose=log_verbose, - ) - else: - logging.debug(f"Creating a POW.") - pow_result = await create_pow_async( - subtensor=subtensor, - wallet=wallet, - netuid=netuid, - output_in_place=output_in_place, - cuda=cuda, - num_processes=num_processes, - update_interval=update_interval, - log_verbose=log_verbose, - ) - - # pow failed - if not pow_result: - # might be registered already on this subnet - is_registered = await subtensor.is_hotkey_registered( - netuid=netuid, hotkey_ss58=wallet.hotkey.ss58_address - ) - if is_registered: - message = f"Already registered in subnet {netuid}." - logging.debug(f"[green]{message}[/green]") - return ExtrinsicResponse(message=message) - - # pow successful, proceed to submit pow to chain for registration - else: - # check if a pow result is still valid - while not await pow_result.is_stale_async(subtensor=subtensor): - call = await SubtensorModule(subtensor).register( - netuid=netuid, - coldkey=wallet.coldkeypub.ss58_address, - hotkey=wallet.hotkey.ss58_address, - block_number=pow_result.block_number, - nonce=pow_result.nonce, - work=[int(byte_) for byte_ in pow_result.seal], - ) - if mev_protection: - response = await submit_encrypted_extrinsic( - subtensor=subtensor, - wallet=wallet, - call=call, - period=period, - raise_error=raise_error, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - wait_for_revealed_execution=wait_for_revealed_execution, - ) - else: - response = await subtensor.sign_and_send_extrinsic( - call=call, - wallet=wallet, - period=period, - raise_error=raise_error, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - if not response.success: - # Look error here - # https://github.com/opentensor/subtensor/blob/development/pallets/subtensor/src/errors.rs - if "HotKeyAlreadyRegisteredInSubNet" in response.message: - logging.debug( - f"[green]Already registered on subnet:[/green] [blue]{netuid}[/blue]." - ) - return response - await asyncio.sleep(0.5) - - if response.success: - is_registered = await subtensor.is_hotkey_registered( - netuid=netuid, hotkey_ss58=wallet.hotkey.ss58_address - ) - if is_registered: - logging.debug("[green]Registered.[/green]") - return response - - # neuron not found, try again - logging.warning("[red]Unknown error. Neuron not found.[/red]") - continue - else: - # Exited loop because pow is no longer valid. - logging.warning("[red]POW is stale.[/red]") - # Try again. - - if attempts < max_allowed_attempts: - # Failed registration, retry pow - attempts += 1 - logging.warning( - f"Failed registration, retrying pow ... [blue]({attempts}/{max_allowed_attempts})[/blue]" - ) - else: - # Failed to register after max attempts. - return ExtrinsicResponse(False, "No more attempts.").with_log() + logging.error(f"Failed to register subnet: {response.message}") + return response except Exception as error: return ExtrinsicResponse.from_exception(raise_error=raise_error, error=error) diff --git a/bittensor/core/extrinsics/children.py b/bittensor/core/extrinsics/children.py index 5172d2c2f2..ed7daf21cd 100644 --- a/bittensor/core/extrinsics/children.py +++ b/bittensor/core/extrinsics/children.py @@ -111,7 +111,7 @@ def root_set_pending_childkey_cooldown_extrinsic( period: Optional[int] = None, raise_error: bool = False, wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, + wait_for_finalization: bool = True, wait_for_revealed_execution: bool = True, ) -> ExtrinsicResponse: """ diff --git a/bittensor/core/extrinsics/coldkey_swap.py b/bittensor/core/extrinsics/coldkey_swap.py index dcd695075f..c408cd7324 100644 --- a/bittensor/core/extrinsics/coldkey_swap.py +++ b/bittensor/core/extrinsics/coldkey_swap.py @@ -60,6 +60,7 @@ def announce_coldkey_swap_extrinsic( - A swap cost is charged when making the first announcement (not when reannouncing). - After making an announcement, all transactions from the coldkey are blocked except for `swap_coldkey_announced`. - The swap can only be executed after the delay period has passed (check via `get_coldkey_swap_announcement`). + - The destination coldkey cannot have any staking hotkeys. It must be completely new without any staking activity. - See: """ try: @@ -68,6 +69,17 @@ def announce_coldkey_swap_extrinsic( ).success: return unlocked + staking_hotkeys = subtensor.get_staking_hotkeys(new_coldkey_ss58) + if staking_hotkeys: + error_msg = "Destination coldkey cannot have any staking hotkeys. Please use a new coldkey for the swap." + if raise_error: + raise ValueError(error_msg) + return ExtrinsicResponse( + success=False, + message=error_msg, + extrinsic_receipt=None, + ) + # Compute hash of new coldkey new_keypair = Keypair( ss58_address=new_coldkey_ss58, @@ -186,6 +198,75 @@ def dispute_coldkey_swap_extrinsic( return ExtrinsicResponse.from_exception(raise_error=raise_error, error=error) +def clear_coldkey_swap_announcement_extrinsic( + subtensor: "Subtensor", + wallet: "Wallet", + *, + mev_protection: bool = DEFAULT_MEV_PROTECTION, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + wait_for_revealed_execution: bool = True, +) -> ExtrinsicResponse: + """ + Clears (withdraws) a pending coldkey swap announcement. + + Callable by the coldkey that has an active, undisputed swap announcement. The reannouncement delay must have + elapsed past the execution block before the announcement can be cleared. + + Parameters: + subtensor: Subtensor instance with the connection to the chain. + wallet: Bittensor wallet object (should be the current coldkey with an active announcement). + mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet. + period: The number of blocks during which the transaction will remain valid. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + wait_for_revealed_execution: Whether to wait for the revealed execution if mev_protection used. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + Notes: + - The coldkey must have an active, undisputed swap announcement. + - The reannouncement delay must have elapsed past the execution block. + """ + try: + if not ( + unlocked := ExtrinsicResponse.unlock_wallet(wallet, raise_error) + ).success: + return unlocked + + call = SubtensorModule(subtensor).clear_coldkey_swap_announcement() + + if mev_protection: + response = submit_encrypted_extrinsic( + subtensor=subtensor, + wallet=wallet, + call=call, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + else: + response = subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + period=period, + raise_error=raise_error, + ) + + return response + + except Exception as error: + return ExtrinsicResponse.from_exception(raise_error=raise_error, error=error) + + def swap_coldkey_announced_extrinsic( subtensor: "Subtensor", wallet: "Wallet", diff --git a/bittensor/core/extrinsics/pallets/subtensor_module.py b/bittensor/core/extrinsics/pallets/subtensor_module.py index 0da15461cb..0dddc4c3e8 100644 --- a/bittensor/core/extrinsics/pallets/subtensor_module.py +++ b/bittensor/core/extrinsics/pallets/subtensor_module.py @@ -243,35 +243,25 @@ def move_stake( alpha_amount=alpha_amount, ) - def register( + def register_limit( self, netuid: int, - coldkey: str, hotkey: str, - block_number: int, - nonce: int, - work: list[int], + limit_price: int, ) -> Call: - """Returns GenericCall instance for Subtensor function SubtensorModule.register. + """Returns GenericCall instance for Subtensor function SubtensorModule.register_limit. Parameters: netuid: The netuid of the subnet to register on. - coldkey: The coldkey SS58 address associated with the neuron. hotkey: The hotkey SS58 address associated with the neuron. - block_number: POW block number. - nonce: POW nonce. - work: List representation of POW seal. + limit_price: Maximum acceptable burn price in RAO. If on-chain burn exceeds this, + the transaction fails with RegistrationPriceLimitExceeded. Returns: GenericCall instance. """ return self.create_composed_call( - netuid=netuid, - coldkey=coldkey, - hotkey=hotkey, - block_number=block_number, - nonce=nonce, - work=work, + netuid=netuid, hotkey=hotkey, limit_price=limit_price ) def register_network(self, hotkey: str) -> Call: @@ -366,6 +356,17 @@ def dispute_coldkey_swap(self) -> Call: """ return self.create_composed_call() + def clear_coldkey_swap_announcement(self) -> Call: + """Returns GenericCall instance for Subtensor function SubtensorModule.clear_coldkey_swap_announcement. + + Callable by the coldkey that has an active swap announcement. Withdraws the announcement + after the reannouncement delay has elapsed past the execution block. + + Returns: + GenericCall instance. + """ + return self.create_composed_call() + def reset_coldkey_swap(self, coldkey: str) -> Call: """Returns GenericCall instance for Subtensor function SubtensorModule.reset_coldkey_swap. diff --git a/bittensor/core/extrinsics/registration.py b/bittensor/core/extrinsics/registration.py index 50962fb9c1..3823b951e8 100644 --- a/bittensor/core/extrinsics/registration.py +++ b/bittensor/core/extrinsics/registration.py @@ -1,17 +1,17 @@ """ -This module provides sync functionalities for registering a wallet with the subtensor network using Proof-of-Work (PoW). +This module provides sync functionalities for registering a wallet with the subtensor network. """ import time -from typing import Optional, Union, TYPE_CHECKING +from typing import Optional, TYPE_CHECKING -from bittensor.core.errors import RegistrationError +from bittensor.core.errors import BalanceTypeError, RegistrationError from bittensor.core.extrinsics.mev_shield import submit_encrypted_extrinsic from bittensor.core.extrinsics.pallets import SubtensorModule from bittensor.core.settings import DEFAULT_MEV_PROTECTION from bittensor.core.types import ExtrinsicResponse +from bittensor.utils.balance import Balance from bittensor.utils.btlogging import logging -from bittensor.utils.registration import create_pow, log_no_torch_error, torch if TYPE_CHECKING: from bittensor_wallet import Wallet @@ -153,9 +153,11 @@ def burned_register_extrinsic( return ExtrinsicResponse.from_exception(raise_error=raise_error, error=error) -def register_subnet_extrinsic( +def register_limit_extrinsic( subtensor: "Subtensor", wallet: "Wallet", + netuid: int, + limit_price: Balance, *, mev_protection: bool = DEFAULT_MEV_PROTECTION, period: Optional[int] = None, @@ -164,12 +166,14 @@ def register_subnet_extrinsic( wait_for_finalization: bool = True, wait_for_revealed_execution: bool = True, ) -> ExtrinsicResponse: - """ - Registers a new subnetwork on the Bittensor blockchain. + """Registers the wallet to chain by recycling TAO, with a maximum burn price limit. Parameters: - subtensor: The subtensor interface to send the extrinsic. - wallet: The wallet to be used for subnet registration. + subtensor: Subtensor instance. + wallet: Bittensor wallet object. + netuid: The ``netuid`` of the subnet to register on. + limit_price: Maximum acceptable burn price as a Balance instance. If the on-chain burn price exceeds + this value, the transaction will fail with RegistrationPriceLimitExceeded. mev_protection: If True, encrypts and submits the transaction through the MEV Shield pallet to protect against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators decrypt and execute it. If False, submits the transaction directly without encryption. @@ -192,17 +196,41 @@ def register_subnet_extrinsic( ).success: return unlocked - balance = subtensor.get_balance(wallet.coldkeypub.ss58_address) - burn_cost = subtensor.get_subnet_burn_cost() + if not isinstance(limit_price, Balance): + raise BalanceTypeError("`limit_price` must be an instance of Balance.") - if burn_cost > balance: + block = subtensor.get_current_block() + if not subtensor.subnet_exists(netuid=netuid, block=block): return ExtrinsicResponse( - False, - f"Insufficient balance {balance} to register subnet. Current burn cost is {burn_cost} TAO.", + False, f"Subnet {netuid} does not exist." ).with_log() - call = SubtensorModule(subtensor).register_network( - hotkey=wallet.hotkey.ss58_address + neuron = subtensor.get_neuron_for_pubkey_and_subnet( + netuid=netuid, hotkey_ss58=wallet.hotkey.ss58_address, block=block + ) + + old_balance = subtensor.get_balance( + address=wallet.coldkeypub.ss58_address, block=block + ) + + if not neuron.is_null: + message = "Already registered." + logging.debug(f"[green]{message}[/green]") + logging.debug(f"\t\tuid: [blue]{neuron.uid}[/blue]") + logging.debug(f"\t\tnetuid: [blue]{neuron.netuid}[/blue]") + logging.debug(f"\t\thotkey: [blue]{neuron.hotkey}[/blue]") + logging.debug(f"\t\tcoldkey: [blue]{neuron.coldkey}[/blue]") + return ExtrinsicResponse( + message=message, data={"neuron": neuron, "old_balance": old_balance} + ) + + recycle_amount = subtensor.recycle(netuid=netuid, block=block) + logging.debug(f"Recycling {recycle_amount} to register on subnet:{netuid}") + + call = SubtensorModule(subtensor).register_limit( + netuid=netuid, + hotkey=wallet.hotkey.ss58_address, + limit_price=limit_price.rao, ) if mev_protection: @@ -225,33 +253,50 @@ def register_subnet_extrinsic( wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, ) - - if not wait_for_finalization and not wait_for_inclusion: + extrinsic_fee = response.extrinsic_fee + logging.debug( + f"The registration fee for SN #[blue]{netuid}[/blue] is [blue]{extrinsic_fee}[/blue]." + ) + if not response.success: + logging.error(f"[red]{response.message}[/red]") + time.sleep(0.5) return response - if response.success: - logging.debug("[green]Successfully registered subnet.[/green]") + new_balance = subtensor.get_balance(address=wallet.coldkeypub.ss58_address) + + logging.debug( + f"Balance: [blue]{old_balance}[/blue] :arrow_right: [green]{new_balance}[/green]" + ) + is_registered = subtensor.is_hotkey_registered( + netuid=netuid, hotkey_ss58=wallet.hotkey.ss58_address + ) + + response.data = { + "neuron": neuron, + "balance_before": old_balance, + "balance_after": new_balance, + "recycle_amount": recycle_amount, + } + + if is_registered: + logging.debug("[green]Registered.[/green]") return response - logging.error(f"Failed to register subnet: {response.message}") - return response + message = f"Neuron with hotkey {wallet.hotkey.ss58_address} not found in subnet {netuid} after registration." + return ExtrinsicResponse( + success=False, + message=message, + extrinsic=response.extrinsic, + error=RegistrationError(message), + ).with_log() except Exception as error: return ExtrinsicResponse.from_exception(raise_error=raise_error, error=error) -def register_extrinsic( +def register_subnet_extrinsic( subtensor: "Subtensor", wallet: "Wallet", - netuid: int, - max_allowed_attempts: int = 3, - output_in_place: bool = True, - cuda: bool = False, - dev_id: Union[list[int], int] = 0, - tpb: int = 256, - num_processes: Optional[int] = None, - update_interval: Optional[int] = None, - log_verbose: bool = False, *, mev_protection: bool = DEFAULT_MEV_PROTECTION, period: Optional[int] = None, @@ -260,20 +305,12 @@ def register_extrinsic( wait_for_finalization: bool = True, wait_for_revealed_execution: bool = True, ) -> ExtrinsicResponse: - """Registers a neuron on the Bittensor subnet with provided netuid using the provided wallet. + """ + Registers a new subnetwork on the Bittensor blockchain. Parameters: - subtensor: Subtensor object to use for chain interactions - wallet: Bittensor wallet object. - netuid: The ``netuid`` of the subnet to register on. - max_allowed_attempts: Maximum number of attempts to register the wallet. - output_in_place: Whether the POW solving should be outputted to the console as it goes along. - cuda: If `True`, the wallet should be registered using CUDA device(s). - dev_id: The CUDA device id to use, or a list of device ids. - tpb: The number of threads per block (CUDA). - num_processes: The number of processes to use to register. - update_interval: The number of nonces to solve between updates. - log_verbose: If `True`, the registration process will log more information. + subtensor: The subtensor interface to send the extrinsic. + wallet: The wallet to be used for subnet registration. mev_protection: If True, encrypts and submits the transaction through the MEV Shield pallet to protect against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators decrypt and execute it. If False, submits the transaction directly without encryption. @@ -296,149 +333,49 @@ def register_extrinsic( ).success: return unlocked - block = subtensor.get_current_block() - if not subtensor.subnet_exists(netuid, block=block): + balance = subtensor.get_balance(wallet.coldkeypub.ss58_address) + burn_cost = subtensor.get_subnet_burn_cost() + + if burn_cost > balance: return ExtrinsicResponse( - False, f"Subnet {netuid} does not exist." + False, + f"Insufficient balance {balance} to register subnet. Current burn cost is {burn_cost} TAO.", ).with_log() - neuron = subtensor.get_neuron_for_pubkey_and_subnet( - hotkey_ss58=wallet.hotkey.ss58_address, netuid=netuid, block=block + call = SubtensorModule(subtensor).register_network( + hotkey=wallet.hotkey.ss58_address ) - if not neuron.is_null: - message = "Already registered." - logging.debug(f"[green]{message}[/green]") - logging.debug(f"\t\tuid: [blue]{neuron.uid}[/blue]") - logging.debug(f"\t\tnetuid: [blue]{neuron.netuid}[/blue]") - logging.debug(f"\t\thotkey: [blue]{neuron.hotkey}[/blue]") - logging.debug(f"\t\tcoldkey: [blue]{neuron.coldkey}[/blue]") - return ExtrinsicResponse(message=message, data={"neuron": neuron}) + if mev_protection: + response = submit_encrypted_extrinsic( + subtensor=subtensor, + wallet=wallet, + call=call, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + else: + response = subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) - logging.debug( - f"Registration hotkey: [blue]{wallet.hotkey.ss58_address}[/blue], Public coldkey: " - f"[blue]{wallet.coldkey.ss58_address}[/blue] in the network: [blue]{subtensor.network}[/blue]." - ) + if not wait_for_finalization and not wait_for_inclusion: + return response + + if response.success: + logging.debug("[green]Successfully registered subnet.[/green]") + return response - if not torch: - log_no_torch_error() - return ExtrinsicResponse(False, "Torch is not installed.").with_log() - - # Attempt rolling registration. - attempts = 1 - - while True: - # Solve latest POW. - if cuda: - if not torch.cuda.is_available(): - return ExtrinsicResponse(False, "CUDA not available.").with_log() - - logging.debug(f"Creating a POW with CUDA.") - pow_result = create_pow( - subtensor=subtensor, - wallet=wallet, - netuid=netuid, - output_in_place=output_in_place, - cuda=cuda, - dev_id=dev_id, - tpb=tpb, - num_processes=num_processes, - update_interval=update_interval, - log_verbose=log_verbose, - ) - else: - logging.debug(f"Creating a POW.") - pow_result = create_pow( - subtensor=subtensor, - wallet=wallet, - netuid=netuid, - output_in_place=output_in_place, - cuda=cuda, - num_processes=num_processes, - update_interval=update_interval, - log_verbose=log_verbose, - ) - - # pow failed - if not pow_result: - # might be registered already on this subnet - is_registered = subtensor.is_hotkey_registered( - netuid=netuid, hotkey_ss58=wallet.hotkey.ss58_address - ) - if is_registered: - message = f"Already registered in subnet {netuid}." - logging.debug(f"[green]{message}[/green]") - return ExtrinsicResponse(message=message) - - # pow successful, proceed to submit pow to chain for registration - else: - # check if a pow result is still valid - while not pow_result.is_stale(subtensor=subtensor): - # create extrinsic call - call = SubtensorModule(subtensor).register( - netuid=netuid, - coldkey=wallet.coldkeypub.ss58_address, - hotkey=wallet.hotkey.ss58_address, - block_number=pow_result.block_number, - nonce=pow_result.nonce, - work=[int(byte_) for byte_ in pow_result.seal], - ) - if mev_protection: - response = submit_encrypted_extrinsic( - subtensor=subtensor, - wallet=wallet, - call=call, - period=period, - raise_error=raise_error, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - wait_for_revealed_execution=wait_for_revealed_execution, - ) - else: - response = subtensor.sign_and_send_extrinsic( - call=call, - wallet=wallet, - period=period, - raise_error=raise_error, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - if not response.success: - # Look error here - # https://github.com/opentensor/subtensor/blob/development/pallets/subtensor/src/errors.rs - if "HotKeyAlreadyRegisteredInSubNet" in response.message: - logging.debug( - f"[green]Already registered on subnet:[/green] [blue]{netuid}[/blue]." - ) - return response - time.sleep(0.5) - - if response.success: - is_registered = subtensor.is_hotkey_registered( - netuid=netuid, hotkey_ss58=wallet.hotkey.ss58_address - ) - if is_registered: - logging.debug("[green]Registered.[/green]") - return response - - # neuron not found, try again - logging.warning("[red]Unknown error. Neuron not found.[/red]") - continue - else: - # Exited loop because pow is no longer valid. - logging.warning("[red]POW is stale.[/red]") - # Try again. - - if attempts < max_allowed_attempts: - # Failed registration, retry pow - attempts += 1 - logging.warning( - f"Failed registration, retrying pow ... [blue]({attempts}/{max_allowed_attempts})[/blue]" - ) - else: - # Failed to register after max attempts. - return ExtrinsicResponse(False, "No more attempts.").with_log() + logging.error(f"Failed to register subnet: {response.message}") + return response except Exception as error: return ExtrinsicResponse.from_exception(raise_error=raise_error, error=error) diff --git a/bittensor/core/extrinsics/utils.py b/bittensor/core/extrinsics/utils.py index a491d82da8..7af0985eab 100644 --- a/bittensor/core/extrinsics/utils.py +++ b/bittensor/core/extrinsics/utils.py @@ -217,11 +217,11 @@ def apply_pure_proxy_data( # If triggered events are not available or event PureCreated does not exist in the response, return the response # with warning message ot raise the error if raise_error is True. message = ( - f"The ExtrinsicResponse doesn't contain pure_proxy data (`pure_account`, `spawner`, `proxy_type`, etc.) " - f"because the extrinsic receipt doesn't have triggered events. This typically happens when " - f"`wait_for_inclusion=False` or when `block_hash` is not available. To get this data, either pass " - f"`wait_for_inclusion=True` when calling this function, or retrieve the data manually from the blockchain " - f"using the extrinsic hash." + "The ExtrinsicResponse doesn't contain pure_proxy data (`pure_account`, `spawner`, `proxy_type`, etc.) " + "because the extrinsic receipt doesn't have triggered events. This typically happens when " + "`wait_for_inclusion=False` or when `block_hash` is not available. To get this data, either pass " + "`wait_for_inclusion=True` when calling this function, or retrieve the data manually from the blockchain " + "using the extrinsic hash." ) if response.extrinsic is not None and hasattr(response.extrinsic, "extrinsic_hash"): extrinsic_hash = response.extrinsic.extrinsic_hash diff --git a/bittensor/core/metagraph.py b/bittensor/core/metagraph.py index 44f4a033c1..5496dfca9c 100644 --- a/bittensor/core/metagraph.py +++ b/bittensor/core/metagraph.py @@ -320,7 +320,7 @@ def S(self) -> Tensor: return self.stake @property - def I(self) -> Tensor: + def I(self) -> Tensor: # noqa: E743 """ Incentive values of neurons represent the rewards they receive for their contributions to the network. The Bittensor network employs an incentive mechanism that rewards neurons based on their diff --git a/bittensor/core/settings.py b/bittensor/core/settings.py index 06c900062b..0fb2bc9b55 100644 --- a/bittensor/core/settings.py +++ b/bittensor/core/settings.py @@ -3,8 +3,6 @@ import re from pathlib import Path -from munch import munchify - ROOT_TAO_STAKE_WEIGHT = 0.18 READ_ONLY = os.getenv("READ_ONLY") == "1" @@ -114,50 +112,46 @@ _BT_PRIORITY_MAX_WORKERS = os.getenv("BT_PRIORITY_MAX_WORKERS") _BT_PRIORITY_MAXSIZE = os.getenv("BT_PRIORITY_MAXSIZE") -DEFAULTS = munchify( - { - "axon": { - "port": int(_BT_AXON_PORT) if _BT_AXON_PORT else 8091, - "ip": os.getenv("BT_AXON_IP") or "[::]", - "external_port": os.getenv("BT_AXON_EXTERNAL_PORT") or None, - "external_ip": os.getenv("BT_AXON_EXTERNAL_IP") or None, - "max_workers": int(_BT_AXON_MAX_WORKERS) if _BT_AXON_MAX_WORKERS else 10, - }, - "logging": { - "debug": bool(os.getenv("BT_LOGGING_DEBUG")) or False, - "trace": bool(os.getenv("BT_LOGGING_TRACE")) or False, - "info": bool(os.getenv("BT_LOGGING_INFO")) or False, - "record_log": bool(os.getenv("BT_LOGGING_RECORD_LOG")) or False, - "logging_dir": None + +class DEFAULTS: + config = False + strict = False + no_version_checking = False + + class axon: + port = int(_BT_AXON_PORT) if _BT_AXON_PORT else 8091 + ip = os.getenv("BT_AXON_IP") or "[::]" + external_port = os.getenv("BT_AXON_EXTERNAL_PORT") or None + external_ip = os.getenv("BT_AXON_EXTERNAL_IP") or None + max_workers = int(_BT_AXON_MAX_WORKERS) if _BT_AXON_MAX_WORKERS else 10 + + class logging: + debug = bool(os.getenv("BT_LOGGING_DEBUG")) or False + trace = bool(os.getenv("BT_LOGGING_TRACE")) or False + info = bool(os.getenv("BT_LOGGING_INFO")) or False + record_log = bool(os.getenv("BT_LOGGING_RECORD_LOG")) or False + logging_dir = ( + None if READ_ONLY - else os.getenv("BT_LOGGING_LOGGING_DIR") or str(MINERS_DIR), - "enable_third_party_loggers": os.getenv( - "BT_LOGGING_ENABLE_THIRD_PARTY_LOGGERS" - ) - or False, - }, - "priority": { - "max_workers": int(_BT_PRIORITY_MAX_WORKERS) - if _BT_PRIORITY_MAX_WORKERS - else 5, - "maxsize": int(_BT_PRIORITY_MAXSIZE) if _BT_PRIORITY_MAXSIZE else 10, - }, - "subtensor": { - "chain_endpoint": os.getenv("BT_SUBTENSOR_CHAIN_ENDPOINT") - or DEFAULT_ENDPOINT, - "network": os.getenv("BT_SUBTENSOR_NETWORK") or DEFAULT_NETWORK, - "_mock": False, - }, - "wallet": { - "name": os.getenv("BT_WALLET_NAME") or "default", - "hotkey": os.getenv("BT_WALLET_HOTKEY") or "default", - "path": os.getenv("BT_WALLET_PATH") or str(WALLETS_DIR), - }, - "config": False, - "strict": False, - "no_version_checking": False, - } -) + else os.getenv("BT_LOGGING_LOGGING_DIR") or str(MINERS_DIR) + ) + enable_third_party_loggers = ( + os.getenv("BT_LOGGING_ENABLE_THIRD_PARTY_LOGGERS") or False + ) + + class priority: + max_workers = int(_BT_PRIORITY_MAX_WORKERS) if _BT_PRIORITY_MAX_WORKERS else 5 + maxsize = int(_BT_PRIORITY_MAXSIZE) if _BT_PRIORITY_MAXSIZE else 10 + + class subtensor: + chain_endpoint = os.getenv("BT_SUBTENSOR_CHAIN_ENDPOINT") or DEFAULT_ENDPOINT + network = os.getenv("BT_SUBTENSOR_NETWORK") or DEFAULT_NETWORK + _mock = False + + class wallet: + name = os.getenv("BT_WALLET_NAME") or "default" + hotkey = os.getenv("BT_WALLET_HOTKEY") or "default" + path = os.getenv("BT_WALLET_PATH") or str(WALLETS_DIR) # Parsing version without any literals. diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 53f594e4f8..6c634aa2e0 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -7,10 +7,12 @@ from async_substrate_interface.errors import SubstrateRequestException from async_substrate_interface.substrate_addons import RetrySyncSubstrate from async_substrate_interface.sync_substrate import SubstrateInterface -from async_substrate_interface.types import ScaleObj from async_substrate_interface.utils.storage import StorageKey from bittensor_drand import get_encrypted_commitment from bittensor_wallet.utils import SS58_FORMAT +from scalecodec import ScaleValue +from scalecodec.base import ScaleType +from scalecodec.utils.math import FixedPoint, fixed_to_decimal from bittensor.core.axon import Axon from bittensor.core.chain_data import ( @@ -38,11 +40,9 @@ SubnetIdentity, SubnetInfo, WeightCommitInfo, - decode_account_id, ) from bittensor.core.chain_data.chain_identity import ChainIdentity from bittensor.core.chain_data.utils import ( - decode_block, decode_metadata, decode_revealed_commitment, decode_revealed_commitment_with_hotkey, @@ -55,6 +55,7 @@ ) from bittensor.core.extrinsics.coldkey_swap import ( announce_coldkey_swap_extrinsic, + clear_coldkey_swap_announcement_extrinsic, dispute_coldkey_swap_extrinsic, swap_coldkey_announced_extrinsic, ) @@ -96,7 +97,7 @@ ) from bittensor.core.extrinsics.registration import ( burned_register_extrinsic, - register_extrinsic, + register_limit_extrinsic, register_subnet_extrinsic, set_subnet_identity_extrinsic, ) @@ -146,6 +147,11 @@ SubtensorMixin, UIDs, Weights, + PositionResponse, + NeuronCertificateResponse, + CommitmentOfResponse, + CrowdloansResponse, + DynamicInfoResponse, ) from bittensor.utils import ( Certificate, @@ -160,7 +166,6 @@ ) from bittensor.utils.balance import ( Balance, - FixedPoint, check_balance_amount, fixed_to_float, ) @@ -287,7 +292,7 @@ def __exit__(self, exc_type, exc_val, exc_tb): def _decode_crowdloan_entry( self, crowdloan_id: int, - data: dict, + data: CrowdloansResponse, block_hash: Optional[str] = None, ) -> "CrowdloanInfo": """ @@ -298,13 +303,13 @@ def _decode_crowdloan_entry( call_data = data.get("call") if call_data and "Inline" in call_data: try: - inline_bytes = bytes(call_data["Inline"][0][0]) - decoded_call = self.substrate.create_scale_object( - type_string="Call", - data=scalecodec.ScaleBytes(inline_bytes), + call_obj = self.substrate.create_scale_object( + "Call", + data=scalecodec.ScaleBytes(call_data["Inline"]), block_hash=block_hash, - ).decode() - data["call"] = decoded_call + ) + call_value = call_obj.decode() + data["call"] = call_value except Exception as e: data["call"] = {"decode_error": str(e), "raw": call_data} @@ -395,12 +400,8 @@ def _runtime_method_exists(self, api: str, method: str, block_hash: str) -> bool """ runtime = self.substrate.init_runtime(block_hash=block_hash) if runtime.metadata_v15 is not None: - metadata_v15_value = runtime.metadata_v15.value() - apis = {entry["name"]: entry for entry in metadata_v15_value["apis"]} try: - api_entry = apis[api] - methods = {entry["name"]: entry for entry in api_entry["methods"]} - _ = methods[method] + _ = runtime.runtime_api_map[api][method] return True except KeyError: return False @@ -524,8 +525,7 @@ def get_hyperparameter( params=[netuid], block_hash=block_hash, ) - - return getattr(result, "value", result) + return result.value @property def block(self) -> int: @@ -637,7 +637,7 @@ def sim_swap( def query_constant( self, module_name: str, constant_name: str, block: Optional[int] = None - ) -> Optional["ScaleObj"]: + ) -> Optional[ScaleType[ScaleValue]]: """Retrieves a constant from the specified module on the Bittensor blockchain. Use this function for nonstandard queries to constants defined within the Bittensor blockchain, if these cannot @@ -720,7 +720,7 @@ def query_module( name: str, params: Optional[list] = None, block: Optional[int] = None, - ) -> Optional[Union["ScaleObj", Any, FixedPoint]]: + ) -> ScaleType: """Queries any module storage on the Bittensor blockchain with the specified parameters and block number. This function is a generic query interface that allows for flexible and diverse data retrieval from various blockchain modules. Use this function for nonstandard queries to storage defined within the Bittensor @@ -767,14 +767,14 @@ def query_runtime_api( block_hash = self.determine_block_hash(block) result = self.substrate.runtime_call(runtime_api, method, params, block_hash) - return result.value + return result def query_subtensor( self, name: str, params: Optional[list] = None, block: Optional[int] = None, - ) -> Optional[Union["ScaleObj", Any]]: + ) -> ScaleType[ScaleValue]: """Queries named storage from the Subtensor module on the Bittensor blockchain. Use this function for nonstandard queries to constants defined within the Bittensor blockchain, if these cannot @@ -833,12 +833,14 @@ def all_subnets(self, block: Optional[int] = None) -> Optional[list["DynamicInfo a subnet, or None if the query fails. """ block_hash = self.determine_block_hash(block=block) - query = self.substrate.runtime_call( - api="SubnetInfoRuntimeApi", - method="get_all_dynamic_info", - block_hash=block_hash, + decoded = cast( + list[DynamicInfoResponse], + self.substrate.runtime_call( + api="SubnetInfoRuntimeApi", + method="get_all_dynamic_info", + block_hash=block_hash, + ), ) - decoded = query.decode() try: subnet_prices = self.get_subnet_prices(block=block) for sn in decoded: @@ -866,10 +868,10 @@ def blocks_since_last_step( Notes: - """ - query = self.query_subtensor( + query: ScaleType[int] = self.query_subtensor( # type: ignore[assignment] name="BlocksSinceLastStep", block=block, params=[netuid] ) - return cast(Optional[int], getattr(query, "value", query)) + return query.value def blocks_since_last_update( self, netuid: int, uid: int, block: Optional[int] = None @@ -885,10 +887,11 @@ def blocks_since_last_update( The number of blocks since the last update, or None if the subnetwork or UID does not exist. """ block = block or self.get_current_block() - call = self.get_hyperparameter( + call: Optional[list[int]] = self.get_hyperparameter( param_name="LastUpdate", netuid=netuid, block=block ) - return None if not call else (block - int(call[uid])) + assert call is not None + return None if len(call) == 0 else (block - int(call[uid])) def blocks_until_next_epoch( self, netuid: int, tempo: Optional[int] = None, block: Optional[int] = None @@ -957,12 +960,13 @@ def bonds( params=[storage_index], block_hash=self.determine_block_hash(block), ) - b_map = [] - for uid, b in b_map_encoded: - if b.value is not None: - b_map.append((uid, b.value)) + bond_map = [] + uid: int + bond: list[tuple[int, int]] + for uid, bond in b_map_encoded: + bond_map.append((uid, bond)) - return b_map + return bond_map def commit_reveal_enabled(self, netuid: int, block: Optional[int] = None) -> bool: """Check if commit-reveal mechanism is enabled for a given subnet at a specific block. @@ -978,12 +982,13 @@ def commit_reveal_enabled(self, netuid: int, block: Optional[int] = None) -> boo - - """ - call = self.get_hyperparameter( + call: Optional[bool] = self.get_hyperparameter( param_name="CommitRevealWeightsEnabled", block=block, netuid=netuid ) - return True if call is True else False + assert call is not None + return call - def difficulty(self, netuid: int, block: Optional[int] = None) -> Optional[int]: + def difficulty(self, netuid: int, block: Optional[int] = None) -> int: """Retrieves the 'Difficulty' hyperparameter for a specified subnet in the Bittensor network. This parameter determines the computational challenge required for neurons to participate in consensus and @@ -1003,11 +1008,10 @@ def difficulty(self, netuid: int, block: Optional[int] = None) -> Optional[int]: - - """ - call = self.get_hyperparameter( + call: Optional[int] = self.get_hyperparameter( param_name="Difficulty", netuid=netuid, block=block ) - if call is None: - return None + assert call is not None return int(call) def does_hotkey_exist(self, hotkey_ss58: str, block: Optional[int] = None) -> bool: @@ -1029,19 +1033,13 @@ def does_hotkey_exist(self, hotkey_ss58: str, block: Optional[int] = None) -> bo Notes: - """ - result = self.substrate.query( + result: ScaleType[str] = self.substrate.query( module="SubtensorModule", storage_function="Owner", params=[hotkey_ss58], block_hash=self.determine_block_hash(block), ) - return_val = ( - False - if result is None - # not the default key (0x0) - else result != "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM" - ) - return return_val + return result.value != "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM" def get_admin_freeze_window(self, block: Optional[int] = None) -> int: """Returns the duration, in blocks, of the administrative freeze window at the end of each epoch. @@ -1060,14 +1058,14 @@ def get_admin_freeze_window(self, block: Optional[int] = None) -> int: - """ - query = self.substrate.query( + query: ScaleType[int] = self.substrate.query( module="SubtensorModule", storage_function="AdminFreezeWindow", block_hash=self.determine_block_hash(block), ) - return cast(int, getattr(query, "value", query)) + return query.value - def get_all_subnets_info(self, block: Optional[int] = None) -> list["SubnetInfo"]: + def get_all_subnets_info(self, block: Optional[int] = None) -> list[SubnetInfo]: """Retrieves detailed information about all subnets within the Bittensor network. Parameters: @@ -1121,9 +1119,11 @@ def get_all_commitments( block=block, ) result = {} + id_: str + value: CommitmentOfResponse for id_, value in query: try: - result[decode_account_id(id_[0])] = decode_metadata(value) + result[id_] = decode_metadata(value) except Exception as error: logging.error( f"Error decoding [red]{id_}[/red] and [red]{value}[/red]: {error}" @@ -1193,10 +1193,10 @@ def get_all_metagraphs_info( method=method, block_hash=block_hash, ) - if query is None or not hasattr(query, "value"): + if query is None: return None - - return MetagraphInfo.list_from_dicts(query.value) + assert isinstance(query, list) + return MetagraphInfo.list_from_dicts(query) def get_all_neuron_certificates( self, netuid: int, block: Optional[int] = None @@ -1225,7 +1225,7 @@ def get_all_neuron_certificates( ) output = {} for key, item in query_certificates: - output[decode_account_id(key)] = Certificate(item.value) + output[key] = Certificate(item) return output def get_all_revealed_commitments( @@ -1326,8 +1326,7 @@ def get_auto_stakes( ) pairs = {} - for netuid, destination in query: - hotkey_ss58 = decode_account_id(destination.value[0]) + for netuid, hotkey_ss58 in query: if hotkey_ss58: pairs[int(netuid)] = hotkey_ss58 @@ -1346,13 +1345,13 @@ def get_balance(self, address: str, block: Optional[int] = None) -> Balance: Returns: Balance: The balance object containing the account's TAO balance. """ - balance = self.substrate.query( + balance: ScaleType[dict[str, Any]] = self.substrate.query( module="System", storage_function="Account", params=[address], block_hash=self.determine_block_hash(block), ) - return Balance(cast(dict[str, Any], balance)["data"]["free"]) + return Balance(balance.value["data"]["free"]) def get_balances( self, @@ -1382,11 +1381,17 @@ def get_balances( ) for address in addresses ] - batch_call = self.substrate.query_multi(calls, block_hash=block_hash) + batch_call: list[tuple[StorageKey, dict]] = self.substrate.query_multi( # type: ignore[assignment] + calls, block_hash=block_hash + ) results = {} + key: StorageKey + val: dict for item in batch_call: - value = item[1] or {"data": {"free": 0}} - results.update({item[0].params[0]: Balance(value["data"]["free"])}) + key, val = item + value = val or {"data": {"free": 0}} + assert key.params is not None + results.update({key.params[0]: Balance(value["data"]["free"])}) return results def get_current_block(self) -> int: @@ -1508,9 +1513,8 @@ def get_children( ) if children: formatted_children = [] - for proportion, child in children.value: + for proportion, formatted_child in children.value: # Convert U64 to int - formatted_child = decode_account_id(child[0]) normalized_proportion = u64_normalized_float(proportion) formatted_children.append((normalized_proportion, formatted_child)) return True, formatted_children, "" @@ -1554,15 +1558,14 @@ def get_children_pending( block_hash=self.determine_block_hash(block), ) children, cooldown = cast( - tuple[list[tuple[int, Any]], int], - getattr(pending_query, "value", pending_query), + tuple[list[tuple[int, Any]], int], pending_query.value ) return ( [ ( u64_normalized_float(proportion), - decode_account_id(child[0]), + child, ) for proportion, child in children ], @@ -1594,16 +1597,16 @@ def get_coldkey_swap_announcement( - See: """ block_hash = self.determine_block_hash(block) - query = self.substrate.query( + query: ScaleType[Optional[tuple[int, str]]] = self.substrate.query( module="SubtensorModule", storage_function="ColdkeySwapAnnouncements", params=[coldkey_ss58], block_hash=block_hash, ) - if query is None: + if query.value is None: return None return ColdkeySwapAnnouncementInfo.from_query( - coldkey_ss58=coldkey_ss58, query=cast(ScaleObj, query) + coldkey_ss58=coldkey_ss58, query=query ) def get_coldkey_swap_announcements( @@ -1661,8 +1664,7 @@ def get_coldkey_swap_announcement_delay( storage_function="ColdkeySwapAnnouncementDelay", block_hash=block_hash, ) - value = getattr(query, "value", query) - return cast(int, value) if value is not None else 0 + return cast(int, query.value) or 0 def get_coldkey_swap_constants( self, @@ -1731,13 +1733,12 @@ def get_coldkey_swap_reannouncement_delay( - See: """ block_hash = self.determine_block_hash(block) - query = self.substrate.query( + query: ScaleType[Optional[int]] = self.substrate.query( module="SubtensorModule", storage_function="ColdkeySwapReannouncementDelay", block_hash=block_hash, ) - value = getattr(query, "value", query) - return cast(int, value) if value is not None else 0 + return query.value or 0 def get_coldkey_swap_dispute( self, @@ -1770,9 +1771,7 @@ def get_coldkey_swap_dispute( ) if query is None: return None - return ColdkeySwapDisputeInfo.from_query( - coldkey_ss58=coldkey_ss58, query=cast(ScaleObj, query) - ) + return ColdkeySwapDisputeInfo.from_query(coldkey_ss58=coldkey_ss58, query=query) def get_coldkey_swap_disputes( self, @@ -1832,8 +1831,9 @@ def get_commitment(self, netuid: int, uid: int, block: Optional[int] = None) -> ) return "" - metadata = cast(dict, self.get_commitment_metadata(netuid, hotkey, block)) + metadata = self.get_commitment_metadata(netuid, hotkey, block) try: + assert not isinstance(metadata, str) return decode_metadata(metadata) except Exception as error: logging.error(error) @@ -1841,7 +1841,7 @@ def get_commitment(self, netuid: int, uid: int, block: Optional[int] = None) -> def get_commitment_metadata( self, netuid: int, hotkey_ss58: str, block: Optional[int] = None - ) -> Union[str, dict]: + ) -> str | CommitmentOfResponse: # TODO: how to handle return data? need good example @roman """Fetches raw commitment metadata from specific subnet for given hotkey. @@ -1857,21 +1857,21 @@ def get_commitment_metadata( Notes: - """ - commit_data = self.substrate.query( + commit_data: ScaleType[Optional[CommitmentOfResponse]] = self.substrate.query( module="Commitments", storage_function="CommitmentOf", params=[netuid, hotkey_ss58], block_hash=self.determine_block_hash(block), ) - if commit_data is None: + if commit_data.value is None: return "" - return cast(Union[str, dict], getattr(commit_data, "value", commit_data)) + return commit_data.value def get_crowdloan_constants( self, constants: Optional[list[str]] = None, block: Optional[int] = None, - ) -> "CrowdloanConstants": + ) -> CrowdloanConstants: """Retrieves runtime configuration constants governing crowdloan behavior and limits on the Bittensor blockchain. If a list of constant names is provided, only those constants will be queried. @@ -1918,7 +1918,7 @@ def get_crowdloan_contributions( self, crowdloan_id: int, block: Optional[int] = None, - ) -> dict[str, "Balance"]: + ) -> dict[str, Balance]: """Retrieves all contributions made to a specific crowdloan campaign. Returns a mapping of contributor coldkey addresses to their contribution amounts in Rao. @@ -1945,14 +1945,16 @@ def get_crowdloan_contributions( block_hash=block_hash, ) result = {} - for record in query.records: - if record[1].value: - result[decode_account_id(record[0])] = Balance.from_rao(record[1].value) + contributor: str + amount: int + for contributor, amount in query: + if amount: + result[contributor] = Balance.from_rao(amount) return result def get_crowdloan_by_id( self, crowdloan_id: int, block: Optional[int] = None - ) -> Optional["CrowdloanInfo"]: + ) -> Optional[CrowdloanInfo]: """Retrieves detailed information about a specific crowdloan campaign. Parameters: @@ -1970,13 +1972,13 @@ def get_crowdloan_by_id( - Crowdloans Overview: """ block_hash = self.determine_block_hash(block) - query = self.substrate.query( + query: ScaleType[Optional[CrowdloansResponse]] = self.substrate.query( module="Crowdloan", storage_function="Crowdloans", params=[crowdloan_id], block_hash=block_hash, ) - if not query: + if not query.value: return None return self._decode_crowdloan_entry( crowdloan_id=crowdloan_id, data=query.value, block_hash=block_hash @@ -2002,13 +2004,12 @@ def get_crowdloan_next_id( - Crowdloan Tutorial: """ block_hash = self.determine_block_hash(block) - result = self.substrate.query( + result: ScaleType[int] = self.substrate.query( module="Crowdloan", storage_function="NextCrowdloanId", block_hash=block_hash, ) - value = cast(int, getattr(result, "value", result)) - return int(value) or 0 + return result.value def get_crowdloans( self, @@ -2040,11 +2041,9 @@ def get_crowdloans( ) crowdloans = [] - - for c_id, value_obj in getattr(query, "records", []): - data = value_obj.value - if not data: - continue + c_id: int + data: CrowdloansResponse + for c_id, data in query: crowdloans.append( self._decode_crowdloan_entry( crowdloan_id=c_id, data=data, block_hash=block_hash @@ -2110,8 +2109,8 @@ def get_delegate_identities( ) return { - decode_account_id(ss58_address[0]): ChainIdentity.from_dict( - decode_hex_identity_dict(identity.value), + ss58_address: ChainIdentity.from_dict( + decode_hex_identity_dict(identity), ) for ss58_address, identity in identities } @@ -2131,13 +2130,13 @@ def get_delegate_take(self, hotkey_ss58: str, block: Optional[int] = None) -> fl Notes: - """ - result = self.query_subtensor( + result: ScaleType[int] = self.query_subtensor( # type: ignore[assignment] name="Delegates", block=block, params=[hotkey_ss58], ) - return u16_normalized_float(result.value) # type: ignore + return u16_normalized_float(result.value) def get_delegated( self, coldkey_ss58: str, block: Optional[int] = None @@ -2172,7 +2171,7 @@ def get_delegated( return DelegatedInfo.list_from_dicts(result) - def get_delegates(self, block: Optional[int] = None) -> list["DelegateInfo"]: + def get_delegates(self, block: Optional[int] = None) -> list[DelegateInfo]: """Fetches all delegates registered on the chain. Delegates are validators that accept stake from other TAO holders (nominators/delegators). This method @@ -2200,7 +2199,7 @@ def get_delegates(self, block: Optional[int] = None) -> list["DelegateInfo"]: else: return [] - def get_existential_deposit(self, block: Optional[int] = None) -> Optional[Balance]: + def get_existential_deposit(self, block: Optional[int] = None) -> Balance: """Retrieves the existential deposit amount for the Bittensor blockchain. The existential deposit is the minimum amount of TAO required for an account to exist on the blockchain. @@ -2216,7 +2215,7 @@ def get_existential_deposit(self, block: Optional[int] = None) -> Optional[Balan Notes: - """ - result = self.substrate.get_constant( + result: Optional[ScaleType[int]] = self.substrate.get_constant( # type: ignore[assignment] module_name="Balances", constant_name="ExistentialDeposit", block_hash=self.determine_block_hash(block), @@ -2225,7 +2224,7 @@ def get_existential_deposit(self, block: Optional[int] = None) -> Optional[Balan if result is None: raise Exception("Unable to retrieve existential deposit amount.") - return Balance.from_rao(getattr(result, "value", 0)) + return Balance.from_rao(result.value) def get_ema_tao_inflow( self, @@ -2269,6 +2268,7 @@ def get_ema_tao_inflow( block_updated, tao_bits = query.value ema_value = int(fixed_to_float(tao_bits)) + # TODO verify this from rao, seems like we're just rounding down return block_updated, Balance.from_rao(ema_value) def get_hotkey_owner( @@ -2286,21 +2286,22 @@ def get_hotkey_owner( Returns: The SS58 address of the owner if the hotkey exists, or `None` if it doesn't. """ - hk_owner_query = self.substrate.query( + hk_owner: ScaleType[str] = self.substrate.query( module="SubtensorModule", storage_function="Owner", params=[hotkey_ss58], block_hash=self.determine_block_hash(block), ) - exists = False - if hk_owner_query: + if hk_owner.value != "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM": exists = self.does_hotkey_exist(hotkey_ss58, block=block) - hotkey_owner = hk_owner_query if exists else None - return cast(Optional[str], getattr(hotkey_owner, "value", hotkey_owner)) + else: + exists = False + hotkey_owner = hk_owner.value if exists else None + return hotkey_owner def get_last_bonds_reset( self, netuid: int, hotkey_ss58: str, block: Optional[int] = None - ): + ) -> ScaleType[Optional[int]]: """Retrieves the block number when bonds were last reset for a specific hotkey on a subnet. Parameters: @@ -2309,18 +2310,20 @@ def get_last_bonds_reset( block: The block number to query. If `None`, queries the current chain head. Returns: - The block number when bonds were last reset, or `None` if no bonds reset has occurred. + A ScaleType object containing the block number when bonds were last reset, or `None` if no bonds reset + has occurred. Notes: - - """ - return self.substrate.query( + block_: ScaleType[Optional[int]] = self.substrate.query( module="Commitments", storage_function="LastBondsReset", params=[netuid, hotkey_ss58], block_hash=self.determine_block_hash(block), ) + return block_ def get_last_commitment_bonds_reset_block( self, @@ -2349,10 +2352,7 @@ def get_last_commitment_bonds_reset_block( ) return None block_data = self.get_last_bonds_reset(netuid, hotkey_ss58, block) - try: - return decode_block(block_data) - except TypeError: - return None + return block_data.value def get_liquidity_list( self, @@ -2422,18 +2422,20 @@ def get_liquidity_list( ) ) - fee_global_tao = fixed_to_float(fee_global_tao_query[1]) - fee_global_alpha = fixed_to_float(fee_global_alpha_query[1]) - sqrt_price = fixed_to_float(sqrt_price_query[1]) + fee_global_tao_raw: FixedPoint = fee_global_tao_query[1] # type: ignore[assignment] + fee_global_alpha_raw: FixedPoint = fee_global_alpha_query[1] # type: ignore[assignment] + sqrt_price_raw: FixedPoint = sqrt_price_query[1] # type: ignore[assignment] + fee_global_tao = fixed_to_float(fee_global_tao_raw) + fee_global_alpha = fixed_to_float(fee_global_alpha_raw) + sqrt_price = fixed_to_float(sqrt_price_raw) current_tick = price_to_tick(sqrt_price**2) - positions_values: list[tuple[dict, int, int]] = [] + positions_values: list[tuple[PositionResponse, int, int]] = [] positions_storage_keys: list[StorageKey] = [] - for _, p in positions_response: - position = p.value - - tick_low_idx = position["tick_low"][0] - tick_high_idx = position["tick_high"][0] + position: PositionResponse + for _, position in positions_response: + tick_low_idx = position["tick_low"] + tick_high_idx = position["tick_high"] tick_low_sk = self.substrate.create_storage_key( pallet="Swap", @@ -2454,8 +2456,9 @@ def get_liquidity_list( positions_storage_keys, block_hash=block_hash ) # iterator with just the values - ticks = iter([x[1] for x in ticks_query]) - positions = [] + tick_values: list[dict] = [x[1] for x in ticks_query] # type: ignore + ticks = iter(tick_values) + positions: list[LiquidityPosition] = [] for position, tick_low_idx, tick_high_idx in positions_values: tick_low = next(ticks) tick_high = next(ticks) @@ -2512,19 +2515,15 @@ def get_liquidity_list( positions.append( LiquidityPosition( - **{ - "id": position.get("id")[0], - "price_low": Balance.from_tao( - tick_to_price(position.get("tick_low")[0]) - ), - "price_high": Balance.from_tao( - tick_to_price(position.get("tick_high")[0]) - ), - "liquidity": Balance.from_rao(position.get("liquidity")), - "fees_tao": fees_tao, - "fees_alpha": fees_alpha, - "netuid": position.get("netuid"), - } + id=position.get("id"), + price_low=Balance.from_tao(tick_to_price(position.get("tick_low"))), + price_high=Balance.from_tao( + tick_to_price(position.get("tick_high")) + ), + liquidity=Balance.from_rao(position.get("liquidity")), + fees_tao=fees_tao, + fees_alpha=fees_alpha, + netuid=position.get("netuid"), ) ) @@ -2550,16 +2549,16 @@ def get_mechanism_emission_split( module, storage_function, block_hash=block_hash ): return None - result = self.substrate.query( + result: ScaleType[Optional[list[int]]] = self.substrate.query( module="SubtensorModule", storage_function="MechanismEmissionSplit", params=[netuid], block_hash=block_hash, ) - if result is None or not hasattr(result, "value"): + if result.value is None: return None - - return [round(i / sum(result.value) * 100) for i in result.value] + total = sum(result.value) + return [round(i / total * 100) for i in result.value] def get_mechanism_count( self, @@ -2585,13 +2584,13 @@ def get_mechanism_count( module, storage_function, block_hash=block_hash ): return 1 - query = self.substrate.query( + query: ScaleType[Optional[int]] = self.substrate.query( module=module, storage_function=storage_function, params=[netuid], block_hash=block_hash, ) - return query.value if query is not None and hasattr(query, "value") else 1 + return query.value or 1 def get_metagraph_info( self, @@ -2680,13 +2679,13 @@ def get_metagraph_info( default_value=None, ) - if query is None or not hasattr(query, "value") or query.value is None: + if query is None: logging.error( f"Subnet mechanism {netuid}.{mechid if mechid else 0} does not exist." ) return None - return MetagraphInfo.from_dict(query.value) + return MetagraphInfo.from_dict(query) def get_mev_shield_current_key( self, block: Optional[int] = None @@ -2708,16 +2707,18 @@ def get_mev_shield_current_key( announced a key yet. """ block_hash = self.determine_block_hash(block=block) - query = self.substrate.query( + query: ScaleType[Optional[bytearray]] = self.substrate.query( module="MevShield", storage_function="CurrentKey", block_hash=block_hash, ) - if query is None: + if query.value_object is None: return None - public_key_bytes = bytes(next(iter(query))) + value: bytearray = query.value_object + + public_key_bytes = bytes(value) # Validate public_key size for ML-KEM-768 if len(public_key_bytes) != MLKEM768_PUBLIC_KEY_SIZE: @@ -2746,16 +2747,18 @@ def get_mev_shield_next_key(self, block: Optional[int] = None) -> Optional[bytes announced the next key yet. """ block_hash = self.determine_block_hash(block=block) - query = self.substrate.query( + query: ScaleType[Optional[bytearray]] = self.substrate.query( module="MevShield", storage_function="NextKey", block_hash=block_hash, ) - if query is None: + if query.value_object is None: return None - public_key_bytes = bytes(next(iter(query))) + value: bytearray = query.value_object + + public_key_bytes = bytes(value) # Validate public_key size for ML-KEM-768 (must be exactly 1184 bytes) if len(public_key_bytes) != MLKEM768_PUBLIC_KEY_SIZE: @@ -2787,7 +2790,7 @@ def get_minimum_required_stake(self) -> Balance: module="SubtensorModule", storage_function="NominatorMinRequiredStake" ) - return Balance.from_rao(getattr(result, "value", 0)) + return Balance.from_rao(result.value or 0) def get_netuids_for_hotkey( self, hotkey_ss58: str, block: Optional[int] = None @@ -2812,10 +2815,12 @@ def get_netuids_for_hotkey( block_hash=self.determine_block_hash(block), ) netuids = [] + netuid: int + is_member: bool if result.records: - for record in result: - if record[1].value: - netuids.append(record[0]) + for netuid, is_member in result: + if is_member: + netuids.append(netuid) return netuids def get_neuron_certificate( @@ -2836,23 +2841,22 @@ def get_neuron_certificate( This function is used for certificate discovery for setting up mutual tls communication between neurons. """ - certificate_query = self.query_module( - module="SubtensorModule", - name="NeuronCertificates", - block=block, - params=[netuid, hotkey_ss58], + certificate_query: ScaleType[Optional[str | NeuronCertificateResponse]] = ( + self.query_module( + module="SubtensorModule", + name="NeuronCertificates", + block=block, + params=[netuid, hotkey_ss58], + ) ) - try: - if certificate_query: - certificate = cast(dict, certificate_query) - return Certificate(certificate) - except AttributeError: - return None + certificate: Optional[str | NeuronCertificateResponse] = certificate_query.value + if certificate is not None: + return Certificate(certificate) return None def get_neuron_for_pubkey_and_subnet( self, hotkey_ss58: str, netuid: int, block: Optional[int] = None - ) -> Optional["NeuronInfo"]: + ) -> NeuronInfo: """ Retrieves information about a neuron based on its public key (hotkey SS58 address) and the specific subnet UID (netuid). This function provides detailed neuron information for a particular subnet within the Bittensor @@ -2876,7 +2880,7 @@ def get_neuron_for_pubkey_and_subnet( params=[netuid, hotkey_ss58], block_hash=block_hash, ) - if (uid := getattr(uid_query, "value", None)) is None: + if (uid := uid_query.value) is None: return NeuronInfo.get_null_neuron() return self.neuron_for_uid( @@ -2941,7 +2945,7 @@ def get_owned_hotkeys( params=[coldkey_ss58], block_hash=block_hash, ) - return [decode_account_id(hotkey[0]) for hotkey in owned_hotkeys or []] + return owned_hotkeys.value or [] def get_parents( self, hotkey_ss58: str, netuid: int, block: Optional[int] = None @@ -2968,11 +2972,10 @@ def get_parents( params=[hotkey_ss58, netuid], block_hash=self.determine_block_hash(block), ) - if parents: + if parents.value: formatted_parents = [] - for proportion, parent in parents.value: + for proportion, formatted_child in parents.value: # Convert U64 to int - formatted_child = decode_account_id(parent[0]) normalized_proportion = u64_normalized_float(proportion) formatted_parents.append((normalized_proportion, formatted_child)) return formatted_parents @@ -3071,14 +3074,13 @@ def get_proxy_announcement( - See: """ block_hash = self.determine_block_hash(block) - query = self.substrate.query( + query: ScaleType[tuple[list[dict], int]] = self.substrate.query( module="Proxy", storage_function="Announcements", params=[delegate_account_ss58], block_hash=block_hash, ) - query_value = getattr(query, "value", query) - return ProxyAnnouncementInfo.from_dict(cast(list[Any], query_value)[0]) + return ProxyAnnouncementInfo.from_dict(query.value) def get_proxy_announcements( self, @@ -3230,15 +3232,17 @@ def get_revealed_commitment_by_hotkey( params=[netuid, hotkey_ss58], block=block, ) - if query is None: + if query.value_serialized is None: return None - return tuple(decode_revealed_commitment(pair) for pair in query) + return tuple( + decode_revealed_commitment(pair) for pair in query.value_serialized + ) def get_root_claim_type( self, coldkey_ss58: str, block: Optional[int] = None, - ) -> Union[str, dict]: + ) -> str | dict[str, dict[str, list[int]]]: """Return the configured root claim type for a given coldkey. The root claim type controls how dividends from staking to the Root Subnet (subnet 0) are processed when they @@ -3249,7 +3253,7 @@ def get_root_claim_type( Parameters: coldkey_ss58: The SS58 address of the coldkey whose root claim preference to query. - block: The block number to query. Do not specify if using `block_hash` or `reuse_block`. + block: The block number to query. Returns: @@ -3268,24 +3272,7 @@ def get_root_claim_type( params=[coldkey_ss58], block_hash=self.determine_block_hash(block), ) - query_value = getattr(query, "value", query) - claim_type = cast(dict[str, Any], query_value) - # Query returns enum as dict: {"Swap": ()} or {"Keep": ()} or {"KeepSubnets": {"subnets": [1, 2, 3]}} - variant_name = next(iter(claim_type.keys())) - variant_value = claim_type[variant_name] - - # For simple variants (Swap, Keep), value is empty tuple, return string - if not variant_value or variant_value == (): - return variant_name - - # For KeepSubnets, value contains the data, return full dict structure - if isinstance(variant_value, dict) and "subnets" in variant_value: - subnets_raw = variant_value["subnets"] - subnets = list(subnets_raw[0]) - - return {variant_name: {"subnets": subnets}} - - return {variant_name: variant_value} + return query.value def get_root_alpha_dividends_per_subnet( self, @@ -3306,14 +3293,13 @@ def get_root_alpha_dividends_per_subnet( Returns: Balance: The root alpha dividends for this hotkey on this subnet in Rao, with unit set to netuid. """ - query = self.substrate.query( + query: ScaleType[int] = self.substrate.query( module="SubtensorModule", storage_function="RootAlphaDividendsPerSubnet", params=[netuid, hotkey_ss58], block_hash=self.determine_block_hash(block), ) - value = getattr(query, "value", query) - return Balance.from_rao(cast(int, value)).set_unit(netuid=netuid) + return Balance.from_rao(query.value, netuid=netuid) def get_root_claimable_rate( self, @@ -3364,15 +3350,15 @@ def get_root_claimable_all_rates( Notes: - See: """ - query = self.substrate.query( + query: ScaleType[list[tuple[int, FixedPoint]]] = self.substrate.query( module="SubtensorModule", storage_function="RootClaimable", params=[hotkey_ss58], block_hash=self.determine_block_hash(block), ) - query_value = getattr(query, "value", query) - bits_list = next(iter(cast(list[list[tuple[int, FixedPoint]]], query_value))) - return {bits[0]: fixed_to_float(bits[1], frac_bits=32) for bits in bits_list} + return { + netuid: fixed_to_float(bits, frac_bits=32) for (netuid, bits) in query.value + } def get_root_claimable_stake( self, @@ -3447,14 +3433,13 @@ def get_root_claimed( Notes: - See: """ - query = self.substrate.query( + query: ScaleType[int] = self.substrate.query( module="SubtensorModule", storage_function="RootClaimed", params=[netuid, hotkey_ss58, coldkey_ss58], block_hash=self.determine_block_hash(block), ) - value = getattr(query, "value", query) - return Balance.from_rao(cast(int, value)).set_unit(netuid=netuid) + return Balance.from_rao(query.value, netuid=netuid) def get_stake( self, @@ -3523,7 +3508,7 @@ def get_stake_for_coldkey_and_hotkey( def get_stake_info_for_coldkey( self, coldkey_ss58: str, block: Optional[int] = None - ) -> list["StakeInfo"]: + ) -> list[StakeInfo]: """ Retrieves the stake information for a given coldkey. @@ -3547,7 +3532,7 @@ def get_stake_info_for_coldkey( def get_stake_info_for_coldkeys( self, coldkey_ss58s: list[str], block: Optional[int] = None - ) -> dict[str, list["StakeInfo"]]: + ) -> dict[str, list[StakeInfo]]: """ Retrieves the stake information for multiple coldkeys. @@ -3568,10 +3553,7 @@ def get_stake_info_for_coldkeys( if query is None: return {} - return { - decode_account_id(ck): StakeInfo.list_from_dicts(st_info) - for ck, st_info in query - } + return {ck: StakeInfo.list_from_dicts(st_info) for ck, st_info in query} def get_stake_for_hotkey( self, hotkey_ss58: str, netuid: int, block: Optional[int] = None @@ -3584,13 +3566,10 @@ def get_stake_for_hotkey( netuid: The subnet ID to query for. block: The block number at which to query the stake information. """ - hotkey_alpha_query = self.query_subtensor( + hotkey_alpha_query: ScaleType[int] = self.query_subtensor( # type: ignore[assignment] name="TotalHotkeyAlpha", params=[hotkey_ss58, netuid], block=block ) - hotkey_alpha = cast(ScaleObj, hotkey_alpha_query) - balance = Balance.from_rao(hotkey_alpha.value) - balance.set_unit(netuid=netuid) - return balance + return Balance.from_rao(hotkey_alpha_query.value, netuid=netuid) get_hotkey_stake = get_stake_for_hotkey @@ -3665,6 +3644,27 @@ def get_stake_weight(self, netuid: int, block: Optional[int] = None) -> list[flo ) return [u16_normalized_float(w) for w in cast(list[int], result or [])] + def get_staking_hotkeys( + self, coldkey_ss58: str, block: Optional[int] = None + ) -> list[str]: + """ + Retrieves the hotkeys that have staked for a given coldkey. + + Parameters: + coldkey_ss58: The SS58 address of the coldkey. + block: The block number at which to query the stake information. + + Returns: + A list of hotkey SS58 addresses that have staked for the given coldkey. + """ + result = self.substrate.query( + module="SubtensorModule", + storage_function="StakingHotkeys", + params=[coldkey_ss58], + block_hash=self.determine_block_hash(block), + ) + return result.value or [] + def get_start_call_delay(self, block: Optional[int] = None) -> int: """ Retrieves the start call delay in blocks. @@ -3675,13 +3675,11 @@ def get_start_call_delay(self, block: Optional[int] = None) -> int: Return: Amount of blocks after the start call can be executed. """ - return cast( - int, - self.query_subtensor( - name="StartCallDelay", - block=block, - ), + query: ScaleType[int] = self.query_subtensor( # type: ignore[assignment] + name="StartCallDelay", + block=block, ) + return query.value def get_subnet_burn_cost(self, block: Optional[int] = None) -> Optional[Balance]: """ @@ -3808,12 +3806,12 @@ def get_subnet_price( return Balance.from_tao(1) block_hash = self.determine_block_hash(block=block) - price_rao = self.substrate.runtime_call( + price_rao: int = self.substrate.runtime_call( # type: ignore[assignment] api="SwapRuntimeApi", method="current_alpha_price", params=[netuid], block_hash=block_hash, - ).value + ) return Balance.from_rao(price_rao) def get_subnet_prices( @@ -3841,10 +3839,10 @@ def get_subnet_prices( ) prices = {} - for id_, current_sqrt_price in current_sqrt_prices: - current_sqrt_price = fixed_to_float(current_sqrt_price) + for id_, current_sqrt_price_bits in current_sqrt_prices: + current_sqrt_price = fixed_to_decimal(current_sqrt_price_bits) current_price = current_sqrt_price * current_sqrt_price - current_price_in_tao = Balance.from_rao(int(current_price * 1e9)) + current_price_in_tao = Balance.from_tao(float(current_price)) prices.update({id_: current_price_in_tao}) # SN0 price is always 1 TAO @@ -3932,10 +3930,11 @@ def get_timelocked_weight_commits( storage_function="TimelockedWeightCommits", params=[storage_index], block_hash=self.determine_block_hash(block=block), + page_size=1, ) commits = result.records[0][1] if result.records else [] - return [WeightCommitInfo.from_vec_u8_v2(commit) for commit in commits] + return [WeightCommitInfo.from_vec_u8_v2(commit) for commit in commits] # type: ignore[arg-type,union-attr] def get_timestamp(self, block: Optional[int] = None) -> datetime: """ @@ -3947,8 +3946,8 @@ def get_timestamp(self, block: Optional[int] = None) -> datetime: Returns: datetime object for the timestamp of the block """ - unix = cast(ScaleObj, self.query_module("Timestamp", "Now", block=block)).value - return datetime.fromtimestamp(unix / 1000, tz=timezone.utc) + unix = self.query_module("Timestamp", "Now", block=block) + return datetime.fromtimestamp(unix.value / 1000, tz=timezone.utc) def get_total_subnets(self, block: Optional[int] = None) -> Optional[int]: """Retrieves the total number of subnets within the Bittensor network as of a specific blockchain block. @@ -4046,7 +4045,7 @@ def get_unstake_fee( def get_vote_data( self, proposal_hash: str, block: Optional[int] = None - ) -> Optional["ProposalVoteData"]: + ) -> Optional[ProposalVoteData]: # TODO: is this all deprecated? Didn't subtensor senate stuff get removed? """ Retrieves the voting data for a specific proposal on the Bittensor blockchain. This data includes information @@ -4062,20 +4061,17 @@ def get_vote_data( This function is important for tracking and understanding the decision-making processes within the Bittensor network, particularly how proposals are received and acted upon by the governing body. """ - vote_data = cast( - Optional[dict[str, Any]], - self.substrate.query( - module="Triumvirate", - storage_function="Voting", - params=[proposal_hash], - block_hash=self.determine_block_hash(block), - ), + vote_data: ScaleType[Optional[dict[str, Any]]] = self.substrate.query( + module="Triumvirate", + storage_function="Voting", + params=[proposal_hash], + block_hash=self.determine_block_hash(block), ) - if vote_data is None: + if vote_data.value is None: return None - return ProposalVoteData.from_dict(vote_data) + return ProposalVoteData.from_dict(vote_data.value) def get_uid_for_hotkey_on_subnet( self, hotkey_ss58: str, netuid: int, block: Optional[int] = None @@ -4232,7 +4228,8 @@ def is_fast_blocks(self) -> bool: - """ - slot_duration_obj = cast(ScaleObj, self.query_constant("Aura", "SlotDuration")) + slot_duration_obj = self.query_constant("Aura", "SlotDuration") + assert slot_duration_obj is not None return slot_duration_obj.value == 250 def is_hotkey_delegate(self, hotkey_ss58: str, block: Optional[int] = None) -> bool: @@ -4345,7 +4342,11 @@ def is_subnet_active(self, netuid: int, block: Optional[int] = None) -> bool: block=block, params=[netuid], ) - return True if query and query.value > 0 else False + qv: Optional[int] = query.value # type: ignore[assignment] + if qv is None or qv <= 0: + return False + else: + return True def last_drand_round(self) -> Optional[int]: """Retrieves the last drand round emitted in Bittensor. @@ -4564,27 +4565,17 @@ def query_identity( See the `Bittensor CLI documentation `_ for supported identity parameters. """ + print(coldkey_ss58) identity_info = self.substrate.query( module="SubtensorModule", storage_function="IdentitiesV2", params=[coldkey_ss58], block_hash=self.determine_block_hash(block), ) - - if not identity_info: - return None - - try: - identity_data = ( - identity_info.value - if hasattr(identity_info, "value") - else identity_info - ) - return ChainIdentity.from_dict( - decode_hex_identity_dict(cast(dict[str, Any], identity_data)), - ) - except TypeError: + identity_data: Optional[dict[str, dict[Any, Any] | str]] = identity_info.value + if identity_data is None: return None + return ChainIdentity.from_dict(decode_hex_identity_dict(identity_data)) def recycle(self, netuid: int, block: Optional[int] = None) -> Optional[Balance]: """Retrieves the 'Burn' hyperparameter for a specified subnet. @@ -4619,14 +4610,17 @@ def subnet(self, netuid: int, block: Optional[int] = None) -> Optional[DynamicIn """ block_hash = self.determine_block_hash(block=block) - query = self.substrate.runtime_call( - api="SubnetInfoRuntimeApi", - method="get_dynamic_info", - params=[netuid], - block_hash=block_hash, + decoded = cast( + Optional[DynamicInfoResponse], + self.substrate.runtime_call( + api="SubnetInfoRuntimeApi", + method="get_dynamic_info", + params=[netuid], + block_hash=block_hash, + ), ) - if isinstance(decoded := query.decode(), dict): + if isinstance(decoded, dict): try: price = self.get_subnet_price(netuid=netuid, block=block) except (SubstrateRequestException, ValueError): @@ -4780,7 +4774,7 @@ def weights( params=[storage_index], block_hash=self.determine_block_hash(block), ) - w_map = [(uid, w.value or []) for uid, w in w_map_encoded] + w_map = [(uid, w or []) for uid, w in w_map_encoded] return w_map @@ -4853,7 +4847,7 @@ def validate_extrinsic_params( ) # Expected params from metadata - expected_params = func_meta.get_param_info() + expected_params = func_meta.get_param_info() # type: ignore provided_params = {} # Validate and filter parameters @@ -5000,6 +4994,7 @@ def sign_and_send_extrinsic( extrinsic_response.extrinsic_receipt = response if response.is_success: + assert response.total_fee_amount is not None extrinsic_response.extrinsic_fee = Balance.from_rao( response.total_fee_amount ) @@ -5046,7 +5041,7 @@ def get_extrinsic_fee( # Estimate fee before sending a transfer call = subtensor.compose_call( call_module="Balances", - call_function="transfer", + call_function="transfer_allow_death", call_params={"dest": destination_ss58, "value": amount.rao} ) fee = subtensor.get_extrinsic_fee(call=call, keypair=wallet.coldkey) @@ -5924,6 +5919,50 @@ def dispute_coldkey_swap( wait_for_revealed_execution=wait_for_revealed_execution, ) + def clear_coldkey_swap_announcement( + self, + wallet: "Wallet", + *, + mev_protection: bool = DEFAULT_MEV_PROTECTION, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + wait_for_revealed_execution: bool = True, + ) -> ExtrinsicResponse: + """ + Clears (withdraws) a pending coldkey swap announcement. + + Callable by the coldkey that has an active, undisputed swap announcement. The reannouncement delay must have + elapsed past the execution block before the announcement can be cleared. + + Parameters: + wallet: Bittensor wallet object (should be the current coldkey with an active announcement). + mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet. + period: The number of blocks during which the transaction will remain valid. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + wait_for_revealed_execution: Whether to wait for the revealed execution if mev_protection used. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + Notes: + - The coldkey must have an active, undisputed swap announcement. + - The reannouncement delay must have elapsed past the execution block. + """ + return clear_coldkey_swap_announcement_extrinsic( + subtensor=self, + wallet=wallet, + mev_protection=mev_protection, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + def dissolve_crowdloan( self, wallet: "Wallet", @@ -6620,14 +6659,7 @@ def register( self, wallet: "Wallet", netuid: int, - max_allowed_attempts: int = 3, - output_in_place: bool = True, - cuda: bool = False, - dev_id: Union[list[int], int] = 0, - tpb: int = 256, - num_processes: Optional[int] = None, - update_interval: Optional[int] = None, - log_verbose: bool = False, + limit_price: Optional[Balance] = None, *, mev_protection: bool = DEFAULT_MEV_PROTECTION, period: Optional[int] = DEFAULT_PERIOD, @@ -6636,56 +6668,119 @@ def register( wait_for_finalization: bool = True, wait_for_revealed_execution: bool = True, ) -> ExtrinsicResponse: - """ - Registers a neuron on the Bittensor subnet with provided netuid using the provided wallet. + """Registers a neuron on the Bittensor network by recycling TAO, with automatic price protection. - Registration is a critical step for a neuron to become an active participant in the network, enabling it to - stake, set weights, and receive incentives. + Uses ``register_limit`` under the hood. If ``limit_price`` is not provided, it is automatically + calculated as the current recycle (burn) cost plus a 0.5% tolerance to protect against price fluctuations. + + For root subnet (``netuid == 0``), delegates to ``root_register_extrinsic``. Parameters: wallet: The wallet associated with the neuron to be registered. netuid: The unique identifier of the subnet. - max_allowed_attempts: Maximum number of attempts to register the wallet. - output_in_place: If `True`, prints the progress of the proof of work to the console in-place. Meaning the - progress is printed on the same lines. - cuda: If `true`, the wallet should be registered using CUDA device(s). - dev_id: The CUDA device id to use, or a list of device ids. - tpb: The number of threads per block (CUDA). - num_processes: The number of processes to use to register. - update_interval: The number of nonces to solve between updates. - log_verbose: If `true`, the registration process will log more information. - mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect + limit_price: Maximum acceptable burn price as a Balance instance. If ``None``, automatically calculated + as ``recycle * 1.005`` (0.5% tolerance). If the on-chain burn price exceeds this value, the + transaction will fail with RegistrationPriceLimitExceeded. + mev_protection: If ``True``, encrypts and submits the transaction through the MEV Shield pallet to protect against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators - decrypt and execute it. If `False`, submits the transaction directly without encryption. + decrypt and execute it. If ``False``, submits the transaction directly without encryption. period: The number of blocks during which the transaction will remain valid after it's submitted. If the transaction is not included in a block within that number of blocks, it will expire and be rejected. You can think of it as an expiration date for the transaction. - raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. - wait_for_inclusion: Whether to wait for the inclusion of the transaction. - wait_for_finalization: Whether to wait for the finalization of the transaction. + raise_error: Raises a relevant exception rather than returning ``False`` if unsuccessful. + wait_for_inclusion: Waits for the transaction to be included in a block. + wait_for_finalization: Waits for the transaction to be finalized on the blockchain. wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. Returns: ExtrinsicResponse: The result object of the extrinsic execution. - This function facilitates the entry of new neurons into the network, supporting the decentralized growth and - scalability of the Bittensor ecosystem. + Notes: + - Rate Limits: + """ + if netuid == 0: + return root_register_extrinsic( + subtensor=self, + wallet=wallet, + mev_protection=mev_protection, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + + if limit_price is not None: + check_balance_amount(limit_price) + else: + recycle = self.recycle(netuid=netuid) + if recycle is None: + return ExtrinsicResponse( + False, f"Subnet {netuid} does not exist." + ).with_log() + limit_price = Balance.from_rao(recycle.rao * 1005 // 1000) + + return register_limit_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + limit_price=limit_price, + mev_protection=mev_protection, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + + def register_limit( + self, + wallet: "Wallet", + netuid: int, + limit_price: Balance, + *, + mev_protection: bool = DEFAULT_MEV_PROTECTION, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + wait_for_revealed_execution: bool = True, + ) -> ExtrinsicResponse: + """ + Registers a neuron on the Bittensor network by recycling TAO, with a maximum burn price limit. + + Unlike ``burned_register``, this method includes a ``limit_price`` parameter that ensures the registration + will only proceed if the current on-chain burn price does not exceed the specified maximum. This protects + against unexpected price spikes between reading the price and submitting the transaction. + + Parameters: + wallet: The wallet associated with the neuron to be registered. + netuid: The unique identifier of the subnet. + limit_price: Maximum acceptable burn price as a Balance instance. If the on-chain burn price exceeds + this value, the transaction will fail with RegistrationPriceLimitExceeded. + mev_protection: If ``True``, encrypts and submits the transaction through the MEV Shield pallet to protect + against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators + decrypt and execute it. If ``False``, submits the transaction directly without encryption. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You + can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning ``False`` if unsuccessful. + wait_for_inclusion: Waits for the transaction to be included in a block. + wait_for_finalization: Waits for the transaction to be finalized on the blockchain. + wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. Notes: - Rate Limits: """ - return register_extrinsic( + check_balance_amount(limit_price) + return register_limit_extrinsic( subtensor=self, wallet=wallet, netuid=netuid, - max_allowed_attempts=max_allowed_attempts, - tpb=tpb, - update_interval=update_interval, - num_processes=num_processes, - cuda=cuda, - dev_id=dev_id, - output_in_place=output_in_place, - log_verbose=log_verbose, + limit_price=limit_price, mev_protection=mev_protection, period=period, raise_error=raise_error, diff --git a/bittensor/core/synapse.py b/bittensor/core/synapse.py index c29629a144..99b7af3ae8 100644 --- a/bittensor/core/synapse.py +++ b/bittensor/core/synapse.py @@ -638,16 +638,15 @@ def to_headers(self) -> dict: # Getting the fields of the instance instance_fields = self.model_dump() + required = self.get_required_fields() # Iterating over the fields of the instance for field, value in instance_fields.items(): - # If the object is not optional, serializing it, encoding it, and adding it to the headers - required = self.get_required_fields() - # Skipping the field if it's already in the headers or its value is None if field in headers or value is None: continue + # If the object is not optional, serializing it, encoding it, and adding it to the headers elif required and field in required: try: # create an empty (dummy) instance of type(value) to pass pydantic validation on the axon side diff --git a/bittensor/core/types.py b/bittensor/core/types.py index a30cfe3b17..67ce355cf4 100644 --- a/bittensor/core/types.py +++ b/bittensor/core/types.py @@ -5,6 +5,7 @@ import numpy as np from numpy.typing import NDArray +from scalecodec.utils.math import FixedPoint from bittensor.core import settings from bittensor.core.chain_data import NeuronInfo, NeuronInfoLite @@ -20,6 +21,12 @@ ) from bittensor.utils.btlogging import logging +try: + from typing import NotRequired +except ImportError: + # fallback to typing_extensions if Python < 3.11 + from typing_extensions import NotRequired + if TYPE_CHECKING: from bittensor_wallet import Wallet from bittensor.utils.balance import Balance @@ -575,3 +582,79 @@ class BlockInfo: header: dict extrinsics: list explorer: str + + +# TypedDicts +class PositionResponse(TypedDict): + id: int + netuid: int + tick_low: int + tick_high: int + liquidity: int + fees_tao: FixedPoint + fees_alpha: FixedPoint + + +class NeuronCertificateResponse(TypedDict): + public_key: str + algorithm: int + + +class _CommitmentFields(TypedDict): + fields: list[dict[str, str]] + + +class CommitmentOfResponse(TypedDict): + deposit: int + block: int + info: _CommitmentFields + + +class CrowdloansResponse(TypedDict): + creator: str + deposit: int + min_contribution: int + end: int + cap: int + funds_account: str + raised: int + target_address: str + call: Optional[dict] + finalized: bool + contributors_count: int + + +class SubnetIdentityResponse(TypedDict): + subnet_name: str + github_repo: str + subnet_contact: str + subnet_url: str + discord: str + description: str + logo_url: str + additional: str + + +class DynamicInfoResponse(TypedDict): + netuid: int + owner_hotkey: str + owner_coldkey: str + subnet_name: list[int] # needs bytes.decode('utf-8') to stringify + token_symbol: list[int] # needs bytes.decode('utf-8') to stringify + tempo: int + last_step: int + blocks_since_last_step: int + emission: int + alpha_in: int + alpha_out: int + tao_in: int + alpha_out_emission: int + alpha_in_emission: int + tao_in_emission: int + pending_alpha_emission: int + pending_root_emission: int + subnet_volume: int + network_registered_at: int + subnet_identity: SubnetIdentityResponse + moving_price: FixedPoint + price: NotRequired["Balance"] diff --git a/bittensor/extras/dev_framework/__init__.py b/bittensor/extras/dev_framework/__init__.py index 6aed8910fc..2fbd8693b4 100644 --- a/bittensor/extras/dev_framework/__init__.py +++ b/bittensor/extras/dev_framework/__init__.py @@ -1,8 +1,8 @@ -from .calls import * # noqa: F401 +from .calls import * # noqa: F403 from .subnet import ( - NETUID, - TestSubnet, - ACTIVATE_SUBNET, - REGISTER_SUBNET, - REGISTER_NEURON, + NETUID as NETUID, + TestSubnet as TestSubnet, + ACTIVATE_SUBNET as ACTIVATE_SUBNET, + REGISTER_SUBNET as REGISTER_SUBNET, + REGISTER_NEURON as REGISTER_NEURON, ) diff --git a/bittensor/extras/dev_framework/calls/__init__.py b/bittensor/extras/dev_framework/calls/__init__.py index 0455d723f9..f88c0a6808 100644 --- a/bittensor/extras/dev_framework/calls/__init__.py +++ b/bittensor/extras/dev_framework/calls/__init__.py @@ -11,9 +11,9 @@ import os from bittensor import Subtensor -from bittensor.extras.dev_framework.calls.sudo_calls import * # noqa: F401 -from bittensor.extras.dev_framework.calls.non_sudo_calls import * # noqa: F401 -from bittensor.extras.dev_framework.calls.pallets import * # noqa: F401 +from bittensor.extras.dev_framework.calls.sudo_calls import * # noqa: F403 +from bittensor.extras.dev_framework.calls.non_sudo_calls import * # noqa: F403 +from bittensor.extras.dev_framework.calls.pallets import * # noqa: F403 HEADER = '''""" This file is auto-generated. Do not edit manually. diff --git a/bittensor/extras/dev_framework/calls/non_sudo_calls.py b/bittensor/extras/dev_framework/calls/non_sudo_calls.py index 62fb1166f0..537351c5c7 100644 --- a/bittensor/extras/dev_framework/calls/non_sudo_calls.py +++ b/bittensor/extras/dev_framework/calls/non_sudo_calls.py @@ -11,7 +11,7 @@ Note: Any manual changes will be overwritten the next time the generator is run. - Subtensor spec version: 376 + Subtensor spec version: 397 """ from collections import namedtuple @@ -26,10 +26,10 @@ ) # args: [delegate: AccountIdLookupOf, proxy_type: T::ProxyType, delay: BlockNumberFor] | Pallet: Proxy ADD_STAKE = namedtuple( "ADD_STAKE", ["wallet", "pallet", "hotkey", "netuid", "amount_staked"] -) # args: [hotkey: T::AccountId, netuid: NetUid, amount_staked: TaoCurrency] | Pallet: SubtensorModule +) # args: [hotkey: T::AccountId, netuid: NetUid, amount_staked: TaoBalance] | Pallet: SubtensorModule ADD_STAKE_BURN = namedtuple( "ADD_STAKE_BURN", ["wallet", "pallet", "hotkey", "netuid", "amount", "limit"] -) # args: [hotkey: T::AccountId, netuid: NetUid, amount: TaoCurrency, limit: Option] | Pallet: SubtensorModule +) # args: [hotkey: T::AccountId, netuid: NetUid, amount: TaoBalance, limit: Option] | Pallet: SubtensorModule ADD_STAKE_LIMIT = namedtuple( "ADD_STAKE_LIMIT", [ @@ -41,7 +41,7 @@ "limit_price", "allow_partial", ], -) # args: [hotkey: T::AccountId, netuid: NetUid, amount_staked: TaoCurrency, limit_price: TaoCurrency, allow_partial: bool] | Pallet: SubtensorModule +) # args: [hotkey: T::AccountId, netuid: NetUid, amount_staked: TaoBalance, limit_price: TaoBalance, allow_partial: bool] | Pallet: SubtensorModule ANNOUNCE = namedtuple( "ANNOUNCE", ["wallet", "pallet", "real", "call_hash"] ) # args: [real: AccountIdLookupOf, call_hash: CallHashOf] | Pallet: Proxy @@ -49,8 +49,8 @@ "ANNOUNCE_COLDKEY_SWAP", ["wallet", "pallet", "new_coldkey_hash"] ) # args: [new_coldkey_hash: T::Hash] | Pallet: SubtensorModule ANNOUNCE_NEXT_KEY = namedtuple( - "ANNOUNCE_NEXT_KEY", ["wallet", "pallet", "public_key"] -) # args: [public_key: BoundedVec>] | Pallet: MevShield + "ANNOUNCE_NEXT_KEY", ["wallet", "pallet", "enc_key"] +) # args: [enc_key: Option] | Pallet: MevShield APPLY_AUTHORIZED_UPGRADE = namedtuple( "APPLY_AUTHORIZED_UPGRADE", ["wallet", "pallet", "code"] ) # args: [code: Vec] | Pallet: System @@ -126,7 +126,7 @@ ) # args: [netuid: NetUid, hotkey: T::AccountId] | Pallet: SubtensorModule BURN_ALPHA = namedtuple( "BURN_ALPHA", ["wallet", "pallet", "hotkey", "amount", "netuid"] -) # args: [hotkey: T::AccountId, amount: AlphaCurrency, netuid: NetUid] | Pallet: SubtensorModule +) # args: [hotkey: T::AccountId, amount: AlphaBalance, netuid: NetUid] | Pallet: SubtensorModule CALL = namedtuple( "CALL", ["wallet", "pallet", "dest", "value", "gas_limit", "storage_deposit_limit", "data"], @@ -171,6 +171,13 @@ CLAIM_ROOT = namedtuple( "CLAIM_ROOT", ["wallet", "pallet", "subnets"] ) # args: [subnets: BTreeSet] | Pallet: SubtensorModule +CLEAR_COLDKEY_SWAP_ANNOUNCEMENT = namedtuple( + "CLEAR_COLDKEY_SWAP_ANNOUNCEMENT", + [ + "wallet", + "pallet", + ], +) # args: [] | Pallet: SubtensorModule CLEAR_IDENTITY = namedtuple( "CLEAR_IDENTITY", ["wallet", "pallet", "identified"] ) # args: [identified: T::AccountId] | Pallet: Registry @@ -423,9 +430,6 @@ KILL_STORAGE = namedtuple( "KILL_STORAGE", ["wallet", "pallet", "keys"] ) # args: [keys: Vec] | Pallet: System -MARK_DECRYPTION_FAILED = namedtuple( - "MARK_DECRYPTION_FAILED", ["wallet", "pallet", "id", "reason"] -) # args: [id: T::Hash, reason: BoundedVec>] | Pallet: MevShield MIGRATE = namedtuple( "MIGRATE", ["wallet", "pallet", "weight_limit"] ) # args: [weight_limit: Weight] | Pallet: Contracts @@ -444,7 +448,7 @@ "destination_netuid", "alpha_amount", ], -) # args: [origin_hotkey: T::AccountId, destination_hotkey: T::AccountId, origin_netuid: NetUid, destination_netuid: NetUid, alpha_amount: AlphaCurrency] | Pallet: SubtensorModule +) # args: [origin_hotkey: T::AccountId, destination_hotkey: T::AccountId, origin_netuid: NetUid, destination_netuid: NetUid, alpha_amount: AlphaBalance] | Pallet: SubtensorModule NOTE_PREIMAGE = namedtuple( "NOTE_PREIMAGE", ["wallet", "pallet", "bytes"] ) # args: [bytes: Vec] | Pallet: Preimage @@ -470,7 +474,7 @@ ) # args: [delegate: AccountIdLookupOf, real: AccountIdLookupOf, force_proxy_type: Option, call: Box<::RuntimeCall>] | Pallet: Proxy RECYCLE_ALPHA = namedtuple( "RECYCLE_ALPHA", ["wallet", "pallet", "hotkey", "amount", "netuid"] -) # args: [hotkey: T::AccountId, amount: AlphaCurrency, netuid: NetUid] | Pallet: SubtensorModule +) # args: [hotkey: T::AccountId, amount: AlphaBalance, netuid: NetUid] | Pallet: SubtensorModule REFUND = namedtuple( "REFUND", ["wallet", "pallet", "crowdloan_id"] ) # args: [crowdloan_id: CrowdloanId] | Pallet: Crowdloan @@ -490,6 +494,9 @@ REGISTER_LEASED_NETWORK = namedtuple( "REGISTER_LEASED_NETWORK", ["wallet", "pallet", "emissions_share", "end_block"] ) # args: [emissions_share: Percent, end_block: Option>] | Pallet: SubtensorModule +REGISTER_LIMIT = namedtuple( + "REGISTER_LIMIT", ["wallet", "pallet", "netuid", "hotkey", "limit_price"] +) # args: [netuid: NetUid, hotkey: T::AccountId, limit_price: u64] | Pallet: SubtensorModule REGISTER_NETWORK = namedtuple( "REGISTER_NETWORK", ["wallet", "pallet", "hotkey"] ) # args: [hotkey: T::AccountId] | Pallet: SubtensorModule @@ -536,10 +543,10 @@ ) # args: [delegate: AccountIdLookupOf, proxy_type: T::ProxyType, delay: BlockNumberFor] | Pallet: Proxy REMOVE_STAKE = namedtuple( "REMOVE_STAKE", ["wallet", "pallet", "hotkey", "netuid", "amount_unstaked"] -) # args: [hotkey: T::AccountId, netuid: NetUid, amount_unstaked: AlphaCurrency] | Pallet: SubtensorModule +) # args: [hotkey: T::AccountId, netuid: NetUid, amount_unstaked: AlphaBalance] | Pallet: SubtensorModule REMOVE_STAKE_FULL_LIMIT = namedtuple( "REMOVE_STAKE_FULL_LIMIT", ["wallet", "pallet", "hotkey", "netuid", "limit_price"] -) # args: [hotkey: T::AccountId, netuid: NetUid, limit_price: Option] | Pallet: SubtensorModule +) # args: [hotkey: T::AccountId, netuid: NetUid, limit_price: Option] | Pallet: SubtensorModule REMOVE_STAKE_LIMIT = namedtuple( "REMOVE_STAKE_LIMIT", [ @@ -551,7 +558,7 @@ "limit_price", "allow_partial", ], -) # args: [hotkey: T::AccountId, netuid: NetUid, amount_unstaked: AlphaCurrency, limit_price: TaoCurrency, allow_partial: bool] | Pallet: SubtensorModule +) # args: [hotkey: T::AccountId, netuid: NetUid, amount_unstaked: AlphaBalance, limit_price: TaoBalance, allow_partial: bool] | Pallet: SubtensorModule REPORT_EQUIVOCATION = namedtuple( "REPORT_EQUIVOCATION", ["wallet", "pallet", "equivocation_proof", "key_owner_proof"] ) # args: [equivocation_proof: Box>>, key_owner_proof: T::KeyOwnerProof] | Pallet: Grandpa @@ -639,6 +646,9 @@ SET = namedtuple( "SET", ["wallet", "pallet", "now"] ) # args: [now: T::Moment] | Pallet: Timestamp +SET_AUTO_PARENT_DELEGATION_ENABLED = namedtuple( + "SET_AUTO_PARENT_DELEGATION_ENABLED", ["wallet", "pallet", "hotkey", "enabled"] +) # args: [hotkey: T::AccountId, enabled: bool] | Pallet: SubtensorModule SET_BASE_FEE_PER_GAS = namedtuple( "SET_BASE_FEE_PER_GAS", ["wallet", "pallet", "fee"] ) # args: [fee: U256] | Pallet: BaseFee @@ -695,6 +705,12 @@ SET_KEY = namedtuple( "SET_KEY", ["wallet", "pallet", "new"] ) # args: [new: AccountIdLookupOf] | Pallet: Sudo +SET_MAX_EXTRINSIC_WEIGHT = namedtuple( + "SET_MAX_EXTRINSIC_WEIGHT", ["wallet", "pallet", "value"] +) # args: [value: u64] | Pallet: MevShield +SET_MAX_PENDING_EXTRINSICS_NUMBER = namedtuple( + "SET_MAX_PENDING_EXTRINSICS_NUMBER", ["wallet", "pallet", "value"] +) # args: [value: u32] | Pallet: MevShield SET_MAX_SPACE = namedtuple( "SET_MAX_SPACE", ["wallet", "pallet", "new_limit"] ) # args: [new_limit: u32] | Pallet: Commitments @@ -705,9 +721,15 @@ SET_OLDEST_STORED_ROUND = namedtuple( "SET_OLDEST_STORED_ROUND", ["wallet", "pallet", "oldest_round"] ) # args: [oldest_round: u64] | Pallet: Drand +SET_ON_INITIALIZE_WEIGHT = namedtuple( + "SET_ON_INITIALIZE_WEIGHT", ["wallet", "pallet", "value"] +) # args: [value: u64] | Pallet: MevShield SET_PENDING_CHILDKEY_COOLDOWN = namedtuple( "SET_PENDING_CHILDKEY_COOLDOWN", ["wallet", "pallet", "cooldown"] ) # args: [cooldown: u64] | Pallet: SubtensorModule +SET_REAL_PAYS_FEE = namedtuple( + "SET_REAL_PAYS_FEE", ["wallet", "pallet", "delegate", "pays_fee"] +) # args: [delegate: AccountIdLookupOf, pays_fee: bool] | Pallet: Proxy SET_RETRY = namedtuple( "SET_RETRY", ["wallet", "pallet", "task", "retries", "period"] ) # args: [task: TaskAddress>, retries: u8, period: BlockNumberFor] | Pallet: Scheduler @@ -720,6 +742,9 @@ SET_STORAGE = namedtuple( "SET_STORAGE", ["wallet", "pallet", "items"] ) # args: [items: Vec] | Pallet: System +SET_STORED_EXTRINSIC_LIFETIME = namedtuple( + "SET_STORED_EXTRINSIC_LIFETIME", ["wallet", "pallet", "value"] +) # args: [value: u32] | Pallet: MevShield SET_SUBNET_IDENTITY = namedtuple( "SET_SUBNET_IDENTITY", [ @@ -745,9 +770,12 @@ START_CALL = namedtuple( "START_CALL", ["wallet", "pallet", "netuid"] ) # args: [netuid: NetUid] | Pallet: SubtensorModule +STORE_ENCRYPTED = namedtuple( + "STORE_ENCRYPTED", ["wallet", "pallet", "encrypted_call"] +) # args: [encrypted_call: BoundedVec] | Pallet: MevShield SUBMIT_ENCRYPTED = namedtuple( - "SUBMIT_ENCRYPTED", ["wallet", "pallet", "commitment", "ciphertext"] -) # args: [commitment: T::Hash, ciphertext: BoundedVec>] | Pallet: MevShield + "SUBMIT_ENCRYPTED", ["wallet", "pallet", "ciphertext"] +) # args: [ciphertext: BoundedVec>] | Pallet: MevShield SUDO = namedtuple( "SUDO", ["wallet", "pallet", "call"] ) # args: [call: Box<::RuntimeCall>] | Pallet: Sudo @@ -756,13 +784,17 @@ ) # args: [new_authorities: BoundedVec<::AuthorityId, T::MaxAuthorities>] | Pallet: AdminUtils SWAP_COLDKEY = namedtuple( "SWAP_COLDKEY", ["wallet", "pallet", "old_coldkey", "new_coldkey", "swap_cost"] -) # args: [old_coldkey: T::AccountId, new_coldkey: T::AccountId, swap_cost: TaoCurrency] | Pallet: SubtensorModule +) # args: [old_coldkey: T::AccountId, new_coldkey: T::AccountId, swap_cost: TaoBalance] | Pallet: SubtensorModule SWAP_COLDKEY_ANNOUNCED = namedtuple( "SWAP_COLDKEY_ANNOUNCED", ["wallet", "pallet", "new_coldkey"] ) # args: [new_coldkey: T::AccountId] | Pallet: SubtensorModule SWAP_HOTKEY = namedtuple( "SWAP_HOTKEY", ["wallet", "pallet", "hotkey", "new_hotkey", "netuid"] ) # args: [hotkey: T::AccountId, new_hotkey: T::AccountId, netuid: Option] | Pallet: SubtensorModule +SWAP_HOTKEY_V2 = namedtuple( + "SWAP_HOTKEY_V2", + ["wallet", "pallet", "hotkey", "new_hotkey", "netuid", "keep_stake"], +) # args: [hotkey: T::AccountId, new_hotkey: T::AccountId, netuid: Option, keep_stake: bool] | Pallet: SubtensorModule SWAP_STAKE = namedtuple( "SWAP_STAKE", [ @@ -773,7 +805,7 @@ "destination_netuid", "alpha_amount", ], -) # args: [hotkey: T::AccountId, origin_netuid: NetUid, destination_netuid: NetUid, alpha_amount: AlphaCurrency] | Pallet: SubtensorModule +) # args: [hotkey: T::AccountId, origin_netuid: NetUid, destination_netuid: NetUid, alpha_amount: AlphaBalance] | Pallet: SubtensorModule SWAP_STAKE_LIMIT = namedtuple( "SWAP_STAKE_LIMIT", [ @@ -786,7 +818,7 @@ "limit_price", "allow_partial", ], -) # args: [hotkey: T::AccountId, origin_netuid: NetUid, destination_netuid: NetUid, alpha_amount: AlphaCurrency, limit_price: TaoCurrency, allow_partial: bool] | Pallet: SubtensorModule +) # args: [hotkey: T::AccountId, origin_netuid: NetUid, destination_netuid: NetUid, alpha_amount: AlphaBalance, limit_price: TaoBalance, allow_partial: bool] | Pallet: SubtensorModule TERMINATE_LEASE = namedtuple( "TERMINATE_LEASE", ["wallet", "pallet", "lease_id", "hotkey"] ) # args: [lease_id: LeaseId, hotkey: T::AccountId] | Pallet: SubtensorModule @@ -816,7 +848,7 @@ "destination_netuid", "alpha_amount", ], -) # args: [destination_coldkey: T::AccountId, hotkey: T::AccountId, origin_netuid: NetUid, destination_netuid: NetUid, alpha_amount: AlphaCurrency] | Pallet: SubtensorModule +) # args: [destination_coldkey: T::AccountId, hotkey: T::AccountId, origin_netuid: NetUid, destination_netuid: NetUid, alpha_amount: AlphaBalance] | Pallet: SubtensorModule TRY_ASSOCIATE_HOTKEY = namedtuple( "TRY_ASSOCIATE_HOTKEY", ["wallet", "pallet", "hotkey"] ) # args: [hotkey: T::AccountId] | Pallet: SubtensorModule diff --git a/bittensor/extras/dev_framework/calls/pallets.py b/bittensor/extras/dev_framework/calls/pallets.py index 72f0dc092f..1ffef1da5d 100644 --- a/bittensor/extras/dev_framework/calls/pallets.py +++ b/bittensor/extras/dev_framework/calls/pallets.py @@ -1,5 +1,5 @@ """ " -Subtensor spec version: 376 +Subtensor spec version: 397 """ System = "System" diff --git a/bittensor/extras/dev_framework/calls/sudo_calls.py b/bittensor/extras/dev_framework/calls/sudo_calls.py index 7b29a05d99..d7b5a0cdc4 100644 --- a/bittensor/extras/dev_framework/calls/sudo_calls.py +++ b/bittensor/extras/dev_framework/calls/sudo_calls.py @@ -11,7 +11,7 @@ Note: Any manual changes will be overwritten the next time the generator is run. - Subtensor spec version: 376 + Subtensor spec version: 397 """ from collections import namedtuple @@ -53,6 +53,13 @@ SUDO_SET_BONDS_RESET_ENABLED = namedtuple( "SUDO_SET_BONDS_RESET_ENABLED", ["wallet", "pallet", "sudo", "netuid", "enabled"] ) # args: [netuid: NetUid, enabled: bool] | Pallet: AdminUtils +SUDO_SET_BURN_HALF_LIFE = namedtuple( + "SUDO_SET_BURN_HALF_LIFE", ["wallet", "pallet", "sudo", "netuid", "burn_half_life"] +) # args: [netuid: NetUid, burn_half_life: u16] | Pallet: AdminUtils +SUDO_SET_BURN_INCREASE_MULT = namedtuple( + "SUDO_SET_BURN_INCREASE_MULT", + ["wallet", "pallet", "sudo", "netuid", "burn_increase_mult"], +) # args: [netuid: NetUid, burn_increase_mult: U64F64] | Pallet: AdminUtils SUDO_SET_CK_BURN = namedtuple( "SUDO_SET_CK_BURN", ["wallet", "pallet", "sudo", "burn"] ) # args: [burn: u64] | Pallet: AdminUtils @@ -114,7 +121,7 @@ ) # args: [netuid: NetUid, max_allowed_validators: u16] | Pallet: AdminUtils SUDO_SET_MAX_BURN = namedtuple( "SUDO_SET_MAX_BURN", ["wallet", "pallet", "sudo", "netuid", "max_burn"] -) # args: [netuid: NetUid, max_burn: TaoCurrency] | Pallet: AdminUtils +) # args: [netuid: NetUid, max_burn: TaoBalance] | Pallet: AdminUtils SUDO_SET_MAX_CHILDKEY_TAKE = namedtuple( "SUDO_SET_MAX_CHILDKEY_TAKE", ["wallet", "pallet", "sudo", "take"] ) # args: [take: u16] | Pallet: SubtensorModule @@ -146,7 +153,7 @@ ) # args: [netuid: NetUid, min_allowed_weights: u16] | Pallet: AdminUtils SUDO_SET_MIN_BURN = namedtuple( "SUDO_SET_MIN_BURN", ["wallet", "pallet", "sudo", "netuid", "min_burn"] -) # args: [netuid: NetUid, min_burn: TaoCurrency] | Pallet: AdminUtils +) # args: [netuid: NetUid, min_burn: TaoBalance] | Pallet: AdminUtils SUDO_SET_MIN_CHILDKEY_TAKE = namedtuple( "SUDO_SET_MIN_CHILDKEY_TAKE", ["wallet", "pallet", "sudo", "take"] ) # args: [take: u16] | Pallet: SubtensorModule @@ -164,7 +171,7 @@ ) # args: [immunity_period: u64] | Pallet: AdminUtils SUDO_SET_NETWORK_MIN_LOCK_COST = namedtuple( "SUDO_SET_NETWORK_MIN_LOCK_COST", ["wallet", "pallet", "sudo", "lock_cost"] -) # args: [lock_cost: TaoCurrency] | Pallet: AdminUtils +) # args: [lock_cost: TaoBalance] | Pallet: AdminUtils SUDO_SET_NETWORK_POW_REGISTRATION_ALLOWED = namedtuple( "SUDO_SET_NETWORK_POW_REGISTRATION_ALLOWED", ["wallet", "pallet", "sudo", "netuid", "registration_allowed"], @@ -191,7 +198,7 @@ ) # args: [netuid: NetUid, immune_neurons: u16] | Pallet: AdminUtils SUDO_SET_RAO_RECYCLED = namedtuple( "SUDO_SET_RAO_RECYCLED", ["wallet", "pallet", "sudo", "netuid", "rao_recycled"] -) # args: [netuid: NetUid, rao_recycled: TaoCurrency] | Pallet: AdminUtils +) # args: [netuid: NetUid, rao_recycled: TaoBalance] | Pallet: AdminUtils SUDO_SET_RECYCLE_OR_BURN = namedtuple( "SUDO_SET_RECYCLE_OR_BURN", ["wallet", "pallet", "sudo", "netuid", "recycle_or_burn"], @@ -253,7 +260,7 @@ ) # args: [netuid: NetUid, toggle: bool] | Pallet: AdminUtils SUDO_SET_TOTAL_ISSUANCE = namedtuple( "SUDO_SET_TOTAL_ISSUANCE", ["wallet", "pallet", "sudo", "total_issuance"] -) # args: [total_issuance: TaoCurrency] | Pallet: AdminUtils +) # args: [total_issuance: TaoBalance] | Pallet: AdminUtils SUDO_SET_TX_CHILDKEY_TAKE_RATE_LIMIT = namedtuple( "SUDO_SET_TX_CHILDKEY_TAKE_RATE_LIMIT", ["wallet", "pallet", "sudo", "tx_rate_limit"], diff --git a/bittensor/extras/dev_framework/subnet.py b/bittensor/extras/dev_framework/subnet.py index b14c530500..13b15366cc 100644 --- a/bittensor/extras/dev_framework/subnet.py +++ b/bittensor/extras/dev_framework/subnet.py @@ -10,7 +10,7 @@ from bittensor.core.types import ExtrinsicResponse from bittensor.extras import SubtensorApi from bittensor.utils.btlogging import logging -from .calls import * # noqa: F401# +from .calls import * # noqa: F403 from .utils import ( is_instance_namedtuple, split_command, diff --git a/bittensor/extras/subtensor_api/extrinsics.py b/bittensor/extras/subtensor_api/extrinsics.py index 43fafc79ad..878a638ec9 100644 --- a/bittensor/extras/subtensor_api/extrinsics.py +++ b/bittensor/extras/subtensor_api/extrinsics.py @@ -13,6 +13,7 @@ def __init__(self, subtensor: Union["_Subtensor", "_AsyncSubtensor"]): self.add_stake_burn = subtensor.add_stake_burn self.add_stake_multiple = subtensor.add_stake_multiple self.announce_coldkey_swap = subtensor.announce_coldkey_swap + self.clear_coldkey_swap_announcement = subtensor.clear_coldkey_swap_announcement self.dispute_coldkey_swap = subtensor.dispute_coldkey_swap self.burned_register = subtensor.burned_register self.claim_root = subtensor.claim_root @@ -26,6 +27,7 @@ def __init__(self, subtensor: Union["_Subtensor", "_AsyncSubtensor"]): self.move_stake = subtensor.move_stake self.refund_crowdloan = subtensor.refund_crowdloan self.register = subtensor.register + self.register_limit = subtensor.register_limit self.register_subnet = subtensor.register_subnet self.remove_liquidity = subtensor.remove_liquidity self.reveal_weights = subtensor.reveal_weights diff --git a/bittensor/extras/subtensor_api/staking.py b/bittensor/extras/subtensor_api/staking.py index 0ab2e5485b..7c6f31ae9c 100644 --- a/bittensor/extras/subtensor_api/staking.py +++ b/bittensor/extras/subtensor_api/staking.py @@ -31,6 +31,7 @@ def __init__(self, subtensor: Union["_Subtensor", "_AsyncSubtensor"]): self.get_stake_info_for_coldkeys = subtensor.get_stake_info_for_coldkeys self.get_stake_movement_fee = subtensor.get_stake_movement_fee self.get_stake_weight = subtensor.get_stake_weight + self.get_staking_hotkeys = subtensor.get_staking_hotkeys self.get_unstake_fee = subtensor.get_unstake_fee self.move_stake = subtensor.move_stake self.set_auto_stake = subtensor.set_auto_stake diff --git a/bittensor/extras/subtensor_api/subnets.py b/bittensor/extras/subtensor_api/subnets.py index 1c969fef00..37cf2ccece 100644 --- a/bittensor/extras/subtensor_api/subnets.py +++ b/bittensor/extras/subtensor_api/subnets.py @@ -48,6 +48,7 @@ def __init__(self, subtensor: Union["_Subtensor", "_AsyncSubtensor"]): self.min_allowed_weights = subtensor.min_allowed_weights self.recycle = subtensor.recycle self.register = subtensor.register + self.register_limit = subtensor.register_limit self.register_subnet = subtensor.register_subnet self.set_subnet_identity = subtensor.set_subnet_identity self.start_call = subtensor.start_call diff --git a/bittensor/utils/__init__.py b/bittensor/utils/__init__.py index 44889d4c40..374f1aa67b 100644 --- a/bittensor/utils/__init__.py +++ b/bittensor/utils/__init__.py @@ -14,7 +14,7 @@ from bittensor_wallet import Keypair from bittensor_wallet.errors import KeyFileError, PasswordError from bittensor_wallet.utils import SS58_FORMAT -from scalecodec import ( +from scalecodec.utils.ss58 import ( ss58_decode, ss58_encode, is_valid_ss58_address as _is_valid_ss58_address, @@ -27,8 +27,7 @@ if TYPE_CHECKING: from bittensor_wallet import Wallet - from bittensor.core.types import ExtrinsicResponse - from bittensor.utils.balance import Balance + from bittensor.core.types import ExtrinsicResponse, NeuronCertificateResponse # keep save from import analyzer as obvious aliases hex_to_ss58 = ss58_encode @@ -94,32 +93,28 @@ def get_netuid_and_mechid_by_storage_index(storage_index: int) -> tuple[int, int class Certificate(str): - def __new__(cls, data: Union[str, dict]): + def __new__(cls, data: "str | NeuronCertificateResponse"): if isinstance(data, dict): - tuple_ascii = data["public_key"][0] - string = chr(data["algorithm"]) + "".join(chr(i) for i in tuple_ascii) + pubkey: str = data["public_key"] + string = chr(data["algorithm"]) + pubkey else: string = data return str.__new__(cls, string) -def decode_hex_identity_dict(info_dictionary: dict[str, Any]) -> dict[str, Any]: +def decode_hex_identity_dict(info_dictionary: dict[str, dict | str]) -> dict[str, Any]: """Decodes a dictionary of hexadecimal identities.""" - decoded_info = {} - for k, v in info_dictionary.items(): - if isinstance(v, dict): - item = next(iter(v.values())) - else: - item = v - if isinstance(item, tuple): - try: - decoded_info[k] = bytes(item).decode() - except UnicodeDecodeError: - print(f"Could not decode: {k}: {item}") + for key, value in info_dictionary.items(): + if isinstance(value, dict): + item = list(value.values())[0] + else: + item = value + if isinstance(item, str) and item.startswith("0x"): + info_dictionary[key] = hex_to_bytes(item.removeprefix("0x")).decode() else: - decoded_info[k] = item - return decoded_info + info_dictionary[key] = item + return info_dictionary def ss58_to_vec_u8(ss58_address: str) -> list[int]: diff --git a/bittensor/utils/balance.py b/bittensor/utils/balance.py index 670174f48e..393481b42f 100644 --- a/bittensor/utils/balance.py +++ b/bittensor/utils/balance.py @@ -1,7 +1,9 @@ -from typing import Optional, TypedDict, Union +from typing import Optional, Union -from scalecodec import ScaleType -from async_substrate_interface.types import ScaleObj +from scalecodec.utils.math import ( + fixed_to_float as fixed_to_float, + FixedPoint as FixedPoint, +) from bittensor.core import settings from bittensor.core.errors import BalanceTypeError, BalanceUnitMismatchError @@ -363,37 +365,6 @@ def set_unit(self, netuid: int): return self -class FixedPoint(TypedDict): - """ - Represents a fixed point ``U64F64`` number. - Where ``bits`` is a U128 representation of the fixed point number. - - This matches the type of the Alpha shares. - """ - - bits: int - - -def fixed_to_float( - fixed: FixedPoint | ScaleType | ScaleObj, frac_bits: int = 64, total_bits: int = 128 -) -> float: - """Converts a fixed-point value (e.g., U64F64) into a floating-point number.""" - # By default, this is a U64F64 - # which is 64 bits of integer and 64 bits of fractional - data: int = ( - fb.value if isinstance((fb := fixed["bits"]), (ScaleType, ScaleObj)) else fb - ) - - # Logical and to get the fractional part; remaining is the integer part - fractional_part = data & (2**frac_bits - 1) - # Shift to get the integer part from the remaining bits - integer_part = data >> (total_bits - frac_bits) - - frac_float = fractional_part / (2**frac_bits) - - return integer_part + frac_float - - # lowercase is added for backwards compatibility to not break API units = UNITS = [ chr( diff --git a/bittensor/utils/btlogging/defines.py b/bittensor/utils/btlogging/defines.py index f71a99e702..526e5444b3 100644 --- a/bittensor/utils/btlogging/defines.py +++ b/bittensor/utils/btlogging/defines.py @@ -2,7 +2,7 @@ BASE_LOG_FORMAT = "%(asctime)s | %(levelname)s | %(message)s" TRACE_LOG_FORMAT = ( - f"%(asctime)s | %(levelname)s | %(name)s:%(filename)s:%(lineno)s | %(message)s" + "%(asctime)s | %(levelname)s | %(name)s:%(filename)s:%(lineno)s | %(message)s" ) DATE_FORMAT = "%Y-%m-%d %H:%M:%S" BITTENSOR_LOGGER_NAME = "bittensor" diff --git a/bittensor/utils/btlogging/loggingmachine.py b/bittensor/utils/btlogging/loggingmachine.py index 0feacf4f93..1575bd745c 100644 --- a/bittensor/utils/btlogging/loggingmachine.py +++ b/bittensor/utils/btlogging/loggingmachine.py @@ -402,6 +402,7 @@ def after_transition(self, event, state): # Default Logging def before_enable_default(self): + self.set_trace(False) """Logs status before enable Default.""" self._logger.info("Enabling default logging (Warning level)") self._logger.setLevel(stdlogging.WARNING) diff --git a/bittensor/utils/liquidity.py b/bittensor/utils/liquidity.py index 16b778449e..3cc253c01d 100644 --- a/bittensor/utils/liquidity.py +++ b/bittensor/utils/liquidity.py @@ -5,9 +5,9 @@ """ import math -from typing import Any from dataclasses import dataclass +from bittensor.core.types import PositionResponse from bittensor.utils import ChainFeatureDisabledWarning, deprecated_message from bittensor.utils.balance import Balance, fixed_to_float @@ -155,7 +155,7 @@ def get_fees_in_range( def calculate_fees( - position: dict[str, Any], + position: PositionResponse, global_fees_tao: float, global_fees_alpha: float, tao_fees_below_low: float, diff --git a/bittensor/utils/mock/__init__.py b/bittensor/utils/mock/__init__.py index 04893c78a3..fc4d22dacf 100644 --- a/bittensor/utils/mock/__init__.py +++ b/bittensor/utils/mock/__init__.py @@ -1 +1 @@ -from .subtensor_mock import MockSubtensor +from .subtensor_mock import MockSubtensor as MockSubtensor diff --git a/bittensor/utils/mock/subtensor_mock.py b/bittensor/utils/mock/subtensor_mock.py index 95c6d4af54..832d24a2c0 100644 --- a/bittensor/utils/mock/subtensor_mock.py +++ b/bittensor/utils/mock/subtensor_mock.py @@ -871,9 +871,6 @@ def _neuron_subnet_exists( dividends = self._get_most_recent_storage( subtensor_state["Dividends"][netuid][uid], block ) - pruning_score = self._get_most_recent_storage( - subtensor_state["PruningScores"][netuid][uid], block - ) last_update = self._get_most_recent_storage( subtensor_state["LastUpdate"][netuid][uid], block ) diff --git a/bittensor/utils/registration/__init__.py b/bittensor/utils/registration/__init__.py index ddcc7a1248..3ac18def71 100644 --- a/bittensor/utils/registration/__init__.py +++ b/bittensor/utils/registration/__init__.py @@ -1,21 +1,15 @@ -from bittensor.utils.registration.pow import ( - create_pow, +from bittensor.utils.registration.torch_utils import ( legacy_torch_api_compat, log_no_torch_error, torch, use_torch, LazyLoadedTorch, - POWSolution, ) -from bittensor.utils.registration.async_pow import create_pow_async __all__ = [ - "create_pow", - "create_pow_async", "legacy_torch_api_compat", "log_no_torch_error", "torch", "use_torch", "LazyLoadedTorch", - "POWSolution", ] diff --git a/bittensor/utils/registration/async_pow.py b/bittensor/utils/registration/async_pow.py deleted file mode 100644 index aa45cf502d..0000000000 --- a/bittensor/utils/registration/async_pow.py +++ /dev/null @@ -1,538 +0,0 @@ -"""This module provides async utilities for solving Proof-of-Work (PoW) challenges in Bittensor network.""" - -import math -import time -from multiprocessing import Event, Lock, Array, Value, Queue -from queue import Empty -from typing import Callable, Union, Optional, TYPE_CHECKING - -from bittensor.core.errors import SubstrateRequestException -from bittensor.utils.btlogging import logging -from bittensor.utils.registration.pow import ( - get_cpu_count, - update_curr_block, - terminate_workers_and_wait_for_exit, - CUDASolver, - torch, - RegistrationStatistics, - RegistrationStatisticsLogger, - Solver, - UsingSpawnStartMethod, -) - -if TYPE_CHECKING: - from bittensor.core.async_subtensor import AsyncSubtensor - from bittensor_wallet import Wallet - from bittensor.utils.registration import POWSolution - - -async def _get_block_with_retry( - subtensor: "AsyncSubtensor", netuid: int -) -> tuple[int, int, str]: - """ - Gets the current block number, difficulty, and block hash from the substrate node. - - Parameters: - subtensor: The subtensor object to use to get the block number, difficulty, and block hash. - netuid: The netuid of the network to get the block number, difficulty, and block hash from. - - Returns: - The current block number, difficulty of the subnet, block hash - - Raises: - Exception: If the block hash is None. - ValueError: If the difficulty is None. - """ - block = await subtensor.substrate.get_block() - block_hash = block["header"]["hash"] - block_number = block["header"]["number"] - try: - difficulty = ( - 1_000_000 - if netuid == -1 - else int( - await subtensor.get_hyperparameter( - param_name="Difficulty", netuid=netuid, block_hash=block_hash - ) - ) - ) - except TypeError: - raise ValueError("Chain error. Difficulty is None") - except SubstrateRequestException: - raise Exception( - "Network error. Could not connect to substrate to get block hash" - ) - return block_number, difficulty, block_hash - - -async def _check_for_newest_block_and_update( - subtensor: "AsyncSubtensor", - netuid: int, - old_block_number: int, - hotkey_bytes: bytes, - curr_diff: Array, - curr_block: Array, - curr_block_num: Value, - update_curr_block_: "Callable", - check_block: Lock, - solvers: list[Solver], - curr_stats: "RegistrationStatistics", -) -> int: - """ - Check for the newest block and update block-related information and states across solvers if a new block is detected. - - Parameters: - subtensor: The subtensor instance interface. - netuid: The network UID for the blockchain. - old_block_number: The previously known block number. - hotkey_bytes: The bytes representation of the hotkey. - curr_diff: The current difficulty level. - curr_block: The current block information. - curr_block_num: The current block number. - update_curr_block_: Function to update current block information. - check_block: Lock object for synchronizing block checking. - solvers: List of solvers to notify of new blocks. - curr_stats: Current registration statistics to update. - - Returns: - int: The updated block number which is the same as the new block - number if it was detected, otherwise the old block number. - """ - block_number = await subtensor.substrate.get_block_number(None) - if block_number != old_block_number: - old_block_number = block_number - # update block information - block_number, difficulty, block_hash = await _get_block_with_retry( - subtensor=subtensor, netuid=netuid - ) - block_bytes = bytes.fromhex(block_hash[2:]) - - update_curr_block_( - curr_diff, - curr_block, - curr_block_num, - block_number, - block_bytes, - difficulty, - hotkey_bytes, - check_block, - ) - # Set new block events for each solver - - for worker in solvers: - worker.newBlockEvent.set() - - # update stats - curr_stats.block_number = block_number - curr_stats.block_hash = block_hash - curr_stats.difficulty = difficulty - - return old_block_number - - -async def _block_solver( - subtensor: "AsyncSubtensor", - wallet: "Wallet", - num_processes: int, - netuid: int, - dev_id: list[int], - tpb: int, - update_interval: int, - curr_block, - curr_block_num, - curr_diff, - n_samples, - alpha_, - output_in_place, - log_verbose, - cuda: bool, -): - """Shared code used by the Solvers to solve the POW solution.""" - limit = int(math.pow(2, 256)) - 1 - - if cuda: - num_processes = len(dev_id) - - # Establish communication queues - # See the _Solver class for more information on the queues. - stop_event = Event() - stop_event.clear() - - solution_queue = Queue() - finished_queues = [Queue() for _ in range(num_processes)] - check_block = Lock() - - hotkey_bytes = ( - wallet.coldkeypub.public_key if netuid == -1 else wallet.hotkey.public_key - ) - - if cuda: - # Create a worker per CUDA device - solvers = [ - CUDASolver( - i, - num_processes, - update_interval, - finished_queues[i], - solution_queue, - stop_event, - curr_block, - curr_block_num, - curr_diff, - check_block, - limit, - dev_id[i], - tpb, - ) - for i in range(num_processes) - ] - else: - # Start consumers - solvers = [ - Solver( - i, - num_processes, - update_interval, - finished_queues[i], - solution_queue, - stop_event, - curr_block, - curr_block_num, - curr_diff, - check_block, - limit, - ) - for i in range(num_processes) - ] - - # Get first block - block_number, difficulty, block_hash = await _get_block_with_retry( - subtensor=subtensor, netuid=netuid - ) - - block_bytes = bytes.fromhex(block_hash[2:]) - old_block_number = block_number - # Set to current block - update_curr_block( - curr_diff, - curr_block, - curr_block_num, - block_number, - block_bytes, - difficulty, - hotkey_bytes, - check_block, - ) - - # Set new block events for each solver to start at the initial block - for worker in solvers: - worker.newBlockEvent.set() - - for worker in solvers: - worker.start() # start the solver processes - - start_time = time.time() # time that the registration started - time_last = start_time # time that the last work blocks completed - - curr_stats = RegistrationStatistics( - time_spent_total=0.0, - time_average=0.0, - rounds_total=0, - time_spent=0.0, - hash_rate_perpetual=0.0, - hash_rate=0.0, - difficulty=difficulty, - block_number=block_number, - block_hash=block_hash, - ) - - start_time_perpetual = time.time() - - logger = RegistrationStatisticsLogger(output_in_place=output_in_place) - logger.start() - - solution = None - - hash_rates = [0] * n_samples # The last n true hash_rates - weights = [alpha_**i for i in range(n_samples)] # weights decay by alpha - - timeout = 0.15 if cuda else 0.15 - while netuid == -1 or not await subtensor.is_hotkey_registered( - wallet.hotkey.ss58_address, netuid - ): - # Wait until a solver finds a solution - try: - solution = solution_queue.get(block=True, timeout=timeout) - if solution is not None: - break - except Empty: - # No solution found, try again - pass - - # check for new block - old_block_number = await _check_for_newest_block_and_update( - subtensor=subtensor, - netuid=netuid, - hotkey_bytes=hotkey_bytes, - old_block_number=old_block_number, - curr_diff=curr_diff, - curr_block=curr_block, - curr_block_num=curr_block_num, - curr_stats=curr_stats, - update_curr_block_=update_curr_block, - check_block=check_block, - solvers=solvers, - ) - - num_time = 0 - for finished_queue in finished_queues: - try: - finished_queue.get(timeout=0.1) - num_time += 1 - - except Empty: - continue - - time_now = time.time() # get current time - time_since_last = time_now - time_last # get time since last work block(s) - if num_time > 0 and time_since_last > 0.0: - # create EWMA of the hash_rate to make measure more robust - - if cuda: - hash_rate_ = (num_time * tpb * update_interval) / time_since_last - else: - hash_rate_ = (num_time * update_interval) / time_since_last - hash_rates.append(hash_rate_) - hash_rates.pop(0) # remove the 0th data point - curr_stats.hash_rate = sum( - [hash_rates[i] * weights[i] for i in range(n_samples)] - ) / (sum(weights)) - - # update time last to now - time_last = time_now - - curr_stats.time_average = ( - curr_stats.time_average * curr_stats.rounds_total - + curr_stats.time_spent - ) / (curr_stats.rounds_total + num_time) - curr_stats.rounds_total += num_time - - # Update stats - curr_stats.time_spent = time_since_last - new_time_spent_total = time_now - start_time_perpetual - if cuda: - curr_stats.hash_rate_perpetual = ( - curr_stats.rounds_total * (tpb * update_interval) - ) / new_time_spent_total - else: - curr_stats.hash_rate_perpetual = ( - curr_stats.rounds_total * update_interval - ) / new_time_spent_total - curr_stats.time_spent_total = new_time_spent_total - - # Update the logger - logger.update(curr_stats, verbose=log_verbose) - - # exited while, solution contains the nonce or wallet is registered - stop_event.set() # stop all other processes - logger.stop() - - # terminate and wait for all solvers to exit - terminate_workers_and_wait_for_exit(solvers) - - return solution - - -async def _solve_for_difficulty_fast_cuda( - subtensor: "AsyncSubtensor", - wallet: "Wallet", - netuid: int, - output_in_place: bool = True, - update_interval: int = 50_000, - tpb: int = 512, - dev_id: Union[list[int], int] = 0, - n_samples: int = 10, - alpha_: float = 0.80, - log_verbose: bool = False, -) -> Optional["POWSolution"]: - """ - Solves the registration fast using CUDA - - Parameters: - subtensor: The subtensor object to use to get the block number, difficulty, and block hash. - wallet: The wallet to register - netuid: The netuid of the subnet to register to. - output_in_place: If true, prints the output in place, otherwise prints to new lines - update_interval: The number of nonces to try before checking for more blocks - tpb: The number of threads per block. CUDA param that should match the GPU capability - dev_id: The CUDA device IDs to execute the registration on, either a single device or a list of devices - n_samples: The number of samples of the hash_rate to keep for the EWMA - alpha_: The alpha for the EWMA for the hash_rate calculation - log_verbose: If true, prints more verbose logging of the registration metrics. - - Note: - The hash rate is calculated as an exponentially weighted moving average in order to make the measure more robust. - """ - if isinstance(dev_id, int): - dev_id = [dev_id] - elif dev_id is None: - dev_id = [0] - - num_processes = min(1, get_cpu_count()) - - if update_interval is None: - update_interval = 50_000 - - if not torch.cuda.is_available(): - raise Exception("CUDA not available") - - # Set mp start to use spawn so CUDA doesn't complain - with UsingSpawnStartMethod(force=True): - curr_block, curr_block_num, curr_diff = CUDASolver.create_shared_memory() - - solution = await _block_solver( - subtensor=subtensor, - wallet=wallet, - num_processes=num_processes, - netuid=netuid, - dev_id=dev_id, - tpb=tpb, - update_interval=update_interval, - curr_block=curr_block, - curr_block_num=curr_block_num, - curr_diff=curr_diff, - n_samples=n_samples, - alpha_=alpha_, - output_in_place=output_in_place, - log_verbose=log_verbose, - cuda=True, - ) - - return solution - - -async def _solve_for_difficulty_fast( - subtensor: "AsyncSubtensor", - wallet: "Wallet", - netuid: int, - output_in_place: bool = True, - num_processes: Optional[int] = None, - update_interval: Optional[int] = None, - n_samples: int = 10, - alpha_: float = 0.80, - log_verbose: bool = False, -) -> Optional["POWSolution"]: - """ - Solves the POW for registration using multiprocessing. - - Parameters: - subtensor: The subtensor object to use to get the block number, difficulty, and block hash. - wallet: wallet to use for registration. - netuid: The netuid of the subnet to register to. - output_in_place: If true, prints the status in place. Otherwise, prints the status on a new line. - num_processes: Number of processes to use. - update_interval: Number of nonces to solve before updating block information. - n_samples: The number of samples of the hash_rate to keep for the EWMA - alpha_: The alpha for the EWMA for the hash_rate calculation - log_verbose: If true, prints more verbose logging of the registration metrics. - - Notes: - The hash rate is calculated as an exponentially weighted moving average in order to make the measure more robust. - We can also modify the update interval to do smaller blocks of work, while still updating the block information - after a different number of nonces, to increase the transparency of the process while still keeping the speed. - """ - if not num_processes: - # get the number of allowed processes for this process - num_processes = min(1, get_cpu_count()) - - if update_interval is None: - update_interval = 50_000 - - curr_block, curr_block_num, curr_diff = Solver.create_shared_memory() - - solution = await _block_solver( - subtensor=subtensor, - wallet=wallet, - num_processes=num_processes, - netuid=netuid, - dev_id=None, - tpb=None, - update_interval=update_interval, - curr_block=curr_block, - curr_block_num=curr_block_num, - curr_diff=curr_diff, - n_samples=n_samples, - alpha_=alpha_, - output_in_place=output_in_place, - log_verbose=log_verbose, - cuda=False, - ) - - return solution - - -async def create_pow_async( - subtensor: "AsyncSubtensor", - wallet: "Wallet", - netuid: int, - output_in_place: bool = True, - cuda: bool = False, - dev_id: Union[list[int], int] = 0, - tpb: int = 256, - num_processes: int = None, - update_interval: int = None, - log_verbose: bool = False, -) -> "POWSolution": - """ - Creates a proof of work for the given subtensor and wallet. - - Parameters: - subtensor: The subtensor instance. - wallet: The wallet to create a proof of work for. - netuid: The netuid for the subnet to create a proof of work for. - output_in_place: If true, prints the progress of the proof of work to the console in-place. Meaning the progress - is printed on the same lines. - cuda: If true, uses CUDA to solve the proof of work. - dev_id: The CUDA device id(s) to use. If cuda is true and dev_id is a list, then multiple CUDA devices will be - used to solve the proof of work. - tpb: The number of threads per block to use when solving the proof of work. Should be a multiple of 32. - num_processes: The number of processes to use when solving the proof of work. If None, then the number of - processes is equal to the number of CPU cores. - update_interval: The number of nonces to run before checking for a new block. - log_verbose: If true, prints the progress of the proof of work more verbosely. - - Returns: - The proof of work solution or None if the wallet is already registered or there is a different error. - - Raises: - ValueError: If the subnet does not exist. - """ - if netuid != -1: - if not await subtensor.subnet_exists(netuid=netuid): - raise ValueError(f"Subnet {netuid} does not exist") - solution: Optional[POWSolution] - if cuda: - logging.debug("Solve difficulty with CUDA.") - solution = await _solve_for_difficulty_fast_cuda( - subtensor=subtensor, - wallet=wallet, - netuid=netuid, - output_in_place=output_in_place, - dev_id=dev_id, - tpb=tpb, - update_interval=update_interval, - log_verbose=log_verbose, - ) - else: - logging.debug("Solve difficulty.") - solution = await _solve_for_difficulty_fast( - subtensor=subtensor, - wallet=wallet, - netuid=netuid, - output_in_place=output_in_place, - num_processes=num_processes, - update_interval=update_interval, - log_verbose=log_verbose, - ) - - return solution diff --git a/bittensor/utils/registration/pow.py b/bittensor/utils/registration/pow.py deleted file mode 100644 index 1fa4c23ee8..0000000000 --- a/bittensor/utils/registration/pow.py +++ /dev/null @@ -1,1181 +0,0 @@ -"""This module provides utilities for solving Proof-of-Work (PoW) challenges in Bittensor network.""" - -import binascii -import functools -import hashlib -import math -import multiprocessing as mp -import os -import random -import subprocess -import time -from dataclasses import dataclass -from datetime import timedelta -from multiprocessing.queues import Queue as QueueType -from queue import Empty, Full -from typing import Callable, Optional, Union, TYPE_CHECKING - -import numpy -from Crypto.Hash import keccak - -from bittensor.utils.btlogging import logging -from bittensor.utils.formatting import get_human_readable, millify -from bittensor.utils.registration.register_cuda import solve_cuda - - -def use_torch() -> bool: - """Force the use of torch over numpy for certain operations.""" - return True if os.getenv("USE_TORCH") == "1" else False - - -def legacy_torch_api_compat(func): - """ - Convert function operating on numpy Input&Output to legacy torch Input&Output API if `use_torch()` is True. - - Parameters: - func: Function with numpy Input/Output to be decorated. - - Returns: - decorated: Decorated function. - """ - - @functools.wraps(func) - def decorated(*args, **kwargs): - if use_torch(): - # if argument is a Torch tensor, convert it to numpy - args = [ - arg.cpu().numpy() if isinstance(arg, torch.Tensor) else arg - for arg in args - ] - kwargs = { - key: value.cpu().numpy() if isinstance(value, torch.Tensor) else value - for key, value in kwargs.items() - } - ret = func(*args, **kwargs) - if use_torch(): - # if return value is a numpy array, convert it to Torch tensor - if isinstance(ret, numpy.ndarray): - ret = torch.from_numpy(ret) - return ret - - return decorated - - -@functools.cache -def _get_real_torch(): - try: - import torch as _real_torch - except ImportError: - _real_torch = None - return _real_torch - - -def log_no_torch_error(): - logging.error( - "This command requires torch. You can install torch for bittensor" - ' with `pip install bittensor[torch]` or `pip install ".[torch]"`' - " if installing from source, and then run the command with USE_TORCH=1 {command}" - ) - - -class LazyLoadedTorch: - """A lazy-loading proxy for the torch module.""" - - def __bool__(self): - return bool(_get_real_torch()) - - def __getattr__(self, name): - if real_torch := _get_real_torch(): - return getattr(real_torch, name) - else: - log_no_torch_error() - raise ImportError("torch not installed") - - -if TYPE_CHECKING: - import torch - from bittensor.core.subtensor import Subtensor - from bittensor.core.async_subtensor import AsyncSubtensor - from bittensor_wallet import Wallet -else: - torch = LazyLoadedTorch() - - -def _hex_bytes_to_u8_list(hex_bytes: bytes) -> list[int]: - """ """ - return [int(hex_bytes[i : i + 2], 16) for i in range(0, len(hex_bytes), 2)] - - -def _create_seal_hash(block_and_hotkey_hash_bytes: bytes, nonce: int) -> bytes: - """ - Create a cryptographic seal hash from the given block and hotkey hash bytes and nonce. - - This function generates a seal hash by combining the given block and hotkey hash bytes with a nonce. - It first converts the nonce to a byte representation, then concatenates it with the first 64 hex characters of the - block and hotkey hash bytes. The result is then hashed using SHA-256 followed by the Keccak-256 algorithm to produce - the final seal hash. - - Parameters: - block_and_hotkey_hash_bytes: The combined hash bytes of the block and hotkey. - nonce: The nonce value used for hashing. - - Returns: - The resulting seal hash. - """ - nonce_bytes = binascii.hexlify(nonce.to_bytes(8, "little")) - pre_seal = nonce_bytes + binascii.hexlify(block_and_hotkey_hash_bytes)[:64] - seal_sh256 = hashlib.sha256(bytearray(_hex_bytes_to_u8_list(pre_seal))).digest() - kec = keccak.new(digest_bits=256) - seal = kec.update(seal_sh256).digest() - return seal - - -def _seal_meets_difficulty(seal: bytes, difficulty: int, limit: int) -> bool: - """Determines if a seal meets the specified difficulty.""" - seal_number = int.from_bytes(seal, "big") - product = seal_number * difficulty - return product < limit - - -@dataclass -class POWSolution: - """A solution to the registration PoW problem.""" - - nonce: int - block_number: int - difficulty: int - seal: bytes - - def is_stale(self, subtensor: "Subtensor") -> bool: - """ - Synchronous implementation. Returns True if the POW is stale. - - This means the block the POW is solved for is within 3 blocks of the current block. - """ - return self.block_number < subtensor.get_current_block() - 3 - - async def is_stale_async(self, subtensor: "AsyncSubtensor") -> bool: - """ - Asynchronous implementation. Returns True if the POW is stale. - - This means the block the POW is solved for is within 3 blocks of the current block. - """ - current_block = await subtensor.substrate.get_block_number(None) - return self.block_number < current_block - 3 - - -class UsingSpawnStartMethod: - def __init__(self, force: bool = False): - self._old_start_method = None - self._force = force - - def __enter__(self): - self._old_start_method = mp.get_start_method(allow_none=True) - if self._old_start_method is None: - self._old_start_method = "spawn" # default to spawn - - mp.set_start_method("spawn", force=self._force) - - def __exit__(self, *args): - # restore the old start method - mp.set_start_method(self._old_start_method, force=True) - - -class _SolverBase(mp.Process): - """ - A process that solves the registration PoW problem. - - Parameters: - proc_num: The number of the process being created. - num_proc: The total number of processes running. - update_interval: The number of nonces to try to solve before checking for a new block. - finished_queue: The queue to put the process number when a process finishes each update_interval. Used for - calculating the average time per update_interval across all processes. - solution_queue: The queue to put the solution the process has found during the pow solve. - stopEvent: The event to set by the main process when all the solver processes should stop. The solver process - will check for the event after each update_interval. The solver process will stop when the event is set. - Used to stop the solver processes when a solution is found. - curr_block: The array containing this process's current block hash. The main process will set the array to the - new block hash when a new block is finalized in the network. The solver process will get the new block hash - from this array when newBlockEvent is set. - curr_block_num: The value containing this process's current block number. The main process will set the value to - the new block number when a new block is finalized in the network. The solver process will get the new block - number from this value when newBlockEvent is set. - curr_diff: The array containing this process's current difficulty. The main process will set the array to the - new difficulty when a new block is finalized in the network. The solver process will get the new difficulty - from this array when newBlockEvent is set. - check_block: The lock to prevent this process from getting the new block data while the main process is updating - the data. - limit: The limit of the pow solve for a valid solution. - """ - - proc_num: int - num_proc: int - update_interval: int - finished_queue: "mp.Queue" - solution_queue: "mp.Queue" - # newBlockEvent: "mp.Event" - newBlockEvent: "mp.Event" - stopEvent: "mp.Event" - hotkey_bytes: bytes - curr_block: "mp.Array" - curr_block_num: "mp.Value" - curr_diff: "mp.Array" - check_block: "mp.Lock" - limit: int - - def __init__( - self, - proc_num, - num_proc, - update_interval, - finished_queue, - solution_queue, - stopEvent, - curr_block, - curr_block_num, - curr_diff, - check_block, - limit, - ): - mp.Process.__init__(self, daemon=True) - self.proc_num = proc_num - self.num_proc = num_proc - self.update_interval = update_interval - self.finished_queue = finished_queue - self.solution_queue = solution_queue - self.newBlockEvent = mp.Event() - self.newBlockEvent.clear() - self.curr_block = curr_block - self.curr_block_num = curr_block_num - self.curr_diff = curr_diff - self.check_block = check_block - self.stopEvent = stopEvent - self.limit = limit - - def run(self): - raise NotImplementedError("_SolverBase is an abstract class") - - @staticmethod - def create_shared_memory() -> tuple["mp.Array", "mp.Value", "mp.Array"]: - """Creates shared memory for the solver processes to use.""" - curr_block = mp.Array("h", 32, lock=True) # byte array - curr_block_num = mp.Value("i", 0, lock=True) # int - curr_diff = mp.Array("Q", [0, 0], lock=True) # [high, low] - - return curr_block, curr_block_num, curr_diff - - -class Solver(_SolverBase): - def run(self): - block_number: int - block_and_hotkey_hash_bytes: bytes - block_difficulty: int - nonce_limit = int(math.pow(2, 64)) - 1 - - # Start at random nonce - nonce_start = random.randint(0, nonce_limit) - nonce_end = nonce_start + self.update_interval - while not self.stopEvent.is_set(): - if self.newBlockEvent.is_set(): - with self.check_block: - block_number = self.curr_block_num.value - block_and_hotkey_hash_bytes = bytes(self.curr_block) - block_difficulty = _registration_diff_unpack(self.curr_diff) - - self.newBlockEvent.clear() - - # Do a block of nonces - solution = _solve_for_nonce_block( - nonce_start, - nonce_end, - block_and_hotkey_hash_bytes, - block_difficulty, - self.limit, - block_number, - ) - if solution is not None: - self.solution_queue.put(solution) - - try: - # Send time - self.finished_queue.put_nowait(self.proc_num) - except Full: - pass - - nonce_start = random.randint(0, nonce_limit) - nonce_start = nonce_start % nonce_limit - nonce_end = nonce_start + self.update_interval - - -class CUDASolver(_SolverBase): - dev_id: int - tpb: int - - def __init__( - self, - proc_num, - num_proc, - update_interval, - finished_queue, - solution_queue, - stopEvent, - curr_block, - curr_block_num, - curr_diff, - check_block, - limit, - dev_id: int, - tpb: int, - ): - super().__init__( - proc_num, - num_proc, - update_interval, - finished_queue, - solution_queue, - stopEvent, - curr_block, - curr_block_num, - curr_diff, - check_block, - limit, - ) - self.dev_id = dev_id - self.tpb = tpb - - def run(self): - block_number: int = 0 # dummy value - block_and_hotkey_hash_bytes: bytes = b"0" * 32 # dummy value - block_difficulty: int = int(math.pow(2, 64)) - 1 # dummy value - nonce_limit = int(math.pow(2, 64)) - 1 # U64MAX - - # Start at random nonce - nonce_start = random.randint(0, nonce_limit) - while not self.stopEvent.is_set(): - if self.newBlockEvent.is_set(): - with self.check_block: - block_number = self.curr_block_num.value - block_and_hotkey_hash_bytes = bytes(self.curr_block) - block_difficulty = _registration_diff_unpack(self.curr_diff) - - self.newBlockEvent.clear() - - # Do a block of nonces - solution = _solve_for_nonce_block_cuda( - nonce_start, - self.update_interval, - block_and_hotkey_hash_bytes, - block_difficulty, - self.limit, - block_number, - self.dev_id, - self.tpb, - ) - if solution is not None: - self.solution_queue.put(solution) - - try: - # Signal that a nonce_block was finished using queue - # send our proc_num - self.finished_queue.put(self.proc_num) - except Full: - pass - - # increase nonce by number of nonces processed - nonce_start += self.update_interval * self.tpb - nonce_start = nonce_start % nonce_limit - - -def _solve_for_nonce_block_cuda( - nonce_start: int, - update_interval: int, - block_and_hotkey_hash_bytes: bytes, - difficulty: int, - limit: int, - block_number: int, - dev_id: int, - tpb: int, -) -> Optional["POWSolution"]: - """Tries to solve the POW on a CUDA device for a block of nonces (nonce_start, nonce_start + update_interval * tpb""" - solution, seal = solve_cuda( - nonce_start, - update_interval, - tpb, - block_and_hotkey_hash_bytes, - difficulty, - limit, - dev_id, - ) - - if solution != -1: - # Check if solution is valid (i.e., not -1) - return POWSolution(solution, block_number, difficulty, seal) - - return None - - -def _solve_for_nonce_block( - nonce_start: int, - nonce_end: int, - block_and_hotkey_hash_bytes: bytes, - difficulty: int, - limit: int, - block_number: int, -) -> Optional["POWSolution"]: - """Tries to solve the POW for a block of nonces (nonce_start, nonce_end)""" - for nonce in range(nonce_start, nonce_end): - # Create seal. - seal = _create_seal_hash(block_and_hotkey_hash_bytes, nonce) - - # Check if seal meets difficulty - if _seal_meets_difficulty(seal, difficulty, limit): - # Found a solution, save it. - return POWSolution(nonce, block_number, difficulty, seal) - - return None - - -def _registration_diff_unpack(packed_diff: "mp.Array") -> int: - """Unpacks the packed two 32-bit integers into one 64-bit integer. Little endian.""" - return int(packed_diff[0] << 32 | packed_diff[1]) - - -def _registration_diff_pack(diff: int, packed_diff: "mp.Array"): - """Packs the difficulty into two 32-bit integers. Little endian.""" - packed_diff[0] = diff >> 32 - packed_diff[1] = diff & 0xFFFFFFFF # low 32 bits - - -def _hash_block_with_hotkey(block_bytes: bytes, hotkey_bytes: bytes) -> bytes: - """Hashes the block with the hotkey using Keccak-256 to get 32 bytes""" - kec = keccak.new(digest_bits=256) - kec = kec.update(bytearray(block_bytes + hotkey_bytes)) - block_and_hotkey_hash_bytes = kec.digest() - return block_and_hotkey_hash_bytes - - -def update_curr_block( - curr_diff: "mp.Array", - curr_block: "mp.Array", - curr_block_num: "mp.Value", - block_number: int, - block_bytes: bytes, - diff: int, - hotkey_bytes: bytes, - lock: "mp.Lock", -): - """ - Update the current block data with the provided block information and difficulty. - - This function updates the current block and its difficulty in a thread-safe manner. It sets the current block - number, hashes the block with the hotkey, updates the current block bytes, and packs the difficulty. - - Parameters: - curr_diff: Shared array to store the current difficulty. - curr_block: Shared array to store the current block data. - curr_block_num: Shared value to store the current block number. - block_number: The block number to set as the current block number. - block_bytes: The block data bytes to be hashed with the hotkey. - diff: The difficulty value to be packed into the current difficulty array. - hotkey_bytes: The hotkey bytes used for hashing the block. - lock: A lock to ensure thread-safe updates. - """ - with lock: - curr_block_num.value = block_number - # Hash the block with the hotkey - block_and_hotkey_hash_bytes = _hash_block_with_hotkey(block_bytes, hotkey_bytes) - for i in range(32): - curr_block[i] = block_and_hotkey_hash_bytes[i] - _registration_diff_pack(diff, curr_diff) - - -def get_cpu_count() -> int: - """Returns the number of CPUs in the system.""" - try: - return len(os.sched_getaffinity(0)) - except AttributeError: - # macOS does not have sched_getaffinity - return os.cpu_count() - - -@dataclass -class RegistrationStatistics: - """Statistics for a registration.""" - - time_spent_total: float - rounds_total: int - time_average: float - time_spent: float - hash_rate_perpetual: float - hash_rate: float - difficulty: int - block_number: int - block_hash: str - - -class Status: - def __init__(self, status: str): - self._status = status - - def start(self): - pass - - def stop(self): - pass - - def update(self, status: str): - self._status = status - - -class Console: - @staticmethod - def status(status: str): - return Status(status) - - @staticmethod - def log(text: str): - print(text) - - -class RegistrationStatisticsLogger: - """Logs statistics for a registration.""" - - status: Optional["Status"] - - def __init__( - self, - console: Optional["Console"] = None, - output_in_place: bool = True, - ) -> None: - if console is None: - console = Console() - - self.console = console - - if output_in_place: - self.status = self.console.status("Solving") - else: - self.status = None - - def start(self) -> None: - if self.status is not None: - self.status.start() - - def stop(self) -> None: - if self.status is not None: - self.status.stop() - - @classmethod - def get_status_message( - cls, stats: "RegistrationStatistics", verbose: bool = False - ) -> str: - """Generates the status message based on registration statistics.""" - message = ( - "Solving\n" - + f"Time Spent (total): [bold white]{timedelta(seconds=stats.time_spent_total)}[/bold white]\n" - + ( - f"Time Spent This Round: {timedelta(seconds=stats.time_spent)}\n" - + f"Time Spent Average: {timedelta(seconds=stats.time_average)}\n" - if verbose - else "" - ) - + f"Registration Difficulty: [bold white]{millify(stats.difficulty)}[/bold white]\n" - + f"Iters (Inst/Perp): [bold white]{get_human_readable(stats.hash_rate, 'H')}/s / " - + f"{get_human_readable(stats.hash_rate_perpetual, 'H')}/s[/bold white]\n" - + f"Block Number: [bold white]{stats.block_number}[/bold white]\n" - + f"Block Hash: [bold white]{stats.block_hash.encode('utf-8')}[/bold white]\n" - ) - return message - - def update(self, stats: "RegistrationStatistics", verbose: bool = False) -> None: - if self.status is not None: - self.status.update(self.get_status_message(stats, verbose=verbose)) - else: - self.console.log(self.get_status_message(stats, verbose=verbose)) - - -def _solve_for_difficulty_fast( - subtensor: "Subtensor", - wallet: "Wallet", - netuid: int, - output_in_place: bool = True, - num_processes: Optional[int] = None, - update_interval: Optional[int] = None, - n_samples: int = 10, - alpha_: float = 0.80, - log_verbose: bool = False, -) -> Optional[POWSolution]: - """ - Solves the POW for registration using multiprocessing. - - Parameters: - subtensor: Subtensor instance. - wallet: wallet to use for registration. - netuid: The netuid of the subnet to register to. - output_in_place: If true, prints the status in place. Otherwise, prints the status on a new line. - num_processes: Number of processes to use. - update_interval: Number of nonces to solve before updating block information. - n_samples: The number of samples of the hash_rate to keep for the EWMA. - alpha_: The alpha for the EWMA for the hash_rate calculation. - log_verbose: If true, prints more verbose logging of the registration metrics. - - Note: - The hash rate is calculated as an exponentially weighted moving average in order to make the measure more robust. - We can also modify the update interval to do smaller blocks of work, while still updating the block information - after a different number of nonces, to increase the transparency of the process while still keeping the speed. - """ - if num_processes is None: - # get the number of allowed processes for this process - num_processes = min(1, get_cpu_count()) - - if update_interval is None: - update_interval = 50_000 - - limit = int(math.pow(2, 256)) - 1 - - curr_block, curr_block_num, curr_diff = Solver.create_shared_memory() - - # Establish communication queues - # See the Solver class for more information on the queues. - stopEvent = mp.Event() - stopEvent.clear() - - solution_queue = mp.Queue() - finished_queues = [mp.Queue() for _ in range(num_processes)] - check_block = mp.Lock() - - hotkey_bytes = ( - wallet.coldkeypub.public_key if netuid == -1 else wallet.hotkey.public_key - ) - # Start consumers - solvers = [ - Solver( - i, - num_processes, - update_interval, - finished_queues[i], - solution_queue, - stopEvent, - curr_block, - curr_block_num, - curr_diff, - check_block, - limit, - ) - for i in range(num_processes) - ] - - # Get first block - block_number, difficulty, block_hash = _get_block_with_retry( - subtensor=subtensor, netuid=netuid - ) - - block_bytes = bytes.fromhex(block_hash[2:]) - old_block_number = block_number - # Set to current block - update_curr_block( - curr_diff, - curr_block, - curr_block_num, - block_number, - block_bytes, - difficulty, - hotkey_bytes, - check_block, - ) - - # Set new block events for each solver to start at the initial block - for worker in solvers: - worker.newBlockEvent.set() - - for worker in solvers: - worker.start() # start the solver processes - - start_time = time.time() # time that the registration started - time_last = start_time # time that the last work blocks completed - - curr_stats = RegistrationStatistics( - time_spent_total=0.0, - time_average=0.0, - rounds_total=0, - time_spent=0.0, - hash_rate_perpetual=0.0, - hash_rate=0.0, - difficulty=difficulty, - block_number=block_number, - block_hash=block_hash, - ) - - start_time_perpetual = time.time() - - logger = RegistrationStatisticsLogger(output_in_place=output_in_place) - logger.start() - - solution = None - - hash_rates = [0] * n_samples # The last n true hash_rates - weights = [alpha_**i for i in range(n_samples)] # weights decay by alpha - - while netuid == -1 or not subtensor.is_hotkey_registered( - netuid=netuid, hotkey_ss58=wallet.hotkey.ss58_address - ): - # Wait until a solver finds a solution - try: - solution = solution_queue.get(block=True, timeout=0.25) - if solution is not None: - break - except Empty: - # No solution found, try again - pass - - # check for new block - old_block_number = _check_for_newest_block_and_update( - subtensor=subtensor, - netuid=netuid, - hotkey_bytes=hotkey_bytes, - old_block_number=old_block_number, - curr_diff=curr_diff, - curr_block=curr_block, - curr_block_num=curr_block_num, - curr_stats=curr_stats, - update_curr_block_=update_curr_block, - check_block=check_block, - solvers=solvers, - ) - - num_time = 0 - for finished_queue in finished_queues: - try: - finished_queue.get(timeout=0.1) - num_time += 1 - - except Empty: - continue - - time_now = time.time() # get current time - time_since_last = time_now - time_last # get time since last work block(s) - if num_time > 0 and time_since_last > 0.0: - # create EWMA of the hash_rate to make measure more robust - - hash_rate_ = (num_time * update_interval) / time_since_last - hash_rates.append(hash_rate_) - hash_rates.pop(0) # remove the 0th data point - curr_stats.hash_rate = sum( - [hash_rates[i] * weights[i] for i in range(n_samples)] - ) / (sum(weights)) - - # update time last to now - time_last = time_now - - curr_stats.time_average = ( - curr_stats.time_average * curr_stats.rounds_total - + curr_stats.time_spent - ) / (curr_stats.rounds_total + num_time) - curr_stats.rounds_total += num_time - - # Update stats - curr_stats.time_spent = time_since_last - new_time_spent_total = time_now - start_time_perpetual - curr_stats.hash_rate_perpetual = ( - curr_stats.rounds_total * update_interval - ) / new_time_spent_total - curr_stats.time_spent_total = new_time_spent_total - - # Update the logger - logger.update(curr_stats, verbose=log_verbose) - - # exited while, solution contains the nonce or wallet is registered - stopEvent.set() # stop all other processes - logger.stop() - - # terminate and wait for all solvers to exit - terminate_workers_and_wait_for_exit(solvers) - - return solution - - -def _get_block_with_retry(subtensor: "Subtensor", netuid: int) -> tuple[int, int, str]: - """ - Gets the current block number, difficulty, and block hash from the substrate node. - - Parameters: - subtensor: The subtensor instance. - netuid: The netuid of the network to get the block number, difficulty, and block hash from. - - Returns: - tuple[int, int, bytes] - - block_number: The current block number. - - difficulty: The current difficulty of the subnet. - - block_hash: The current block hash. - - Raises: - Exception: If the block hash is None. - ValueError: If the difficulty is None. - """ - block_number = subtensor.get_current_block() - difficulty = 1_000_000 if netuid == -1 else subtensor.difficulty(netuid=netuid) - block_hash = subtensor.get_block_hash(block_number) - if block_hash is None: - raise Exception( - "Network error. Could not connect to substrate to get block hash" - ) - if difficulty is None: - raise ValueError("Chain error. Difficulty is None") - return block_number, difficulty, block_hash - - -def _check_for_newest_block_and_update( - subtensor: "Subtensor", - netuid: int, - old_block_number: int, - hotkey_bytes: bytes, - curr_diff: "mp.Array", - curr_block: "mp.Array", - curr_block_num: "mp.Value", - update_curr_block_: "Callable", - check_block: "mp.Lock", - solvers: Union[list["Solver"], list["CUDASolver"]], - curr_stats: "RegistrationStatistics", -) -> int: - """ - Checks for a new block and updates the current block information if a new block is found. - - Parameters: - subtensor: Subtensor instance. - netuid: The netuid to use for retrieving the difficulty. - old_block_number: The old block number to check against. - hotkey_bytes: The bytes of the hotkey's pubkey. - curr_diff: The current difficulty as a multiprocessing array. - curr_block: Where the current block is stored as a multiprocessing array. - curr_block_num: Where the current block number is stored as a multiprocessing value. - update_curr_block_: A function that updates the current block. - check_block: A mp lock that is used to check for a new block. - solvers: A list of solvers to update the current block for. - curr_stats: The current registration statistics to update. - - Returns: - The current block number. - """ - block_number = subtensor.get_current_block() - if block_number != old_block_number: - old_block_number = block_number - # update block information - block_number, difficulty, block_hash = _get_block_with_retry( - subtensor=subtensor, netuid=netuid - ) - block_bytes = bytes.fromhex(block_hash[2:]) - - update_curr_block_( - curr_diff, - curr_block, - curr_block_num, - block_number, - block_bytes, - difficulty, - hotkey_bytes, - check_block, - ) - # Set new block events for each solver - - for worker in solvers: - worker.newBlockEvent.set() - - # update stats - curr_stats.block_number = block_number - curr_stats.block_hash = block_hash - curr_stats.difficulty = difficulty - - return old_block_number - - -def _solve_for_difficulty_fast_cuda( - subtensor: "Subtensor", - wallet: "Wallet", - netuid: int, - output_in_place: bool = True, - update_interval: int = 50_000, - tpb: int = 512, - dev_id: Union[list[int], int] = 0, - n_samples: int = 10, - alpha_: float = 0.80, - log_verbose: bool = False, -) -> Optional["POWSolution"]: - """ - Solves the registration fast using CUDA. - - Parameters: - subtensor: Subtensor instance. - wallet: Bittensor Wallet instance. - netuid: The netuid of the subnet to register to. - output_in_place: If true, prints the output in place, otherwise prints to new lines. - update_interval: The number of nonces to try before checking for more blocks. - tpb: The number of threads per block. CUDA param that should match the GPU capability - dev_id: The CUDA device IDs to execute the registration on, either a single device or a list of devices. - n_samples: The number of samples of the hash_rate to keep for the EWMA. - alpha_: The alpha for the EWMA for the hash_rate calculation. - log_verbose: If true, prints more verbose logging of the registration metrics. - - Note: - The hash rate is calculated as an exponentially weighted moving average in order to make the measure more robust. - """ - if isinstance(dev_id, int): - dev_id = [dev_id] - elif dev_id is None: - dev_id = [0] - - if update_interval is None: - update_interval = 50_000 - - if not torch.cuda.is_available(): - raise Exception("CUDA not available") - - limit = int(math.pow(2, 256)) - 1 - - # Set mp start to use spawn so CUDA doesn't complain - with UsingSpawnStartMethod(force=True): - curr_block, curr_block_num, curr_diff = CUDASolver.create_shared_memory() - - # Create a worker per CUDA device - num_processes = len(dev_id) - - # Establish communication queues - stopEvent = mp.Event() - stopEvent.clear() - solution_queue = mp.Queue() - finished_queues = [mp.Queue() for _ in range(num_processes)] - check_block = mp.Lock() - - hotkey_bytes = wallet.hotkey.public_key - # Start workers - solvers = [ - CUDASolver( - i, - num_processes, - update_interval, - finished_queues[i], - solution_queue, - stopEvent, - curr_block, - curr_block_num, - curr_diff, - check_block, - limit, - dev_id[i], - tpb, - ) - for i in range(num_processes) - ] - - # Get first block - block_number, difficulty, block_hash = _get_block_with_retry( - subtensor=subtensor, netuid=netuid - ) - - block_bytes = bytes.fromhex(block_hash[2:]) - old_block_number = block_number - - # Set to current block - update_curr_block( - curr_diff, - curr_block, - curr_block_num, - block_number, - block_bytes, - difficulty, - hotkey_bytes, - check_block, - ) - - # Set new block events for each solver to start at the initial block - for worker in solvers: - worker.newBlockEvent.set() - - for worker in solvers: - worker.start() # start the solver processes - - start_time = time.time() # time that the registration started - time_last = start_time # time that the last work blocks completed - - curr_stats = RegistrationStatistics( - time_spent_total=0.0, - time_average=0.0, - rounds_total=0, - time_spent=0.0, - hash_rate_perpetual=0.0, - hash_rate=0.0, # EWMA hash_rate (H/s) - difficulty=difficulty, - block_number=block_number, - block_hash=block_hash, - ) - - start_time_perpetual = time.time() - - logger = RegistrationStatisticsLogger(output_in_place=output_in_place) - logger.start() - - hash_rates = [0] * n_samples # The last n true hash_rates - weights = [alpha_**i for i in range(n_samples)] # weights decay by alpha - - solution = None - while netuid == -1 or not subtensor.is_hotkey_registered( - netuid=netuid, hotkey_ss58=wallet.hotkey.ss58_address - ): - # Wait until a solver finds a solution - try: - solution = solution_queue.get(block=True, timeout=0.15) - if solution is not None: - break - except Empty: - # No solution found, try again - pass - - # check for new block - old_block_number = _check_for_newest_block_and_update( - subtensor=subtensor, - netuid=netuid, - hotkey_bytes=hotkey_bytes, - curr_diff=curr_diff, - curr_block=curr_block, - curr_block_num=curr_block_num, - old_block_number=old_block_number, - curr_stats=curr_stats, - update_curr_block_=update_curr_block, - check_block=check_block, - solvers=solvers, - ) - - num_time = 0 - # Get times for each solver - for finished_queue in finished_queues: - try: - finished_queue.get(timeout=0.1) - num_time += 1 - - except Empty: - continue - - time_now = time.time() # get current time - time_since_last = time_now - time_last # get time since last work block(s) - if num_time > 0 and time_since_last > 0.0: - # create EWMA of the hash_rate to make measure more robust - - hash_rate_ = (num_time * tpb * update_interval) / time_since_last - hash_rates.append(hash_rate_) - hash_rates.pop(0) # remove the 0th data point - curr_stats.hash_rate = sum( - [hash_rates[i] * weights[i] for i in range(n_samples)] - ) / (sum(weights)) - - # update time last to now - time_last = time_now - - curr_stats.time_average = ( - curr_stats.time_average * curr_stats.rounds_total - + curr_stats.time_spent - ) / (curr_stats.rounds_total + num_time) - curr_stats.rounds_total += num_time - - # Update stats - curr_stats.time_spent = time_since_last - new_time_spent_total = time_now - start_time_perpetual - curr_stats.hash_rate_perpetual = ( - curr_stats.rounds_total * (tpb * update_interval) - ) / new_time_spent_total - curr_stats.time_spent_total = new_time_spent_total - - # Update the logger - logger.update(curr_stats, verbose=log_verbose) - - # exited while, found_solution contains the nonce or wallet is registered - - stopEvent.set() # stop all other processes - logger.stop() - - # terminate and wait for all solvers to exit - terminate_workers_and_wait_for_exit(solvers) - - return solution - - -def terminate_workers_and_wait_for_exit( - workers: list[Union[mp.Process, QueueType]], -) -> None: - for worker in workers: - if isinstance(worker, QueueType): - worker.join_thread() - else: - try: - worker.join(3.0) - except subprocess.TimeoutExpired: - worker.terminate() - try: - worker.close() - except ValueError: - worker.terminate() - - -def create_pow( - subtensor: "Subtensor", - wallet: "Wallet", - netuid: int, - output_in_place: bool = True, - cuda: bool = False, - dev_id: Union[list[int], int] = 0, - tpb: int = 256, - num_processes: Optional[int] = None, - update_interval: Optional[int] = None, - log_verbose: bool = False, -) -> Optional["POWSolution"]: - """ - Creates a proof of work for the given subtensor and wallet. - - Parameters: - subtensor: The Subtensor instance. - wallet: The Bittensor Wallet instance. - netuid: The netuid for the subnet to create a proof of work for. - output_in_place: If true, prints the progress of the proof of work to the console in-place. Meaning the progress - is printed on the same lines. - cuda: If true, uses CUDA to solve the proof of work. - dev_id: The CUDA device id(s) to use. If cuda is true and dev_id is a list, then multiple CUDA devices will be - used to solve the proof of work. - tpb: The number of threads per block to use when solving the proof of work. Should be a multiple of 32. - num_processes: The number of processes to use when solving the proof of work. If None, then the number of - processes is equal to the number of CPU cores. - update_interval: The number of nonces to run before checking for a new block. - log_verbose: If true, prints the progress of the proof of work more verbosely. - - Returns: - The proof of work solution or None if the wallet is already registered or there is a different error. - - Raises: - ValueError: If the subnet does not exist. - """ - if netuid != -1: - if not subtensor.subnet_exists(netuid=netuid): - raise ValueError(f"Subnet {netuid} does not exist.") - - if cuda: - logging.debug("Solve difficulty with CUDA.") - solution: Optional[POWSolution] = _solve_for_difficulty_fast_cuda( - subtensor=subtensor, - wallet=wallet, - netuid=netuid, - output_in_place=output_in_place, - dev_id=dev_id, - tpb=tpb, - update_interval=update_interval, - log_verbose=log_verbose, - ) - else: - logging.debug("Solve difficulty.") - solution: Optional[POWSolution] = _solve_for_difficulty_fast( - subtensor=subtensor, - wallet=wallet, - netuid=netuid, - output_in_place=output_in_place, - num_processes=num_processes, - update_interval=update_interval, - log_verbose=log_verbose, - ) - return solution diff --git a/bittensor/utils/registration/register_cuda.py b/bittensor/utils/registration/register_cuda.py deleted file mode 100644 index 34c8e00115..0000000000 --- a/bittensor/utils/registration/register_cuda.py +++ /dev/null @@ -1,123 +0,0 @@ -"""This module provides functions for solving Proof of Work (PoW) problems using CUDA.""" - -import binascii -import hashlib -import io -from contextlib import redirect_stdout -from typing import Any, Union - -import numpy as np -from Crypto.Hash import keccak - - -def _hex_bytes_to_u8_list(hex_bytes: bytes) -> list[int]: - """ - Convert a sequence of bytes in hexadecimal format to a list of - unsigned 8-bit integers. - - Parameters: - hex_bytes: A sequence of bytes in hexadecimal format. - - Returns: - A list of unsigned 8-bit integers. - - """ - return [int(hex_bytes[i : i + 2], 16) for i in range(0, len(hex_bytes), 2)] - - -def _create_seal_hash(block_and_hotkey_hash_hex_: bytes, nonce: int) -> bytes: - """Creates a seal hash from the block and hotkey hash and nonce.""" - nonce_bytes = binascii.hexlify(nonce.to_bytes(8, "little")) - pre_seal = nonce_bytes + block_and_hotkey_hash_hex_ - seal_sh256 = hashlib.sha256(bytearray(_hex_bytes_to_u8_list(pre_seal))).digest() - kec = keccak.new(digest_bits=256) - return kec.update(seal_sh256).digest() - - -def _seal_meets_difficulty(seal_: bytes, difficulty: int, limit: int) -> bool: - """Checks if the seal meets the given difficulty.""" - seal_number = int.from_bytes(seal_, "big") - product = seal_number * difficulty - # limit = int(math.pow(2, 256)) - 1 - return product < limit - - -def solve_cuda( - nonce_start: "np.int64", - update_interval: "np.int64", - tpb: int, - block_and_hotkey_hash_bytes: bytes, - difficulty: int, - limit: int, - dev_id: int = 0, -) -> Union[tuple[Any, bytes], tuple[int, bytes], tuple[Any, None]]: - """ - Solves the PoW problem using CUDA. - - Parameters: - nonce_start: Starting nonce. - update_interval: Number of nonces to solve before updating block information. - tpb: Threads per block. - block_and_hotkey_hash_bytes: Keccak(Bytes of the block hash + bytes of the hotkey) 64 bytes. - difficulty: Difficulty of the PoW problem. - limit: Upper limit of the nonce. - dev_id: The CUDA device ID. - - Returns: - Tuple of the nonce and the seal corresponding to the solution. Returns -1 for nonce if no solution is found. - """ - - try: - import cubit - except ImportError: - raise ImportError( - "Please install cubit. See the instruction https://github.com/opentensor/cubit?tab=readme-ov-file#install." - ) - - upper = int(limit // difficulty) - - upper_bytes = upper.to_bytes(32, byteorder="little", signed=False) - - # Call cython function - # int blockSize, uint64 nonce_start, uint64 update_interval, const unsigned char[:] limit, - # const unsigned char[:] block_bytes, int dev_id - block_and_hotkey_hash_hex = binascii.hexlify(block_and_hotkey_hash_bytes)[:64] - - solution = cubit.solve_cuda( - tpb, - nonce_start, - update_interval, - upper_bytes, - block_and_hotkey_hash_hex, - dev_id, - ) # 0 is first GPU - seal = None - if solution != -1: - seal = _create_seal_hash(block_and_hotkey_hash_hex, solution) - if _seal_meets_difficulty(seal, difficulty, limit): - return solution, seal - else: - return -1, b"\x00" * 32 - return solution, seal - - -def reset_cuda(): - """Resets the CUDA environment.""" - try: - import cubit - except ImportError: - raise ImportError("Please install cubit") - cubit.reset_cuda() - - -def log_cuda_errors() -> str: - """Logs any CUDA errors.""" - try: - import cubit - except ImportError: - raise ImportError("Please install cubit") - - file = io.StringIO() - with redirect_stdout(file): - cubit.log_cuda_errors() - return file.getvalue() diff --git a/bittensor/utils/registration/torch_utils.py b/bittensor/utils/registration/torch_utils.py new file mode 100644 index 0000000000..38fd9131a1 --- /dev/null +++ b/bittensor/utils/registration/torch_utils.py @@ -0,0 +1,82 @@ +"""Torch compatibility utilities for Bittensor.""" + +import functools +import os +from typing import TYPE_CHECKING + +import numpy + +from bittensor.utils.btlogging import logging + + +def use_torch() -> bool: + """Force the use of torch over numpy for certain operations.""" + return True if os.getenv("USE_TORCH") == "1" else False + + +def legacy_torch_api_compat(func): + """ + Convert function operating on numpy Input&Output to legacy torch Input&Output API if `use_torch()` is True. + + Parameters: + func: Function with numpy Input/Output to be decorated. + + Returns: + decorated: Decorated function. + """ + + @functools.wraps(func) + def decorated(*args, **kwargs): + if use_torch(): + args = [ + arg.cpu().numpy() if isinstance(arg, torch.Tensor) else arg + for arg in args + ] + kwargs = { + key: value.cpu().numpy() if isinstance(value, torch.Tensor) else value + for key, value in kwargs.items() + } + ret = func(*args, **kwargs) + if use_torch(): + if isinstance(ret, numpy.ndarray): + ret = torch.from_numpy(ret) + return ret + + return decorated + + +@functools.cache +def _get_real_torch(): + try: + import torch as _real_torch + except ImportError: + _real_torch = None + return _real_torch + + +def log_no_torch_error(): + logging.error( + "This command requires torch. You can install torch for bittensor" + ' with `pip install bittensor[torch]` or `pip install ".[torch]"`' + " if installing from source, and then run the command with USE_TORCH=1 {command}" + ) + + +class LazyLoadedTorch: + """A lazy-loading proxy for the torch module.""" + + def __bool__(self): + return bool(_get_real_torch()) + + def __getattr__(self, name): + if real_torch := _get_real_torch(): + return getattr(real_torch, name) + else: + log_no_torch_error() + raise ImportError("torch not installed") + + +if TYPE_CHECKING: + import torch +else: + torch = LazyLoadedTorch() diff --git a/bittensor/utils/version.py b/bittensor/utils/version.py index e78fd109e8..53aa8b8550 100644 --- a/bittensor/utils/version.py +++ b/bittensor/utils/version.py @@ -117,6 +117,6 @@ def check_latest_version_in_pypi(): except InvalidVersion: # stay silent if InvalidVersion pass - except (requests.RequestException, KeyError) as e: + except (requests.RequestException, KeyError): # stay silent if not internet connection or pypi.org issue pass diff --git a/pyproject.toml b/pyproject.toml index 3c600ad6dd..557681cbec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "bittensor" -version = "10.2.1" +version = "10.3.0" description = "Bittensor SDK" readme = "README.md" authors = [ @@ -15,11 +15,10 @@ requires-python = ">=3.10,<3.15" dependencies = [ "wheel", "setuptools~=70.0", - "aiohttp>=3.9,<4.0", + "aiohttp>=3.13.4,<4.0", "asyncstdlib~=3.13.0", "colorama~=0.4.6", "fastapi>=0.110.1", - "munch>=4.0.0", "numpy>=2.0.1,<3.0.0", "msgpack-numpy-opentensor~=0.5", "netaddr==1.3.0", @@ -28,13 +27,13 @@ dependencies = [ "pycryptodome>=3.18.0,<4.0.0", "pyyaml>=6.0", "retry==0.9.2", - "requests>=2.0.0,<3.0", + "requests>=2.33.0,<3.0", "pydantic>=2.3,<3", - "scalecodec==1.2.12", + "cyscale>=0.3.1,<1.0.0", "uvicorn", "bittensor-drand>=1.3.0,<2.0.0", "bittensor-wallet==4.0.1", - "async-substrate-interface>=1.6.2,<2.0.0" + "async-substrate-interface==2.0.2", ] [project.optional-dependencies] @@ -49,13 +48,12 @@ dev = [ "pytest-cov==4.0.0", "ddt==1.6.0", "hypothesis==6.81.1", - "flake8==7.0.0", - "mypy==1.8.0", + "mypy==1.20.1", "types-retry==0.9.9.4", "typing_extensions>= 4.0.0; python_version<'3.11'", "freezegun==1.5.0", - "httpx==0.27.0", - "ruff==0.11.5", + "httpx==0.27.0", # used by tests/unit_tests/test_axon: The starlette.testclient module requires the httpx package to be installed. + "ruff==0.15.12", "aioresponses==0.7.6", "factory-boy==3.3.0", "types-requests", @@ -64,7 +62,7 @@ torch = [ "torch>=1.13.1,<3.0" ] cli = [ - "bittensor-cli>=9.16.0" + "bittensor-cli>=9.21.0" ] @@ -97,3 +95,9 @@ classifiers = [ [tool.setuptools] package-dir = { "bittensor" = "bittensor" } script-files = ["bittensor/utils/certifi.sh"] + +[tool.ruff] +exclude = [".git", "__pycache__", "__init__.py", "docs/source/conf.py", "old", "build", "dist", "venv", ".venv", ".tox"] + +[tool.ruff.lint] +select = ["E9", "F63", "F7", "F82", "F401"] diff --git a/tests/consistency/test_proxy_types.py b/tests/consistency/test_proxy_types.py index ab9462f8bc..5bb3e8be6b 100644 --- a/tests/consistency/test_proxy_types.py +++ b/tests/consistency/test_proxy_types.py @@ -1,5 +1,4 @@ from bittensor.core.chain_data.proxy import ProxyType -from bittensor.core.extrinsics.pallets import SubtensorModule, Proxy, Balances def get_proxy_type_fields(meta): diff --git a/tests/e2e_tests/test_coldkey_swap.py b/tests/e2e_tests/test_coldkey_swap.py index 926c7f6ff0..0cd3613176 100644 --- a/tests/e2e_tests/test_coldkey_swap.py +++ b/tests/e2e_tests/test_coldkey_swap.py @@ -38,15 +38,24 @@ def test_coldkey_swap(subtensor, alice_wallet, bob_wallet, charlie_wallet, dave_ - Step 2: Attempt to execute other transaction (transfer) from announced coldkey - Step 3: Verify transaction is blocked (except swap_coldkey_announced) - 5. Dispute and root reset: + 5. Clear announcement: + - Step 1: Announce swap + - Step 2: Attempt to clear too early (should fail) + - Step 3: Wait for clear block (execution_block + reannouncement_delay) + - Step 4: Clear the announcement + - Step 5: Verify announcement is removed + - Step 6: Verify transfers are unblocked after clear + + 6. Dispute and root reset: - Step 1: Dave announces swap, then disputes it (dispute_coldkey_swap) - Step 2: Verify dispute is recorded (get_coldkey_swap_dispute) - Step 3: Verify account is blocked (transfer fails) - - Step 4: Root resets coldkey swap (reset_coldkey_swap) - - Step 5: Verify dispute and announcement are cleared - - Step 6: Verify transfers are unblocked after reset + - Step 4: Verify clear is blocked while disputed + - Step 5: Root resets coldkey swap (reset_coldkey_swap) + - Step 6: Verify dispute and announcement are cleared + - Step 7: Verify transfers are unblocked after reset - 6. Root swap override: + 7. Root swap override: - Step 1: Root swaps Dave to Charlie without announcement - Step 2: Verify announcement and dispute are cleared - Step 3: Verify old coldkey is reaped @@ -434,7 +443,74 @@ def assert_coldkey_reaped(coldkey_ss58: str, label: str) -> None: ) assert response.success, f"Failed to fund Dave: {response.message}" - # === 5. Dispute and root reset === + # === 5. Clear announcement === + logging.console.info("Testing clear announcement") + + # Step 1: Alice announces swap to Bob + logging.console.info("Step 1: Alice announces swap to Bob") + existing_announcement = subtensor.wallets.get_coldkey_swap_announcement( + coldkey_ss58=alice_wallet.coldkeypub.ss58_address + ) + assert existing_announcement is None, ( + "No announcement should exist before clear test" + ) + response = subtensor.extrinsics.announce_coldkey_swap( + wallet=alice_wallet, + new_coldkey_ss58=bob_wallet.coldkeypub.ss58_address, + ) + assert response.success, f"Failed to announce swap: {response.message}" + announcement = subtensor.wallets.get_coldkey_swap_announcement( + coldkey_ss58=alice_wallet.coldkeypub.ss58_address + ) + assert announcement is not None, "Announcement should exist" + + # Step 2: Attempt to clear too early (before clear_block) + logging.console.info("Step 2: Attempting clear too early (should fail)") + clear_block = announcement.execution_block + reannouncement_delay + current_block = subtensor.chain.get_current_block() + assert current_block < clear_block, "Current block should be before clear block" + response = subtensor.extrinsics.clear_coldkey_swap_announcement( + wallet=alice_wallet, + raise_error=False, + ) + assert not response.success, "Clear should fail before clear block" + logging.console.info("Clear too early correctly rejected") + + # Step 3: Wait for clear_block (execution_block + reannouncement_delay) + logging.console.info( + f"Step 3: Waiting for clear block {clear_block} " + f"(execution={announcement.execution_block} + reannounce_delay={reannouncement_delay})" + ) + subtensor.wait_for_block(clear_block) + + # Step 4: Clear the announcement + logging.console.info("Step 4: Clearing announcement") + response = subtensor.extrinsics.clear_coldkey_swap_announcement( + wallet=alice_wallet, + ) + assert response.success, f"Failed to clear announcement: {response.message}" + + # Step 5: Verify announcement is removed + logging.console.info("Step 5: Verify announcement is removed") + announcement_after_clear = subtensor.wallets.get_coldkey_swap_announcement( + coldkey_ss58=alice_wallet.coldkeypub.ss58_address + ) + assert announcement_after_clear is None, ( + "Announcement should be removed after clear" + ) + + # Step 6: Verify transfers work again + logging.console.info("Step 6: Verify transfers are unblocked after clear") + response = subtensor.extrinsics.transfer( + wallet=alice_wallet, + destination_ss58=charlie_wallet.coldkeypub.ss58_address, + amount=Balance.from_tao(1), + raise_error=False, + ) + assert response.success, "Transfer should be allowed after clear" + logging.console.info("Clear announcement test completed successfully") + + # === 6. Dispute and root reset === logging.console.info("Testing dispute and root reset") # Step 1: Dave announces swap to Charlie @@ -478,6 +554,14 @@ def assert_coldkey_reaped(coldkey_ss58: str, label: str) -> None: raise_error=False, ) assert not response.success, "Transfer should be blocked while disputed" + + # Step 4b: Verify clear is also blocked while disputed + logging.console.info("Step 4b: Verify clear is blocked while disputed") + response = subtensor.extrinsics.clear_coldkey_swap_announcement( + wallet=dave_wallet, + raise_error=False, + ) + assert not response.success, "Clear should be blocked while disputed" logging.console.info("Account blocking verified") # Step 5: Root resets the coldkey swap (alice_wallet is //Alice, root) @@ -513,7 +597,7 @@ def assert_coldkey_reaped(coldkey_ss58: str, label: str) -> None: assert response.success, "Transfer should be allowed after reset" logging.console.info("Dispute scenario completed successfully") - # === 6. Root swap override === + # === 7. Root swap override === logging.console.info("Testing root swap override") # Ensure Dave has enough balance for root swap cost @@ -584,15 +668,24 @@ async def test_coldkey_swap_async( - Step 2: Attempt to execute other transaction (transfer) from announced coldkey - Step 3: Verify transaction is blocked (except swap_coldkey_announced) - 5. Dispute and root reset: + 5. Clear announcement: + - Step 1: Announce swap + - Step 2: Attempt to clear too early (should fail) + - Step 3: Wait for clear block (execution_block + reannouncement_delay) + - Step 4: Clear the announcement + - Step 5: Verify announcement is removed + - Step 6: Verify transfers are unblocked after clear + + 6. Dispute and root reset: - Step 1: Dave announces swap, then disputes it (dispute_coldkey_swap) - Step 2: Verify dispute is recorded (get_coldkey_swap_dispute) - Step 3: Verify account is blocked (transfer fails) - - Step 4: Root resets coldkey swap (reset_coldkey_swap) - - Step 5: Verify dispute and announcement are cleared - - Step 6: Verify transfers are unblocked after reset + - Step 4: Verify clear is blocked while disputed + - Step 5: Root resets coldkey swap (reset_coldkey_swap) + - Step 6: Verify dispute and announcement are cleared + - Step 7: Verify transfers are unblocked after reset - 6. Root swap override: + 7. Root swap override: - Step 1: Root swaps Dave to Charlie without announcement - Step 2: Verify announcement and dispute are cleared - Step 3: Verify old coldkey is reaped @@ -996,7 +1089,76 @@ async def assert_coldkey_reaped(coldkey_ss58: str, label: str) -> None: ) assert response.success, f"Failed to fund Dave: {response.message}" - # === 5. Dispute and root reset === + # === 5. Clear announcement === + logging.console.info("Testing clear announcement") + + # Step 1: Alice announces swap to Bob + logging.console.info("Step 1: Alice announces swap to Bob") + existing_announcement = await async_subtensor.wallets.get_coldkey_swap_announcement( + coldkey_ss58=alice_wallet.coldkeypub.ss58_address + ) + assert existing_announcement is None, ( + "No announcement should exist before clear test" + ) + response = await async_subtensor.extrinsics.announce_coldkey_swap( + wallet=alice_wallet, + new_coldkey_ss58=bob_wallet.coldkeypub.ss58_address, + ) + assert response.success, f"Failed to announce swap: {response.message}" + announcement = await async_subtensor.wallets.get_coldkey_swap_announcement( + coldkey_ss58=alice_wallet.coldkeypub.ss58_address + ) + assert announcement is not None, "Announcement should exist" + + # Step 2: Attempt to clear too early (before clear_block) + logging.console.info("Step 2: Attempting clear too early (should fail)") + clear_block = announcement.execution_block + reannouncement_delay + current_block = await async_subtensor.chain.get_current_block() + assert current_block < clear_block, "Current block should be before clear block" + response = await async_subtensor.extrinsics.clear_coldkey_swap_announcement( + wallet=alice_wallet, + raise_error=False, + ) + assert not response.success, "Clear should fail before clear block" + logging.console.info("Clear too early correctly rejected") + + # Step 3: Wait for clear_block (execution_block + reannouncement_delay) + logging.console.info( + f"Step 3: Waiting for clear block {clear_block} " + f"(execution={announcement.execution_block} + reannounce_delay={reannouncement_delay})" + ) + await async_subtensor.wait_for_block(clear_block) + + # Step 4: Clear the announcement + logging.console.info("Step 4: Clearing announcement") + response = await async_subtensor.extrinsics.clear_coldkey_swap_announcement( + wallet=alice_wallet, + ) + assert response.success, f"Failed to clear announcement: {response.message}" + + # Step 5: Verify announcement is removed + logging.console.info("Step 5: Verify announcement is removed") + announcement_after_clear = ( + await async_subtensor.wallets.get_coldkey_swap_announcement( + coldkey_ss58=alice_wallet.coldkeypub.ss58_address + ) + ) + assert announcement_after_clear is None, ( + "Announcement should be removed after clear" + ) + + # Step 6: Verify transfers work again + logging.console.info("Step 6: Verify transfers are unblocked after clear") + response = await async_subtensor.extrinsics.transfer( + wallet=alice_wallet, + destination_ss58=charlie_wallet.coldkeypub.ss58_address, + amount=Balance.from_tao(1), + raise_error=False, + ) + assert response.success, "Transfer should be allowed after clear" + logging.console.info("Clear announcement test completed successfully") + + # === 6. Dispute and root reset === logging.console.info("Testing dispute and root reset") # Step 1: Dave announces swap to Charlie @@ -1040,6 +1202,14 @@ async def assert_coldkey_reaped(coldkey_ss58: str, label: str) -> None: raise_error=False, ) assert not response.success, "Transfer should be blocked while disputed" + + # Step 4b: Verify clear is also blocked while disputed + logging.console.info("Step 4b: Verify clear is blocked while disputed") + response = await async_subtensor.extrinsics.clear_coldkey_swap_announcement( + wallet=dave_wallet, + raise_error=False, + ) + assert not response.success, "Clear should be blocked while disputed" logging.console.info("Account blocking verified") # Step 5: Root resets the coldkey swap (alice_wallet is //Alice, root) @@ -1077,7 +1247,7 @@ async def assert_coldkey_reaped(coldkey_ss58: str, label: str) -> None: assert response.success, "Transfer should be allowed after reset" logging.console.info("Dispute scenario completed successfully") - # === 6. Root swap override === + # === 7. Root swap override === logging.console.info("Testing root swap override") # Ensure Dave has enough balance for root swap cost diff --git a/tests/e2e_tests/test_commitment.py b/tests/e2e_tests/test_commitment.py index be1f14b5e8..484063089f 100644 --- a/tests/e2e_tests/test_commitment.py +++ b/tests/e2e_tests/test_commitment.py @@ -24,7 +24,10 @@ def test_commitment(subtensor, alice_wallet, dave_wallet): ] dave_sn.execute_steps(steps) - with pytest.raises(SubstrateRequestException, match="AccountNotAllowedCommit"): + with pytest.raises( + SubstrateRequestException, + match=r"AccountNotAllowedCommit|Invalid signing address", + ): subtensor.commitments.set_commitment( wallet=alice_wallet, netuid=dave_sn.netuid, @@ -89,7 +92,10 @@ async def test_commitment_async(async_subtensor, alice_wallet, dave_wallet): ] await dave_sn.async_execute_steps(steps) - with pytest.raises(SubstrateRequestException, match="AccountNotAllowedCommit"): + with pytest.raises( + SubstrateRequestException, + match=r"AccountNotAllowedCommit|Invalid signing address", + ): await async_subtensor.commitments.set_commitment( wallet=alice_wallet, netuid=dave_sn.netuid, diff --git a/tests/e2e_tests/test_crowdloan.py b/tests/e2e_tests/test_crowdloan.py index 52b99bfc9c..b771d36609 100644 --- a/tests/e2e_tests/test_crowdloan.py +++ b/tests/e2e_tests/test_crowdloan.py @@ -1,6 +1,5 @@ from bittensor import Balance from bittensor.core.extrinsics.pallets import SubtensorModule -from bittensor_wallet import Wallet import pytest import asyncio diff --git a/tests/e2e_tests/test_delegate.py b/tests/e2e_tests/test_delegate.py index d0a4a6fb95..07a4d67402 100644 --- a/tests/e2e_tests/test_delegate.py +++ b/tests/e2e_tests/test_delegate.py @@ -2,7 +2,6 @@ from bittensor.core.chain_data.chain_identity import ChainIdentity from bittensor.core.chain_data.delegate_info import DelegatedInfo, DelegateInfo -from bittensor.core.chain_data.proposal_vote_data import ProposalVoteData from bittensor.core.errors import ( DelegateTakeTooHigh, DelegateTxRateLimitExceeded, @@ -11,13 +10,9 @@ ) from bittensor.utils.balance import Balance from tests.e2e_tests.utils import ( - async_propose, async_set_identity, - async_vote, get_dynamic_balance, - propose, set_identity, - vote, TestSubnet, AdminUtils, ACTIVATE_SUBNET, @@ -26,7 +21,6 @@ SUDO_SET_NOMINATOR_MIN_REQUIRED_STAKE, SUDO_SET_TX_DELEGATE_TAKE_RATE_LIMIT, ) -from tests.helpers.helpers import CloseInValue DEFAULT_DELEGATE_TAKE = 0.179995422293431 diff --git a/tests/e2e_tests/test_dendrite.py b/tests/e2e_tests/test_dendrite.py index fda19d35de..584fe09635 100644 --- a/tests/e2e_tests/test_dendrite.py +++ b/tests/e2e_tests/test_dendrite.py @@ -46,7 +46,7 @@ def test_dendrite(subtensor, templates, alice_wallet, bob_wallet): REGISTER_SUBNET(alice_wallet), SUDO_SET_TEMPO(alice_wallet, AdminUtils, True, NETUID, TEMPO_TO_SET), ACTIVATE_SUBNET(alice_wallet), - SUDO_SET_MAX_ALLOWED_VALIDATORS(alice_wallet, AdminUtils, True, NETUID, 1), + SUDO_SET_MAX_ALLOWED_VALIDATORS(alice_wallet, AdminUtils, True, NETUID, 2), SUDO_SET_WEIGHTS_SET_RATE_LIMIT(alice_wallet, AdminUtils, True, NETUID, 10), REGISTER_NEURON(bob_wallet), ] @@ -160,7 +160,7 @@ async def test_dendrite_async(async_subtensor, templates, alice_wallet, bob_wall REGISTER_SUBNET(alice_wallet), SUDO_SET_TEMPO(alice_wallet, AdminUtils, True, NETUID, TEMPO_TO_SET), ACTIVATE_SUBNET(alice_wallet), - SUDO_SET_MAX_ALLOWED_VALIDATORS(alice_wallet, AdminUtils, True, NETUID, 1), + SUDO_SET_MAX_ALLOWED_VALIDATORS(alice_wallet, AdminUtils, True, NETUID, 2), SUDO_SET_WEIGHTS_SET_RATE_LIMIT(alice_wallet, AdminUtils, True, NETUID, 10), REGISTER_NEURON(bob_wallet), ] diff --git a/tests/e2e_tests/test_incentive.py b/tests/e2e_tests/test_incentive.py index 76f1f6cf41..e02455e166 100644 --- a/tests/e2e_tests/test_incentive.py +++ b/tests/e2e_tests/test_incentive.py @@ -2,7 +2,7 @@ import time import pytest -from bittensor.utils.btlogging import logging +from bittensor.utils.balance import Balance from tests.e2e_tests.utils import ( TestSubnet, AdminUtils, @@ -45,6 +45,14 @@ def test_incentive(subtensor, templates, alice_wallet, bob_wallet): "Alice & Bob not registered in the subnet" ) + # Stake so Alice has active_stake for Yuma3 dividend calculation + assert subtensor.staking.add_stake( + wallet=alice_wallet, + netuid=alice_sn.netuid, + hotkey_ss58=alice_wallet.hotkey.ss58_address, + amount=Balance.from_tao(10_000), + ).success + # Wait for the first epoch to pass subtensor.wait_for_block( subtensor.subnets.get_next_epoch_start_block(alice_sn.netuid) + 5 @@ -106,46 +114,46 @@ def test_incentive(subtensor, templates, alice_wallet, bob_wallet): ) assert validators[bob_uid] == 0 - while True: + max_retries = 30 + last_error = None + for _ in range(max_retries): + time.sleep(1) try: neurons = subtensor.neurons.neurons(netuid=alice_sn.netuid) - logging.info(f"neurons: {neurons}") # Get current emissions and validate that Alice has gotten tao alice_neuron = neurons[0] assert alice_neuron.validator_permit is True - assert alice_neuron.dividends == 1.0 - assert alice_neuron.stake.tao > 0 - assert alice_neuron.validator_trust > 0.99 - assert alice_neuron.incentive < 0.5 - assert alice_neuron.consensus < 0.5 + assert alice_neuron.dividends == 1.0, f"dividends={alice_neuron.dividends}" + assert alice_neuron.stake.tao > 0, f"stake={alice_neuron.stake.tao}" + assert alice_neuron.validator_trust > 0.99, ( + f"vtrust={alice_neuron.validator_trust}" + ) + assert alice_neuron.incentive < 0.5, f"incentive={alice_neuron.incentive}" + assert alice_neuron.consensus < 0.5, f"consensus={alice_neuron.consensus}" bob_neuron = neurons[1] - assert bob_neuron.incentive > 0.5 - assert bob_neuron.consensus > 0.5 + assert bob_neuron.incentive > 0.5, f"bob.incentive={bob_neuron.incentive}" + assert bob_neuron.consensus > 0.5, f"bob.consensus={bob_neuron.consensus}" bonds = subtensor.subnets.bonds(alice_sn.netuid) - assert bonds == [ - ( - 0, - [ - (0, 65535), - (1, 65535), - ], - ), - ( - 1, - [], - ), - ] + assert len(bonds) == 2, f"bonds={bonds}" + assert bonds[0][0] == 0, f"bonds={bonds}" + assert len(bonds[0][1]) == 1, f"bonds={bonds}" + assert bonds[0][1][0][0] == 1, f"bonds={bonds}" + assert bonds[0][1][0][1] > 0, f"bonds={bonds}" + assert bonds[1] == (1, []), f"bonds={bonds}" break - except Exception: + except Exception as e: + last_error = e subtensor.wait_for_block(subtensor.block) continue + else: + pytest.fail(f"Neuron metrics did not reach expected values: {last_error}") @pytest.mark.asyncio @@ -178,6 +186,16 @@ async def test_incentive_async(async_subtensor, templates, alice_wallet, bob_wal "Alice & Bob not registered in the subnet" ) + # Stake so Alice has active_stake for Yuma3 dividend calculation + assert ( + await async_subtensor.staking.add_stake( + wallet=alice_wallet, + netuid=alice_sn.netuid, + hotkey_ss58=alice_wallet.hotkey.ss58_address, + amount=Balance.from_tao(10_000), + ) + ).success + # Wait for the first epoch to pass next_epoch_start_block = await async_subtensor.subnets.get_next_epoch_start_block( netuid=alice_sn.netuid @@ -245,43 +263,43 @@ async def test_incentive_async(async_subtensor, templates, alice_wallet, bob_wal ) assert validators[bob_uid] == 0 - while True: + max_retries = 30 + last_error = None + for _ in range(max_retries): + await asyncio.sleep(1) try: neurons = await async_subtensor.neurons.neurons(netuid=alice_sn.netuid) - logging.info(f"neurons: {neurons}") # Get current emissions and validate that Alice has gotten tao alice_neuron = neurons[0] assert alice_neuron.validator_permit is True - assert alice_neuron.dividends == 1.0 - assert alice_neuron.stake.tao > 0 - assert alice_neuron.validator_trust > 0.99 - assert alice_neuron.incentive < 0.5 - assert alice_neuron.consensus < 0.5 + assert alice_neuron.dividends == 1.0, f"dividends={alice_neuron.dividends}" + assert alice_neuron.stake.tao > 0, f"stake={alice_neuron.stake.tao}" + assert alice_neuron.validator_trust > 0.99, ( + f"vtrust={alice_neuron.validator_trust}" + ) + assert alice_neuron.incentive < 0.5, f"incentive={alice_neuron.incentive}" + assert alice_neuron.consensus < 0.5, f"consensus={alice_neuron.consensus}" bob_neuron = neurons[1] - assert bob_neuron.incentive > 0.5 - assert bob_neuron.consensus > 0.5 + assert bob_neuron.incentive > 0.5, f"bob.incentive={bob_neuron.incentive}" + assert bob_neuron.consensus > 0.5, f"bob.consensus={bob_neuron.consensus}" bonds = await async_subtensor.subnets.bonds(alice_sn.netuid) - assert bonds == [ - ( - 0, - [ - (0, 65535), - (1, 65535), - ], - ), - ( - 1, - [], - ), - ] + assert len(bonds) == 2, f"bonds={bonds}" + assert bonds[0][0] == 0, f"bonds={bonds}" + assert len(bonds[0][1]) == 1, f"bonds={bonds}" + assert bonds[0][1][0][0] == 1, f"bonds={bonds}" + assert bonds[0][1][0][1] > 0, f"bonds={bonds}" + assert bonds[1] == (1, []), f"bonds={bonds}" break - except Exception: + except Exception as e: + last_error = e await async_subtensor.wait_for_block(await async_subtensor.block) continue + else: + pytest.fail(f"Neuron metrics did not reach expected values: {last_error}") diff --git a/tests/e2e_tests/test_metagraph.py b/tests/e2e_tests/test_metagraph.py index 5c337eac02..b5d58bb37c 100644 --- a/tests/e2e_tests/test_metagraph.py +++ b/tests/e2e_tests/test_metagraph.py @@ -1,6 +1,7 @@ import os.path import shutil import time +from dataclasses import replace import numpy as np import pytest @@ -12,8 +13,7 @@ ) from bittensor.utils.balance import Balance from bittensor.utils.btlogging import logging -from bittensor.utils.registration.pow import LazyLoadedTorch -from bittensor.utils.weight_utils import convert_and_normalize_weights_and_uids +from bittensor.utils.registration.torch_utils import LazyLoadedTorch from tests.e2e_tests.utils import ( AdminUtils, NETUID, @@ -27,6 +27,12 @@ NULL_KEY = tuple(bytearray(32)) +def _strip_unique_fields_for_metagraph_parity(m: MetagraphInfo) -> MetagraphInfo: + # get_metagraph_info (selective mechagraph) contains validators/commitments; + # get_all_metagraphs_info does not get these fields from the chain. + return replace(m, validators=None, commitments=None) + + torch = LazyLoadedTorch() @@ -665,7 +671,7 @@ def test_metagraph_info(subtensor, alice_wallet, bob_wallet): hotkeys=["5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM"], coldkeys=["5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM"], identities=[None], - axons=( + axons=[ { "block": 0, "version": 0, @@ -676,18 +682,18 @@ def test_metagraph_info(subtensor, alice_wallet, bob_wallet): "placeholder1": 0, "placeholder2": 0, }, - ), - active=(True,), - validator_permit=(False,), - pruning_score=[0.0], - last_update=(0,), + ], + active=[True], + validator_permit=[False], + pruning_score=[], + last_update=[0], emission=[Balance(0).set_unit(1)], dividends=[0.0], incentives=[0.0], consensus=[0.0], - trust=[0.0], - rank=[0.0], - block_at_registration=(0,), + trust=[], + rank=[], + block_at_registration=[0], alpha_stake=[Balance.from_tao(1.0).set_unit(1)], tao_stake=[Balance(0)], total_stake=[Balance.from_tao(1.0).set_unit(1)], @@ -762,19 +768,19 @@ def test_metagraph_info(subtensor, alice_wallet, bob_wallet): bonds_moving_avg=4.87890977618477e-14, hotkeys=[], coldkeys=[], - identities={}, - axons=(), - active=(), - validator_permit=(), + identities=[], + axons=[], + active=[], + validator_permit=[], pruning_score=[], - last_update=(), + last_update=[], emission=[], dividends=[], incentives=[], consensus=[], trust=[], rank=[], - block_at_registration=(), + block_at_registration=[], alpha_stake=[], tao_stake=[], total_stake=[], @@ -833,7 +839,9 @@ def test_metagraph_info(subtensor, alice_wallet, bob_wallet): metagraph_infos = subtensor.metagraphs.get_all_metagraphs_info(block=block) assert len(metagraph_infos) == 4 - assert metagraph_infos[-1] == metagraph_info + assert metagraph_infos[-1] == _strip_unique_fields_for_metagraph_parity( + metagraph_info + ) # non-existed subnet metagraph_info = subtensor.metagraphs.get_metagraph_info(netuid=bob_sn.netuid + 1) @@ -914,7 +922,7 @@ async def test_metagraph_info_async(async_subtensor, alice_wallet, bob_wallet): hotkeys=["5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM"], coldkeys=["5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM"], identities=[None], - axons=( + axons=[ { "block": 0, "version": 0, @@ -925,18 +933,18 @@ async def test_metagraph_info_async(async_subtensor, alice_wallet, bob_wallet): "placeholder1": 0, "placeholder2": 0, }, - ), - active=(True,), - validator_permit=(False,), - pruning_score=[0.0], - last_update=(0,), + ], + active=[True], + validator_permit=[False], + pruning_score=[], + last_update=[0], emission=[Balance(0).set_unit(1)], dividends=[0.0], incentives=[0.0], consensus=[0.0], - trust=[0.0], - rank=[0.0], - block_at_registration=(0,), + trust=[], + rank=[], + block_at_registration=[0], alpha_stake=[Balance.from_tao(1.0).set_unit(1)], tao_stake=[Balance(0)], total_stake=[Balance.from_tao(1.0).set_unit(1)], @@ -1011,19 +1019,19 @@ async def test_metagraph_info_async(async_subtensor, alice_wallet, bob_wallet): bonds_moving_avg=4.87890977618477e-14, hotkeys=[], coldkeys=[], - identities={}, - axons=(), - active=(), - validator_permit=(), + identities=[], + axons=[], + active=[], + validator_permit=[], pruning_score=[], - last_update=(), + last_update=[], emission=[], dividends=[], incentives=[], consensus=[], trust=[], rank=[], - block_at_registration=(), + block_at_registration=[], alpha_stake=[], tao_stake=[], total_stake=[], @@ -1088,7 +1096,9 @@ async def test_metagraph_info_async(async_subtensor, alice_wallet, bob_wallet): ) assert len(metagraph_infos) == 4 - assert metagraph_infos[-1] == metagraph_info + assert metagraph_infos[-1] == _strip_unique_fields_for_metagraph_parity( + metagraph_info + ) # non-existed subnet metagraph_info = await async_subtensor.metagraphs.get_metagraph_info( @@ -1127,8 +1137,8 @@ def test_metagraph_info_with_indexes(subtensor, alice_wallet, bob_wallet): name="omron", owner_hotkey=alice_wallet.hotkey.ss58_address, owner_coldkey=alice_wallet.coldkey.ss58_address, - active=(True,), - axons=( + active=[True], + axons=[ { "block": 0, "ip": 0, @@ -1139,7 +1149,7 @@ def test_metagraph_info_with_indexes(subtensor, alice_wallet, bob_wallet): "protocol": 0, "version": 0, }, - ), + ], symbol=None, identity=None, network_registered_at=None, @@ -1230,8 +1240,8 @@ def test_metagraph_info_with_indexes(subtensor, alice_wallet, bob_wallet): name="omron", owner_hotkey=alice_wallet.hotkey.ss58_address, owner_coldkey=alice_wallet.coldkey.ss58_address, - active=(True, True), - axons=( + active=[True, True], + axons=[ { "block": 0, "ip": 0, @@ -1252,7 +1262,7 @@ def test_metagraph_info_with_indexes(subtensor, alice_wallet, bob_wallet): "protocol": 0, "version": 0, }, - ), + ], symbol=None, identity=None, network_registered_at=None, @@ -1356,8 +1366,8 @@ async def test_metagraph_info_with_indexes_async( name="omron", owner_hotkey=alice_wallet.hotkey.ss58_address, owner_coldkey=alice_wallet.coldkey.ss58_address, - active=(True,), - axons=( + active=[True], + axons=[ { "block": 0, "ip": 0, @@ -1368,7 +1378,7 @@ async def test_metagraph_info_with_indexes_async( "protocol": 0, "version": 0, }, - ), + ], symbol=None, identity=None, network_registered_at=None, @@ -1461,8 +1471,8 @@ async def test_metagraph_info_with_indexes_async( name="omron", owner_hotkey=alice_wallet.hotkey.ss58_address, owner_coldkey=alice_wallet.coldkey.ss58_address, - active=(True, True), - axons=( + active=[True, True], + axons=[ { "block": 0, "ip": 0, @@ -1483,7 +1493,7 @@ async def test_metagraph_info_with_indexes_async( "protocol": 0, "version": 0, }, - ), + ], symbol=None, identity=None, network_registered_at=None, diff --git a/tests/e2e_tests/test_register_limit.py b/tests/e2e_tests/test_register_limit.py new file mode 100644 index 0000000000..cfcb8b4090 --- /dev/null +++ b/tests/e2e_tests/test_register_limit.py @@ -0,0 +1,142 @@ +import pytest + +from bittensor.utils.balance import Balance +from bittensor.utils.btlogging import logging +from tests.e2e_tests.utils import ( + TestSubnet, + ACTIVATE_SUBNET, + REGISTER_SUBNET, +) + + +def test_register_limit_success(subtensor, alice_wallet, bob_wallet): + """Tests successful registration with register_limit when limit_price is above burn.""" + alice_sn = TestSubnet(subtensor) + alice_sn.execute_steps( + [ + REGISTER_SUBNET(alice_wallet), + ACTIVATE_SUBNET(alice_wallet), + ] + ) + + recycle = subtensor.subnets.recycle(alice_sn.netuid) + assert recycle is not None, ( + "Recycle amount should not be None after subnet activation" + ) + + limit_price = recycle * 2 + logging.console.info( + f"Registering Bob with register_limit on SN #{alice_sn.netuid}, " + f"recycle={recycle}, limit_price={limit_price}" + ) + result = subtensor.subnets.register_limit(bob_wallet, alice_sn.netuid, limit_price) + assert result.success, "register_limit should succeed with limit above burn price" + + +def test_register_limit_price_exceeded(subtensor, alice_wallet, bob_wallet): + """Tests that register_limit fails when limit_price is below the current burn price.""" + alice_sn = TestSubnet(subtensor) + alice_sn.execute_steps( + [ + REGISTER_SUBNET(alice_wallet), + ACTIVATE_SUBNET(alice_wallet), + ] + ) + + logging.console.info( + f"Attempting register_limit with limit_price=1 on SN #{alice_sn.netuid}" + ) + result = subtensor.subnets.register_limit( + bob_wallet, alice_sn.netuid, limit_price=Balance.from_rao(1) + ) + assert not result.success, ( + "register_limit should fail with limit_price=1 (below burn)" + ) + + +@pytest.mark.asyncio +async def test_register_limit_success_async(async_subtensor, alice_wallet, bob_wallet): + """Tests successful async registration with register_limit when limit_price is above burn.""" + alice_sn = TestSubnet(async_subtensor) + await alice_sn.async_execute_steps( + [ + REGISTER_SUBNET(alice_wallet), + ACTIVATE_SUBNET(alice_wallet), + ] + ) + + recycle = await async_subtensor.subnets.recycle(alice_sn.netuid) + assert recycle is not None, ( + "Recycle amount should not be None after subnet activation" + ) + + limit_price = recycle * 2 + logging.console.info( + f"Registering Bob with register_limit on SN #{alice_sn.netuid}, " + f"recycle={recycle}, limit_price={limit_price}" + ) + result = await async_subtensor.subnets.register_limit( + bob_wallet, alice_sn.netuid, limit_price + ) + assert result.success, "register_limit should succeed with limit above burn price" + + +@pytest.mark.asyncio +async def test_register_limit_price_exceeded_async( + async_subtensor, alice_wallet, bob_wallet +): + """Tests that async register_limit fails when limit_price is below the current burn price.""" + alice_sn = TestSubnet(async_subtensor) + await alice_sn.async_execute_steps( + [ + REGISTER_SUBNET(alice_wallet), + ACTIVATE_SUBNET(alice_wallet), + ] + ) + + logging.console.info( + f"Attempting register_limit with limit_price=1 on SN #{alice_sn.netuid}" + ) + result = await async_subtensor.subnets.register_limit( + bob_wallet, alice_sn.netuid, limit_price=Balance.from_rao(1) + ) + assert not result.success, ( + "register_limit should fail with limit_price=1 (below burn)" + ) + + +def test_register_auto_limit_price(subtensor, alice_wallet, bob_wallet): + """Tests successful registration via register() with auto-calculated limit_price.""" + alice_sn = TestSubnet(subtensor) + alice_sn.execute_steps( + [ + REGISTER_SUBNET(alice_wallet), + ACTIVATE_SUBNET(alice_wallet), + ] + ) + + logging.console.info( + f"Registering Bob with register (auto limit_price) on SN #{alice_sn.netuid}" + ) + result = subtensor.subnets.register(bob_wallet, alice_sn.netuid) + assert result.success, "register should succeed with auto-calculated limit_price" + + +@pytest.mark.asyncio +async def test_register_auto_limit_price_async( + async_subtensor, alice_wallet, bob_wallet +): + """Tests successful async registration via register() with auto-calculated limit_price.""" + alice_sn = TestSubnet(async_subtensor) + await alice_sn.async_execute_steps( + [ + REGISTER_SUBNET(alice_wallet), + ACTIVATE_SUBNET(alice_wallet), + ] + ) + + logging.console.info( + f"Registering Bob with register (auto limit_price) on SN #{alice_sn.netuid}" + ) + result = await async_subtensor.subnets.register(bob_wallet, alice_sn.netuid) + assert result.success, "register should succeed with auto-calculated limit_price" diff --git a/tests/e2e_tests/test_root_claim.py b/tests/e2e_tests/test_root_claim.py index 012a0858ac..fa541d6d70 100644 --- a/tests/e2e_tests/test_root_claim.py +++ b/tests/e2e_tests/test_root_claim.py @@ -382,7 +382,9 @@ def test_root_claim_keep_with_zero_num_root_auto_claims( hotkey_ss58=alice_wallet.hotkey.ss58_address, netuid=sn2.netuid, ) - assert stake_after_charlie >= claimable_stake_before_charlie - Balance.from_rao(1, sn2.netuid) + assert stake_after_charlie >= claimable_stake_before_charlie - Balance.from_rao( + 1, sn2.netuid + ) logging.console.info(f"[blue]Charlie after:[/blue]") logging.console.info(f"RootClaimed: {claimed_after_charlie}") @@ -559,7 +561,9 @@ async def test_root_claim_keep_with_zero_num_root_auto_claims_async( hotkey_ss58=alice_wallet.hotkey.ss58_address, netuid=sn2.netuid, ) - assert stake_after_charlie >= claimable_stake_before_charlie - Balance.from_rao(1, sn2.netuid) + assert stake_after_charlie >= claimable_stake_before_charlie - Balance.from_rao( + 1, sn2.netuid + ) logging.console.info(f"[blue]Charlie after:[/blue]") logging.console.info(f"RootClaimed: {claimed_after_charlie}") diff --git a/tests/e2e_tests/test_staking.py b/tests/e2e_tests/test_staking.py index f0010f234f..5d571249b5 100644 --- a/tests/e2e_tests/test_staking.py +++ b/tests/e2e_tests/test_staking.py @@ -20,6 +20,32 @@ from tests.helpers.helpers import CloseInValue +def _get_expected_balance_after_neuron_registrations( + initial_balance: Balance, sns: list[TestSubnet] +) -> Balance: + """ + Get the expected balance after neuron registrations + by subtracting the total registration cost from the initial balance. + """ + total_registration_cost = Balance.from_rao(0) + + for sn in sns: + for call in sn.calls: + if call.operation != REGISTER_NEURON.__name__: + continue + + balance_before = call.response.data.get("balance_before") + balance_after = call.response.data.get("balance_after") + + assert balance_before is not None and balance_after is not None, ( + "REGISTER_NEURON response missing balance data." + ) + + total_registration_cost += balance_before - balance_after + + return initial_balance - total_registration_cost + + def test_single_operation(subtensor, alice_wallet, bob_wallet): """ Tests: @@ -334,6 +360,9 @@ def test_batch_operations(subtensor, alice_wallet, bob_wallet): - Checks Accounts Balance """ subnets_tested = 2 + bob_balance_before_setup = subtensor.wallets.get_balance( + bob_wallet.coldkey.ss58_address + ) sns = [TestSubnet(subtensor) for _ in range(subnets_tested)] @@ -357,6 +386,9 @@ def test_batch_operations(subtensor, alice_wallet, bob_wallet): ) netuids = [sn.netuid for sn in sns] + expected_bob_balance = _get_expected_balance_after_neuron_registrations( + bob_balance_before_setup, sns + ) balances = subtensor.wallets.get_balances( alice_wallet.coldkey.ss58_address, @@ -443,10 +475,7 @@ def test_batch_operations(subtensor, alice_wallet, bob_wallet): bob_wallet.coldkey.ss58_address, ) - assert CloseInValue( # Make sure we are within 0.0002 TAO due to tx fees - balances[bob_wallet.coldkey.ss58_address], Balance.from_rao(5_000_000) - ) == Balance.from_tao(999_999.7979) - + assert balances[bob_wallet.coldkey.ss58_address] == expected_bob_balance assert balances[alice_wallet.coldkey.ss58_address] > alice_balance @@ -460,6 +489,9 @@ async def test_batch_operations_async(async_subtensor, alice_wallet, bob_wallet) - Checks Accounts Balance """ subnets_tested = 2 + bob_balance_before_setup = await async_subtensor.wallets.get_balance( + bob_wallet.coldkey.ss58_address + ) sns = [TestSubnet(async_subtensor) for _ in range(subnets_tested)] @@ -483,6 +515,9 @@ async def test_batch_operations_async(async_subtensor, alice_wallet, bob_wallet) ) netuids = [sn.netuid for sn in sns] + expected_bob_balance = _get_expected_balance_after_neuron_registrations( + bob_balance_before_setup, sns + ) balances = await async_subtensor.wallets.get_balances( alice_wallet.coldkey.ss58_address, @@ -569,10 +604,7 @@ async def test_batch_operations_async(async_subtensor, alice_wallet, bob_wallet) bob_wallet.coldkey.ss58_address, ) - assert CloseInValue( # Make sure we are within 0.0002 TAO due to tx fees - balances[bob_wallet.coldkey.ss58_address], Balance.from_rao(5_000_000) - ) == Balance.from_tao(999_999.7979) - + assert balances[bob_wallet.coldkey.ss58_address] == expected_bob_balance assert balances[alice_wallet.coldkey.ss58_address] > alice_balance diff --git a/tests/e2e_tests/test_subnets.py b/tests/e2e_tests/test_subnets.py index 7079073d82..5368988946 100644 --- a/tests/e2e_tests/test_subnets.py +++ b/tests/e2e_tests/test_subnets.py @@ -1,5 +1,4 @@ import pytest -from bittensor.utils.btlogging import logging def test_subnets(subtensor, alice_wallet): diff --git a/tests/e2e_tests/test_subtensor_functions.py b/tests/e2e_tests/test_subtensor_functions.py index 1783a44e56..cec1d35884 100644 --- a/tests/e2e_tests/test_subtensor_functions.py +++ b/tests/e2e_tests/test_subtensor_functions.py @@ -15,6 +15,7 @@ ACTIVATE_SUBNET, REGISTER_NEURON, ) +from tests.helpers import CloseInValue """ Verifies: @@ -142,12 +143,9 @@ def test_subtensor_extrinsics(subtensor, templates, alice_wallet, bob_wallet): bob_balance = subtensor.wallets.get_balance(bob_wallet.coldkeypub.ss58_address) - alice_sn.execute_steps( - [ - ACTIVATE_SUBNET(alice_wallet), - REGISTER_NEURON(bob_wallet), - ] - ) + alice_sn.execute_one(ACTIVATE_SUBNET(alice_wallet)) + recycle_amount = subtensor.subnets.recycle(netuid) + reg_response = alice_sn.execute_one(REGISTER_NEURON(bob_wallet)) # Verify Bob's UID on netuid 2 is 1 assert ( @@ -157,17 +155,16 @@ def test_subtensor_extrinsics(subtensor, templates, alice_wallet, bob_wallet): == 1 ), "UID for Bob's hotkey on netuid 2 is not 1 as expected" - # Fetch recycle_amount to register to the subnet - recycle_amount = subtensor.subnets.recycle(netuid) - fee = alice_sn.calls[-1].response.extrinsic_fee + fee = reg_response.extrinsic_fee bob_balance_post_reg = subtensor.wallets.get_balance( bob_wallet.coldkeypub.ss58_address ) - # Ensure recycled amount is only deducted from the balance after registration - assert bob_balance - recycle_amount - fee == bob_balance_post_reg, ( - "Balance for Bob is not correct after burned register" - ) + # Burn decays every block and bumps after registration; hence tolerance + expected_post = bob_balance - recycle_amount - fee + assert ( + CloseInValue(bob_balance_post_reg, Balance.from_tao(0.002)) == expected_post + ), "Balance for Bob is not correct after burned register" with templates.validator(alice_wallet, netuid): # wait for 5 seconds for the metagraph and subtensor to refresh with latest data @@ -311,12 +308,9 @@ async def test_subtensor_extrinsics_async( bob_wallet.coldkeypub.ss58_address ) - await alice_sn.async_execute_steps( - [ - ACTIVATE_SUBNET(alice_wallet), - REGISTER_NEURON(bob_wallet), - ] - ) + await alice_sn.async_execute_one(ACTIVATE_SUBNET(alice_wallet)) + recycle_amount = await async_subtensor.subnets.recycle(netuid) + reg_response = await alice_sn.async_execute_one(REGISTER_NEURON(bob_wallet)) # Verify Bob's UID on netuid 2 is 1 assert ( @@ -326,17 +320,16 @@ async def test_subtensor_extrinsics_async( == 1 ), "UID for Bob's hotkey on netuid 2 is not 1 as expected." - # Fetch recycle_amount to register to the subnet - recycle_amount = await async_subtensor.subnets.recycle(netuid) - fee = alice_sn.calls[-1].response.extrinsic_fee + fee = reg_response.extrinsic_fee bob_balance_post_reg = await async_subtensor.wallets.get_balance( bob_wallet.coldkeypub.ss58_address ) - # Ensure recycled amount is only deducted from the balance after registration - assert bob_balance - recycle_amount - fee == bob_balance_post_reg, ( - "Balance for Bob is not correct after burned register" - ) + expected_post = bob_balance - recycle_amount - fee + # Burn decays every block and bumps after registration; hence tolerance + assert ( + CloseInValue(bob_balance_post_reg, Balance.from_tao(0.005)) == expected_post + ), "Balance for Bob is not correct after burned register" # neuron_info_old = subtensor.get_neuron_for_pubkey_and_subnet( # alice_wallet.hotkey.ss58_address, netuid=netuid diff --git a/tests/e2e_tests/test_transfer.py b/tests/e2e_tests/test_transfer.py index b6859238e2..136fb98bba 100644 --- a/tests/e2e_tests/test_transfer.py +++ b/tests/e2e_tests/test_transfer.py @@ -4,10 +4,9 @@ import pytest from bittensor.utils.balance import Balance -from bittensor import logging if typing.TYPE_CHECKING: - from bittensor.extras import SubtensorApi + pass def test_transfer(subtensor, alice_wallet): diff --git a/tests/e2e_tests/utils/e2e_test_utils.py b/tests/e2e_tests/utils/e2e_test_utils.py index 1f24643d1f..2e7239bffe 100644 --- a/tests/e2e_tests/utils/e2e_test_utils.py +++ b/tests/e2e_tests/utils/e2e_test_utils.py @@ -6,7 +6,6 @@ from typing import Optional from bittensor_wallet import Keypair, Wallet -from bittensor.extras import SubtensorApi from bittensor.utils.btlogging import logging template_path = os.getcwd() + "/neurons/" @@ -64,7 +63,7 @@ def clone_or_update_templates(specific_commit=None): """ install_dir = template_path repo_mapping = { - templates_repo: "https://github.com/opentensor/subnet-template.git", + templates_repo: "https://github.com/latent-to/subnet-template.git", } cwd = os.getcwd() diff --git a/tests/helpers/integration_websocket_data.py b/tests/helpers/integration_websocket_data.py index 307436fd86..a83492dc03 100644 --- a/tests/helpers/integration_websocket_data.py +++ b/tests/helpers/integration_websocket_data.py @@ -12427,4 +12427,119 @@ } }, }, + "decode_crowdloan_entry": { + "chain_getHead": { + "[]": { + "result": "0x8563f9d378caf2943368320b79ab5a90ec51ffb2ba7ab23002a811819297316d" + } + }, + "chain_getHeader": { + '["0xe0abc804366b36f9293c2a00fbc7a911c19247885f39a99a20f5c06389a4093e"]': { + "result": { + "parentHash": "0xf287b6afbb10dd480373d62a6d47f2b8f7c0419cc6ac5d61ba0c0b7f189d17ba", + "number": "0x79ccc2", + "stateRoot": "0x3247e495b19698fae0064a9bc61c7723d28adbb8c6806f8605ec7bab74bde028", + "extrinsicsRoot": "0xb79368ee28d3ca69081559d11928b9c718d4a9df865e2e49861ecd9d64e67b41", + "digest": { + "logs": [ + "0x066175726120c6c1d20800000000", + "0x0466726f6e890201f2206a7ea2500ddf7de73fb26e223298f68422086014b3a76e93b43af11fd8c410a39ff5f50fa3b06912db770e3dc5345aeb4e8e1811918362aa52268c2d508411d7058ab4599ff651cc7af3e46d62305f8cc0fc7cfe9010cad57b0456df620d42858bd66a675981f36caab57b5d45466a0dcddfd0447a83231dfdf38c70a34dc6247105ed2a0dfb4c02614ce6b405fd600cfe7a3f5d4dd578a8e0f87c712fec81", + "0x0561757261010114610206165bf247c62abbec8218aadf695afe1df9894195795d510eae5a2b1241a93d5e8f8262a6bb6a4d6d1eacd408609b2d925d770adbfa47f78f7efb0985", + ] + }, + } + }, + '["0x8563f9d378caf2943368320b79ab5a90ec51ffb2ba7ab23002a811819297316d"]': { + "result": { + "parentHash": "0xe0abc804366b36f9293c2a00fbc7a911c19247885f39a99a20f5c06389a4093e", + "number": "0x79ccc3", + "stateRoot": "0xf79f46321b59ee29fdb69327bf18e3d6bb7886ffdb972ba4a862a666c4d5ced6", + "extrinsicsRoot": "0xd4b2304c575e1d6d0a1e2baf6d7a1288415121e0b30d37319d182e95358565cf", + "digest": { + "logs": [ + "0x066175726120c7c1d20800000000", + "0x0466726f6e890201a781ed2bf8f8c5a308a7c69e1a2c1668c07fa1fd6105f57df141b282f34fb6b510a2973c5c0c1949c76e622fab9f33c8a31e63a16773ae1b7917babc0fc916304483ecd3e5ba88ad18abbee85b43ab2952d3ee8823dc7357db4cb2c20f2612b4fc29e6837aa829a90e91b78a072a91208f66b06d388347cc250e1475b9470f3c71bb26aa52583dca3f6f258fe1440fdac6044c6e6e3c3971c5e3cabbc75cb86790", + "0x05617572610101e059e901febcad1547d1f1809f6f663f56e6e14e2eac77e6899387b43ade5b023f0c4fc1bc7153be4c725b6c34fae5397ac994980550638ea480b0095294f08d", + ] + }, + } + }, + }, + "state_getRuntimeVersion": { + '["0xf287b6afbb10dd480373d62a6d47f2b8f7c0419cc6ac5d61ba0c0b7f189d17ba"]': { + "result": { + "specName": "node-subtensor", + "implName": "node-subtensor", + "authoringVersion": 1, + "specVersion": 393, + "implVersion": 1, + "apis": [ + ["0xdf6acb689907609b", 5], + ["0x37e397fc7c91f5e4", 2], + ["0x40fe3ad401f8959a", 6], + ["0xfbc577b9d747efd6", 1], + ["0xd2bc9897eed08f15", 3], + ["0xf78b278be53f454c", 2], + ["0xdd718d5cc53262d4", 1], + ["0xab3c0572291feb8b", 1], + ["0xed99c5acb25eedf5", 3], + ["0xbc9d89904f5b923f", 1], + ["0x37c8bb1350a9a2a8", 4], + ["0xf3ff14d5ab527059", 3], + ["0x582211f65bb14b89", 6], + ["0xe65b00e46cedd0aa", 2], + ["0x68b66ba122c93fa7", 2], + ["0x42e62be4a39e5b60", 1], + ["0x806df4ccaa9ed485", 1], + ["0x8375104b299b74c5", 1], + ["0x5d1fbfbe852f2807", 1], + ["0xc6886e2f8e598b0a", 1], + ["0xcbca25e39f142387", 2], + ["0xa8b093e6508d9e9c", 1], + ["0x1c4585bd5c707202", 1], + ], + "transactionVersion": 1, + "systemVersion": 1, + "stateVersion": 1, + } + }, + '["0xe0abc804366b36f9293c2a00fbc7a911c19247885f39a99a20f5c06389a4093e"]': { + "result": { + "specName": "node-subtensor", + "implName": "node-subtensor", + "authoringVersion": 1, + "specVersion": 393, + "implVersion": 1, + "apis": [ + ["0xdf6acb689907609b", 5], + ["0x37e397fc7c91f5e4", 2], + ["0x40fe3ad401f8959a", 6], + ["0xfbc577b9d747efd6", 1], + ["0xd2bc9897eed08f15", 3], + ["0xf78b278be53f454c", 2], + ["0xdd718d5cc53262d4", 1], + ["0xab3c0572291feb8b", 1], + ["0xed99c5acb25eedf5", 3], + ["0xbc9d89904f5b923f", 1], + ["0x37c8bb1350a9a2a8", 4], + ["0xf3ff14d5ab527059", 3], + ["0x582211f65bb14b89", 6], + ["0xe65b00e46cedd0aa", 2], + ["0x68b66ba122c93fa7", 2], + ["0x42e62be4a39e5b60", 1], + ["0x806df4ccaa9ed485", 1], + ["0x8375104b299b74c5", 1], + ["0x5d1fbfbe852f2807", 1], + ["0xc6886e2f8e598b0a", 1], + ["0xcbca25e39f142387", 2], + ["0xa8b093e6508d9e9c", 1], + ["0x1c4585bd5c707202", 1], + ], + "transactionVersion": 1, + "systemVersion": 1, + "stateVersion": 1, + } + }, + }, + }, } diff --git a/tests/integration_tests/test_subtensor_integration.py b/tests/integration_tests/test_subtensor_integration.py index 527076f07b..8f3708a3b0 100644 --- a/tests/integration_tests/test_subtensor_integration.py +++ b/tests/integration_tests/test_subtensor_integration.py @@ -1,7 +1,8 @@ import pytest -from bittensor.core.chain_data import AxonInfo, NeuronInfo +from bittensor.core.chain_data import AxonInfo, NeuronInfo, CrowdloanInfo from bittensor.core.subtensor import Subtensor +from bittensor.core.types import CrowdloansResponse from bittensor.utils.balance import Balance from tests.helpers.helpers import FakeWebsocket from bittensor.utils.mock.subtensor_mock import MockSubtensor @@ -161,3 +162,54 @@ async def test_archive_node_retry(mocker): current_block = subtensor.substrate.get_block_number() old_block = current_block - 1000 assert isinstance((subtensor.substrate.get_block(block_number=old_block)), dict) + + +@pytest.mark.asyncio +async def test_decode_crowdloan_entry(mocker): + entry = CrowdloansResponse( + **{ + "creator": "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY", + "deposit": 10000000000, + "min_contribution": 1000000000, + "end": 10055, + "cap": 100000000000, + "funds_account": "5EYCAe5fvncWtwXjyNBBHFPvNVDH5LPQ2harKS7KdAGbezkb", + "raised": 10000000000, + "target_address": None, + "call": { + "Inline": "0x0500008eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a4802286bee" + }, + "finalized": False, + "contributors_count": 1, + } + ) + subtensor = await prepare_test(mocker, "decode_crowdloan_entry") + actual = subtensor._decode_crowdloan_entry(17, entry) + expected = CrowdloanInfo( + id=17, + creator="5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY", + deposit=Balance.from_tao(10.0), + min_contribution=Balance.from_tao(1.0), + end=10055, + cap=Balance.from_tao(100.0), + funds_account="5EYCAe5fvncWtwXjyNBBHFPvNVDH5LPQ2harKS7KdAGbezkb", + raised=Balance.from_tao(10.0), + target_address=None, + call={ + "call_index": "0x0500", + "call_function": "transfer_allow_death", + "call_module": "Balances", + "call_args": [ + { + "name": "dest", + "type": "AccountIdLookupOf", + "value": "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty", + }, + {"name": "value", "type": "Balance", "value": 1000000000}, + ], + "call_hash": "0x117349ae93488150fa503b1ff7a0a94bfaa3ba193950a3d812ce32b9bb69fb02", + }, + finalized=False, + contributors_count=1, + ) + assert actual == expected diff --git a/tests/unit_tests/chain_data/test_coldkey_swap.py b/tests/unit_tests/chain_data/test_coldkey_swap.py index e13e245af6..f5a2aa33b6 100644 --- a/tests/unit_tests/chain_data/test_coldkey_swap.py +++ b/tests/unit_tests/chain_data/test_coldkey_swap.py @@ -1,4 +1,4 @@ -from async_substrate_interface.types import ScaleObj +from scalecodec.base import ScaleType from bittensor.core.chain_data.coldkey_swap import ( ColdkeySwapAnnouncementInfo, @@ -10,7 +10,7 @@ def test_coldkey_swap_announcement_info_from_query_none(mocker): """Test from_query returns None when query has no value.""" # Prep coldkey_ss58 = mocker.Mock(spec=str) - query = mocker.Mock(spec=ScaleObj) + query = mocker.Mock(spec=ScaleType, value=None) # Call from_query = ColdkeySwapAnnouncementInfo.from_query(coldkey_ss58, query) @@ -24,29 +24,23 @@ def test_coldkey_swap_announcement_info_from_query_happy_path(mocker): # Prep coldkey_ss58 = mocker.Mock(spec=str) fake_block = mocker.Mock(spec=int) - fake_hash_data = mocker.Mock(spec=list) - query = mocker.Mock(value=(fake_block, (fake_hash_data,))) - - mocked_bytes = mocker.patch("bittensor.core.chain_data.coldkey_swap.bytes") + fake_hash_data = mocker.Mock(spec=str) + query = mocker.Mock(value=(fake_block, fake_hash_data)) # Call from_query = ColdkeySwapAnnouncementInfo.from_query(coldkey_ss58, query) # Asserts - mocked_bytes.assert_called_once_with(fake_hash_data) assert from_query is not None, "Should return ColdkeySwapAnnouncementInfo object" assert from_query.coldkey == coldkey_ss58 assert from_query.execution_block == fake_block - assert ( - from_query.new_coldkey_hash - == mocked_bytes.return_value.hex.return_value.__radd__.return_value - ) + assert from_query.new_coldkey_hash == fake_hash_data def test_coldkey_swap_dispute_info_from_query_none(mocker): """Test from_query returns None when query has no value.""" coldkey_ss58 = mocker.Mock(spec=str) - query = mocker.Mock(spec=ScaleObj) + query = mocker.Mock(spec=ScaleType) query.value = None from_query = ColdkeySwapDisputeInfo.from_query(coldkey_ss58, query) @@ -58,7 +52,7 @@ def test_coldkey_swap_dispute_info_from_query_happy_path(mocker): """Test from_query returns ColdkeySwapDisputeInfo when query has valid data.""" coldkey_ss58 = mocker.Mock(spec=str) fake_block = 12345 - query = mocker.Mock(spec=ScaleObj, value=fake_block) + query = mocker.Mock(spec=ScaleType, value=fake_block) from_query = ColdkeySwapDisputeInfo.from_query(coldkey_ss58, query) @@ -71,11 +65,7 @@ def test_coldkey_swap_dispute_info_from_record(mocker): """Test from_record returns ColdkeySwapDisputeInfo from query_map record.""" decoded_coldkey = "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY" disputed_block = 999 - record = (mocker.Mock(), mocker.Mock(value=disputed_block)) - mocker.patch( - "bittensor.core.chain_data.coldkey_swap.decode_account_id", - return_value=decoded_coldkey, - ) + record = (decoded_coldkey, disputed_block) from_record = ColdkeySwapDisputeInfo.from_record(record) diff --git a/tests/unit_tests/chain_data/test_utils.py b/tests/unit_tests/chain_data/test_utils.py index 602e03c192..8339cc71eb 100644 --- a/tests/unit_tests/chain_data/test_utils.py +++ b/tests/unit_tests/chain_data/test_utils.py @@ -8,93 +8,20 @@ [ ( { + "block": 5097676, "deposit": 0, - "block": 5415815, "info": { - "fields": ( - ( - { - "Raw64": ( - ( - 51, - 98, - 99, - 54, - 49, - 48, - 57, - 102, - 49, - 101, - 49, - 51, - 102, - 102, - 56, - 102, - 55, - 101, - 98, - 54, - 97, - 102, - 54, - 49, - 53, - 101, - 49, - 102, - 56, - 101, - 49, - 55, - 99, - 57, - 97, - 100, - 100, - 48, - 97, - 50, - 56, - 98, - 99, - 48, - 50, - 54, - 55, - 57, - 52, - 99, - 56, - 54, - 97, - 101, - 50, - 56, - 57, - 57, - 50, - 99, - 102, - 48, - 52, - 53, - ), - ) - }, - ), - ) + "fields": [ + { + "Raw97": "0x7b27706565725f6964273a2027313244334b6f6f57524e7735344157347a725157655a4c32627568553850373167666f3950585151414855774541653468413334272c20276d6f64656c5f68756767696e67666163655f6964273a204e6f6e657d" + } + ] }, }, - "3bc6109f1e13ff8f7eb6af615e1f8e17c9add0a28bc026794c86ae28992cf045", + "{'peer_id': '12D3KooWRNw54AW4zrQWeZL2buhU8P71gfo9PXQQAHUwEAe4hA34', 'model_huggingface_id': None}", ), ( - { - "deposit": 0, - "block": 5866237, - "info": {"fields": (({"ResetBondsFlag": ()},),)}, - }, + {"block": 6161535, "deposit": 0, "info": {"fields": ["ResetBondsFlag"]}}, "", ), ], diff --git a/tests/unit_tests/extrinsics/asyncex/test_coldkey_swap.py b/tests/unit_tests/extrinsics/asyncex/test_coldkey_swap.py index 7307f9a4bf..6611134d52 100644 --- a/tests/unit_tests/extrinsics/asyncex/test_coldkey_swap.py +++ b/tests/unit_tests/extrinsics/asyncex/test_coldkey_swap.py @@ -1,10 +1,7 @@ import pytest from bittensor_wallet import Wallet -from scalecodec.types import GenericCall from bittensor.core.extrinsics.asyncex import coldkey_swap -from bittensor.core.extrinsics.pallets import SubtensorModule -from bittensor.core.settings import DEFAULT_MEV_PROTECTION from bittensor.core.types import ExtrinsicResponse from bittensor.core.chain_data.coldkey_swap import ColdkeySwapAnnouncementInfo @@ -22,6 +19,9 @@ async def test_announce_coldkey_swap_extrinsic(subtensor, mocker): "unlock_wallet", return_value=ExtrinsicResponse(success=True, message="Unlocked"), ) + mocked_get_staking_hotkeys = mocker.patch.object( + subtensor, "get_staking_hotkeys", new=mocker.AsyncMock(return_value=[]) + ) mocked_keypair = mocker.patch( "bittensor.core.extrinsics.asyncex.coldkey_swap.Keypair" ) @@ -55,6 +55,7 @@ async def test_announce_coldkey_swap_extrinsic(subtensor, mocker): # Asserts mocked_unlock_wallet.assert_called_once_with(wallet, False) + mocked_get_staking_hotkeys.assert_awaited_once_with(new_coldkey_ss58) mocked_keypair.assert_called_once_with(ss58_address=new_coldkey_ss58) mocked_compute_hash.assert_called_once_with(mocked_keypair_instance) mocked_subtensor_module.assert_called_once_with(subtensor) @@ -85,6 +86,9 @@ async def test_announce_coldkey_swap_extrinsic_with_mev_protection(subtensor, mo "unlock_wallet", return_value=ExtrinsicResponse(success=True, message="Unlocked"), ) + mocked_get_staking_hotkeys = mocker.patch.object( + subtensor, "get_staking_hotkeys", new=mocker.AsyncMock(return_value=[]) + ) mocked_keypair = mocker.patch( "bittensor.core.extrinsics.asyncex.coldkey_swap.Keypair" ) @@ -118,6 +122,7 @@ async def test_announce_coldkey_swap_extrinsic_with_mev_protection(subtensor, mo # Asserts mocked_unlock_wallet.assert_called_once_with(wallet, False) + mocked_get_staking_hotkeys.assert_awaited_once_with(new_coldkey_ss58) mocked_subtensor_module.assert_called_once_with(subtensor) mocked_pallet_instance.announce_coldkey_swap.assert_awaited_once_with( new_coldkey_hash="0x" + "00" * 32 diff --git a/tests/unit_tests/extrinsics/asyncex/test_registration.py b/tests/unit_tests/extrinsics/asyncex/test_registration.py index 278d20c2c8..7e842b7173 100644 --- a/tests/unit_tests/extrinsics/asyncex/test_registration.py +++ b/tests/unit_tests/extrinsics/asyncex/test_registration.py @@ -1,309 +1,10 @@ import pytest from bittensor.core.types import ExtrinsicResponse +from bittensor.utils.balance import Balance from bittensor.core.extrinsics.asyncex import registration as async_registration -@pytest.mark.asyncio -async def test_register_extrinsic_success(subtensor, fake_wallet, mocker): - """Tests successful registration.""" - # Preps - fake_wallet.hotkey.ss58_address = "hotkey_ss58" - fake_wallet.coldkey.ss58_address = "coldkey_ss58" - - mocked_subnet_exists = mocker.patch.object( - subtensor, "subnet_exists", return_value=True - ) - mocked_get_neuron = mocker.patch.object( - subtensor, - "get_neuron_for_pubkey_and_subnet", - return_value=mocker.Mock(is_null=True), - ) - mocked_create_pow = mocker.patch.object( - async_registration, - "create_pow_async", - return_value=mocker.Mock( - is_stale_async=mocker.AsyncMock(return_value=False), seal=[] - ), - ) - mocked_compose_call = mocker.patch.object(subtensor, "compose_call") - mocked_sign_and_send_extrinsic = mocker.patch.object( - subtensor, "sign_and_send_extrinsic", return_value=ExtrinsicResponse(True, "") - ) - mocked_is_hotkey_registered = mocker.patch.object( - subtensor, "is_hotkey_registered", return_value=True - ) - - # Call - result = await async_registration.register_extrinsic( - subtensor=subtensor, - wallet=fake_wallet, - netuid=1, - wait_for_inclusion=True, - wait_for_finalization=True, - ) - - # Asserts - mocked_subnet_exists.assert_called_once_with( - 1, - block_hash=subtensor.substrate.get_chain_head.return_value, - ) - mocked_get_neuron.assert_called_once_with( - hotkey_ss58="hotkey_ss58", - netuid=1, - block_hash=subtensor.substrate.get_chain_head.return_value, - ) - mocked_create_pow.assert_called_once() - mocked_sign_and_send_extrinsic.assert_called_once_with( - call=mocked_compose_call.return_value, - wallet=fake_wallet, - wait_for_inclusion=True, - wait_for_finalization=True, - period=None, - raise_error=False, - ) - mocked_is_hotkey_registered.assert_called_once_with( - netuid=1, hotkey_ss58="hotkey_ss58" - ) - - assert result[0] - - -@pytest.mark.asyncio -async def test_register_extrinsic_success_with_cuda(subtensor, fake_wallet, mocker): - """Tests successful registration with CUDA enabled.""" - # Preps - fake_wallet.hotkey.ss58_address = "hotkey_ss58" - fake_wallet.coldkey.ss58_address = "coldkey_ss58" - - mocked_subnet_exists = mocker.patch.object( - subtensor, "subnet_exists", return_value=True - ) - mocked_get_neuron = mocker.patch.object( - subtensor, - "get_neuron_for_pubkey_and_subnet", - return_value=mocker.Mock(is_null=True), - ) - mocker.patch("torch.cuda.is_available", return_value=True) - mocked_create_pow = mocker.patch.object( - async_registration, - "create_pow_async", - return_value=mocker.Mock( - is_stale_async=mocker.AsyncMock(return_value=False), seal=[] - ), - ) - mocked_compose_call = mocker.patch.object(subtensor, "compose_call") - mocked_sign_and_send_extrinsic = mocker.patch.object( - subtensor, "sign_and_send_extrinsic", return_value=ExtrinsicResponse(True, "") - ) - mocked_is_hotkey_registered = mocker.patch.object( - subtensor, "is_hotkey_registered", return_value=True - ) - - # Call - result = await async_registration.register_extrinsic( - subtensor=subtensor, - wallet=fake_wallet, - netuid=1, - cuda=True, - wait_for_inclusion=True, - wait_for_finalization=True, - ) - - # Asserts - mocked_subnet_exists.assert_called_once_with( - 1, - block_hash=subtensor.substrate.get_chain_head.return_value, - ) - mocked_get_neuron.assert_called_once_with( - hotkey_ss58="hotkey_ss58", - netuid=1, - block_hash=subtensor.substrate.get_chain_head.return_value, - ) - mocked_create_pow.assert_called_once() - mocked_sign_and_send_extrinsic.assert_called_once_with( - call=mocked_compose_call.return_value, - wallet=fake_wallet, - wait_for_inclusion=True, - wait_for_finalization=True, - period=None, - raise_error=False, - ) - mocked_is_hotkey_registered.assert_called_once_with( - netuid=1, hotkey_ss58="hotkey_ss58" - ) - assert result[0] - - -@pytest.mark.asyncio -async def test_register_extrinsic_failed_with_cuda(subtensor, fake_wallet, mocker): - """Tests failed registration with CUDA enabled.""" - # Preps - fake_wallet.hotkey.ss58_address = "hotkey_ss58" - fake_wallet.coldkey.ss58_address = "coldkey_ss58" - - mocked_subnet_exists = mocker.patch.object( - subtensor, "subnet_exists", return_value=True - ) - mocked_get_neuron = mocker.patch.object( - subtensor, - "get_neuron_for_pubkey_and_subnet", - return_value=mocker.Mock(is_null=True), - ) - mocker.patch("torch.cuda.is_available", return_value=False) - - # Call - result = await async_registration.register_extrinsic( - subtensor=subtensor, - wallet=fake_wallet, - netuid=1, - cuda=True, - wait_for_inclusion=True, - wait_for_finalization=True, - ) - - # Asserts - mocked_subnet_exists.assert_called_once_with( - 1, - block_hash=subtensor.substrate.get_chain_head.return_value, - ) - mocked_get_neuron.assert_called_once_with( - hotkey_ss58="hotkey_ss58", - netuid=1, - block_hash=subtensor.substrate.get_chain_head.return_value, - ) - assert result == ExtrinsicResponse( - False, - "CUDA not available.", - extrinsic_function="register_extrinsic", - ) - - -@pytest.mark.asyncio -async def test_register_extrinsic_subnet_not_exists(subtensor, fake_wallet, mocker): - """Tests registration when subnet does not exist.""" - # Preps - netuid = 14 - mocked_subnet_exists = mocker.patch.object( - subtensor, "subnet_exists", return_value=False - ) - - # Call - result = await async_registration.register_extrinsic( - subtensor=subtensor, - wallet=fake_wallet, - netuid=netuid, - ) - - # Asserts - mocked_subnet_exists.assert_called_once_with( - netuid, - block_hash=subtensor.substrate.get_chain_head.return_value, - ) - assert result == ExtrinsicResponse( - False, - f"Subnet {netuid} does not exist.", - extrinsic_function="register_extrinsic", - ) - - -@pytest.mark.asyncio -async def test_register_extrinsic_already_registered(subtensor, fake_wallet, mocker): - """Tests registration when the key is already registered.""" - # Preps - netuid = 14 - mocked_get_neuron = mocker.patch.object( - subtensor, - "get_neuron_for_pubkey_and_subnet", - return_value=mocker.Mock(is_null=False), - ) - - # Call - success, message = await async_registration.register_extrinsic( - subtensor=subtensor, - wallet=fake_wallet, - netuid=netuid, - ) - - # Asserts - mocked_get_neuron.assert_called_once_with( - hotkey_ss58=fake_wallet.hotkey.ss58_address, - netuid=netuid, - block_hash=subtensor.substrate.get_chain_head.return_value, - ) - assert success is True - assert message == f"Already registered." - - -@pytest.mark.asyncio -async def test_register_extrinsic_max_attempts_reached(subtensor, fake_wallet, mocker): - # Preps - fake_wallet.hotkey.ss58_address = "hotkey_ss58" - fake_wallet.coldkey.ss58_address = "coldkey_ss58" - - stale_responses = iter([False, False, False, True]) - - async def is_stale_side_effect(*_, **__): - return next(stale_responses, True) - - fake_pow_result = mocker.Mock() - fake_pow_result.is_stale_async = mocker.AsyncMock(side_effect=is_stale_side_effect) - fake_pow_result.seal = [] - - mocked_subnet_exists = mocker.patch.object( - subtensor, "subnet_exists", return_value=True - ) - mocked_get_neuron = mocker.patch.object( - subtensor, - "get_neuron_for_pubkey_and_subnet", - return_value=mocker.Mock(is_null=True), - ) - mocked_create_pow = mocker.patch.object( - async_registration, - "create_pow_async", - return_value=fake_pow_result, - ) - mocked_compose_call = mocker.patch.object(subtensor, "compose_call") - mocked_sign_and_send_extrinsic = mocker.patch.object( - subtensor, - "sign_and_send_extrinsic", - return_value=ExtrinsicResponse(False, "Test Error"), - ) - - # Call - result = await async_registration.register_extrinsic( - subtensor=subtensor, - wallet=fake_wallet, - netuid=1, - max_allowed_attempts=3, - wait_for_inclusion=True, - wait_for_finalization=True, - ) - - # Asserts - mocked_subnet_exists.assert_called_once_with( - 1, - block_hash=subtensor.substrate.get_chain_head.return_value, - ) - mocked_get_neuron.assert_called_once_with( - hotkey_ss58="hotkey_ss58", - netuid=1, - block_hash=subtensor.substrate.get_chain_head.return_value, - ) - assert mocked_create_pow.call_count == 3 - assert mocked_sign_and_send_extrinsic.call_count == 3 - mocked_sign_and_send_extrinsic.assert_called_with( - call=mocked_compose_call.return_value, - wallet=fake_wallet, - wait_for_inclusion=True, - wait_for_finalization=True, - period=None, - raise_error=False, - ) - assert result[0] is False - assert result[1] == "No more attempts." - - @pytest.mark.asyncio async def test_set_subnet_identity_extrinsic_is_success(subtensor, fake_wallet, mocker): """Verify that set_subnet_identity_extrinsic calls the correct functions and returns the correct result.""" @@ -432,3 +133,58 @@ async def test_set_subnet_identity_extrinsic_is_failed(subtensor, fake_wallet, m ) assert result == mocked_sign_and_send_extrinsic.return_value + + +@pytest.mark.parametrize( + "subnet_exists, neuron_is_null, recycle_success, is_registered, expected_result, test_id", + [ + # Happy paths + (True, False, None, None, True, "neuron-not-null"), + (True, True, True, True, True, "happy-path-wallet-registered"), + # Error paths + (False, True, False, None, False, "subnet-non-existence"), + (True, True, False, False, False, "error-path-recycling-failed"), + (True, True, True, False, False, "error-path-not-registered"), + ], +) +@pytest.mark.asyncio +async def test_register_limit_extrinsic( + subtensor, + fake_wallet, + subnet_exists, + neuron_is_null, + recycle_success, + is_registered, + expected_result, + test_id, + mocker, +): + # Arrange + fake_wallet.hotkey.ss58_address = "hotkey_ss58" + fake_wallet.coldkeypub.ss58_address = "coldkey_ss58" + + mocker.patch.object(subtensor, "subnet_exists", return_value=subnet_exists) + mocker.patch.object( + subtensor, + "get_neuron_for_pubkey_and_subnet", + return_value=mocker.Mock(is_null=neuron_is_null), + ) + mocker.patch.object(subtensor, "get_balance", return_value=mocker.Mock()) + mocker.patch.object(subtensor, "recycle", return_value=mocker.Mock()) + mocker.patch.object(subtensor, "compose_call") + mocker.patch.object( + subtensor, + "sign_and_send_extrinsic", + return_value=ExtrinsicResponse(recycle_success, "Mock error message"), + ) + mocker.patch.object(subtensor, "is_hotkey_registered", return_value=is_registered) + + # Act + result = await async_registration.register_limit_extrinsic( + subtensor=subtensor, + wallet=fake_wallet, + netuid=123, + limit_price=Balance.from_rao(1000000000), + ) + # Assert + assert result.success == expected_result, f"Test failed for test_id: {test_id}" diff --git a/tests/unit_tests/extrinsics/asyncex/test_root.py b/tests/unit_tests/extrinsics/asyncex/test_root.py index 87b95f0776..42b73ce266 100644 --- a/tests/unit_tests/extrinsics/asyncex/test_root.py +++ b/tests/unit_tests/extrinsics/asyncex/test_root.py @@ -95,7 +95,6 @@ async def test_root_register_extrinsic_success(subtensor, fake_wallet, mocker): storage_function="Uids", params=[0, "fake_hotkey_address"], block_hash=None, - reuse_block_hash=False, ) assert result.success is True assert result.message == "Success" @@ -329,7 +328,6 @@ async def test_root_register_extrinsic_uid_not_found(subtensor, fake_wallet, moc storage_function="Uids", params=[0, "fake_hotkey_address"], block_hash=None, - reuse_block_hash=False, ) assert result.success is False diff --git a/tests/unit_tests/extrinsics/asyncex/test_weights.py b/tests/unit_tests/extrinsics/asyncex/test_weights.py index 649dc79b15..c365bf4ed5 100644 --- a/tests/unit_tests/extrinsics/asyncex/test_weights.py +++ b/tests/unit_tests/extrinsics/asyncex/test_weights.py @@ -1,7 +1,6 @@ import pytest from bittensor.core.extrinsics.asyncex import weights as weights_module -from bittensor.core.settings import version_as_int from bittensor.core.types import ExtrinsicResponse diff --git a/tests/unit_tests/extrinsics/test_children.py b/tests/unit_tests/extrinsics/test_children.py index d1db6e49ca..5d0343c139 100644 --- a/tests/unit_tests/extrinsics/test_children.py +++ b/tests/unit_tests/extrinsics/test_children.py @@ -91,7 +91,7 @@ def test_root_set_pending_childkey_cooldown_extrinsic(subtensor, mocker, fake_wa period=None, raise_error=False, wait_for_inclusion=True, - wait_for_finalization=False, + wait_for_finalization=True, ) assert success is True assert "Success" in message diff --git a/tests/unit_tests/extrinsics/test_coldkey_swap.py b/tests/unit_tests/extrinsics/test_coldkey_swap.py index 9ef59d549d..bf490e25bc 100644 --- a/tests/unit_tests/extrinsics/test_coldkey_swap.py +++ b/tests/unit_tests/extrinsics/test_coldkey_swap.py @@ -20,6 +20,9 @@ def test_announce_coldkey_swap_extrinsic(subtensor, mocker): "unlock_wallet", return_value=ExtrinsicResponse(success=True, message="Unlocked"), ) + mocked_get_staking_hotkeys = mocker.patch.object( + subtensor, "get_staking_hotkeys", return_value=[] + ) mocked_keypair = mocker.patch("bittensor.core.extrinsics.coldkey_swap.Keypair") mocked_keypair_instance = mocker.MagicMock() mocked_keypair_instance.public_key = b"\x00" * 32 @@ -49,6 +52,7 @@ def test_announce_coldkey_swap_extrinsic(subtensor, mocker): # Asserts mocked_unlock_wallet.assert_called_once_with(wallet, False) + mocked_get_staking_hotkeys.assert_called_once_with(new_coldkey_ss58) mocked_keypair.assert_called_once_with(ss58_address=new_coldkey_ss58) mocked_compute_hash.assert_called_once_with(mocked_keypair_instance) mocked_subtensor_module.assert_called_once_with(subtensor) @@ -78,6 +82,9 @@ def test_announce_coldkey_swap_extrinsic_with_mev_protection(subtensor, mocker): "unlock_wallet", return_value=ExtrinsicResponse(success=True, message="Unlocked"), ) + mocked_get_staking_hotkeys = mocker.patch.object( + subtensor, "get_staking_hotkeys", return_value=[] + ) mocked_keypair = mocker.patch("bittensor.core.extrinsics.coldkey_swap.Keypair") mocked_keypair_instance = mocker.MagicMock() mocked_keypair_instance.public_key = b"\x00" * 32 @@ -107,6 +114,7 @@ def test_announce_coldkey_swap_extrinsic_with_mev_protection(subtensor, mocker): # Asserts mocked_unlock_wallet.assert_called_once_with(wallet, False) + mocked_get_staking_hotkeys.assert_called_once_with(new_coldkey_ss58) mocked_subtensor_module.assert_called_once_with(subtensor) mocked_pallet_instance.announce_coldkey_swap.assert_called_once_with( new_coldkey_hash="0x" + "00" * 32 @@ -371,6 +379,50 @@ def test_dispute_coldkey_swap_extrinsic(subtensor, mocker): assert response == mocked_sign_and_send_extrinsic.return_value +def test_clear_coldkey_swap_announcement_extrinsic(subtensor, mocker): + """Verify that sync clear_coldkey_swap_announcement_extrinsic calls pallet and sign_and_send_extrinsic.""" + wallet = mocker.MagicMock(spec=Wallet) + + mocked_unlock_wallet = mocker.patch.object( + ExtrinsicResponse, + "unlock_wallet", + return_value=ExtrinsicResponse(success=True, message="Unlocked"), + ) + mocked_subtensor_module = mocker.patch.object( + coldkey_swap, "SubtensorModule", return_value=mocker.MagicMock() + ) + mocked_pallet_instance = mocked_subtensor_module.return_value + mocked_pallet_instance.clear_coldkey_swap_announcement.return_value = ( + mocker.MagicMock() + ) + mocked_sign_and_send_extrinsic = mocker.patch.object( + subtensor, + "sign_and_send_extrinsic", + return_value=ExtrinsicResponse(True, "Success"), + ) + + # Call + response = coldkey_swap.clear_coldkey_swap_announcement_extrinsic( + subtensor=subtensor, + wallet=wallet, + mev_protection=False, + ) + + # Asserts + mocked_unlock_wallet.assert_called_once_with(wallet, False) + mocked_subtensor_module.assert_called_once_with(subtensor) + mocked_pallet_instance.clear_coldkey_swap_announcement.assert_called_once_with() + mocked_sign_and_send_extrinsic.assert_called_once_with( + call=mocked_pallet_instance.clear_coldkey_swap_announcement.return_value, + wallet=wallet, + wait_for_inclusion=True, + wait_for_finalization=True, + period=None, + raise_error=False, + ) + assert response == mocked_sign_and_send_extrinsic.return_value + + def test_reset_coldkey_swap_extrinsic(subtensor, mocker): """Verify that sync reset_coldkey_swap_extrinsic uses sudo_call_extrinsic.""" wallet = mocker.MagicMock(spec=Wallet) @@ -457,6 +509,14 @@ def test_subtensor_module_dispute_reset_swap_coldkey_call_names(subtensor, mocke call_params={}, ) + mocked_compose_call.reset_mock() + pallet.clear_coldkey_swap_announcement() + mocked_compose_call.assert_called_with( + call_module="SubtensorModule", + call_function="clear_coldkey_swap_announcement", + call_params={}, + ) + mocked_compose_call.reset_mock() pallet.reset_coldkey_swap( coldkey="5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY" diff --git a/tests/unit_tests/extrinsics/test_mev_shield.py b/tests/unit_tests/extrinsics/test_mev_shield.py index 2c0fba851b..549c3d55b9 100644 --- a/tests/unit_tests/extrinsics/test_mev_shield.py +++ b/tests/unit_tests/extrinsics/test_mev_shield.py @@ -1,4 +1,3 @@ -from bittensor_wallet import Wallet from scalecodec.types import GenericCall from async_substrate_interface import ExtrinsicReceipt from async_substrate_interface.errors import SubstrateRequestException diff --git a/tests/unit_tests/extrinsics/test_registration.py b/tests/unit_tests/extrinsics/test_registration.py index cefd3ca6b8..631b46b6e6 100644 --- a/tests/unit_tests/extrinsics/test_registration.py +++ b/tests/unit_tests/extrinsics/test_registration.py @@ -3,7 +3,7 @@ from bittensor.core.types import ExtrinsicResponse from bittensor.core.extrinsics import registration from bittensor.core.subtensor import Subtensor -from bittensor.utils.registration import POWSolution +from bittensor.utils.balance import Balance # Mocking external dependencies @@ -25,171 +25,6 @@ def mock_wallet(mocker): return mock -@pytest.fixture -def mock_pow_solution(mocker): - mock = mocker.MagicMock(spec=POWSolution) - mock.block_number = 123 - mock.nonce = 456 - mock.seal = [0, 1, 2, 3] - mock.is_stale.return_value = False - return mock - - -@pytest.fixture -def mock_new_wallet(mocker): - mock = mocker.MagicMock(spec=Wallet) - mock.coldkeypub.ss58_address = "mock_address" - mock.coldkey = mocker.MagicMock() - mock.hotkey = mocker.MagicMock() - return mock - - -@pytest.mark.parametrize( - "subnet_exists, neuron_is_null, cuda_available, expected_result, expected_message", - [ - ( - False, - True, - True, - False, - "Subnet 123 does not exist.", - ), - (True, False, True, True, "Already registered."), - (True, True, False, False, "CUDA not available."), - ], - ids=["subnet-does-not-exist", "neuron-already-registered", "cuda-unavailable"], -) -def test_register_extrinsic_without_pow( - mock_subtensor, - mock_wallet, - mocker, - subnet_exists, - neuron_is_null, - cuda_available, - expected_result, - expected_message, -): - # Arrange - mocker.patch.object(mock_subtensor, "subnet_exists", return_value=subnet_exists) - fake_neuron = mocker.patch.object( - mock_subtensor, - "get_neuron_for_pubkey_and_subnet", - return_value=mocker.MagicMock(is_null=neuron_is_null), - ) - mocker.patch("torch.cuda.is_available", return_value=cuda_available) - mocker.patch( - "bittensor.utils.registration.pow._get_block_with_retry", - return_value=(0, 0, "00ff11ee"), - ) - - # Act - result = registration.register_extrinsic( - subtensor=mock_subtensor, - wallet=mock_wallet, - netuid=123, - wait_for_inclusion=True, - wait_for_finalization=True, - max_allowed_attempts=3, - output_in_place=True, - cuda=True, - dev_id=0, - tpb=256, - num_processes=None, - update_interval=None, - log_verbose=False, - ) - - # Assert - data = ( - {"neuron": fake_neuron.return_value} - if fake_neuron.call_count > 0 and cuda_available - else None - ) - expected_result = ExtrinsicResponse( - expected_result, - expected_message, - extrinsic_function="register_extrinsic", - data=data, - ) - assert result == expected_result - - -@pytest.mark.parametrize( - "pow_success, pow_stale, registration_success, cuda, hotkey_registered, expected_result", - [ - (True, False, True, False, False, True), - (True, False, True, True, False, True), - # Pow failed but key was registered already - (False, False, False, False, True, True), - # Pow was a success but registration failed with error 'key already registered' - (True, False, False, False, False, False), - ], - ids=[ - "successful-with-valid-pow", - "successful-with-valid-cuda-pow", - "hotkey-registered", - "registration-fail-key-registered", - ], -) -def test_register_extrinsic_with_pow( - mock_subtensor, - mock_wallet, - mock_pow_solution, - pow_success, - pow_stale, - registration_success, - cuda, - hotkey_registered, - expected_result, - mocker, -): - # Arrange - mocker.patch( - "bittensor.utils.registration.pow._solve_for_difficulty_fast", - return_value=mock_pow_solution if pow_success else None, - ) - mocker.patch( - "bittensor.utils.registration.pow._solve_for_difficulty_fast_cuda", - return_value=mock_pow_solution if pow_success else None, - ) - mocker.patch.object( - mock_subtensor, - "sign_and_send_extrinsic", - return_value=ExtrinsicResponse( - registration_success, "HotKeyAlreadyRegisteredInSubNet" - ), - ) - mocker.patch("torch.cuda.is_available", return_value=cuda) - - # Act - if pow_success: - mock_pow_solution.is_stale.return_value = pow_stale - - if not pow_success and hotkey_registered: - mock_subtensor.is_hotkey_registered = mocker.MagicMock( - return_value=hotkey_registered - ) - - result = registration.register_extrinsic( - subtensor=mock_subtensor, - wallet=mock_wallet, - netuid=123, - wait_for_inclusion=True, - wait_for_finalization=True, - max_allowed_attempts=3, - output_in_place=True, - cuda=cuda, - dev_id=0, - tpb=256, - num_processes=None, - update_interval=None, - log_verbose=False, - ) - - # Assert - assert result[0] is expected_result - - @pytest.mark.parametrize( "subnet_exists, neuron_is_null, recycle_success, is_registered, expected_result, test_id", [ @@ -364,3 +199,57 @@ def test_set_subnet_identity_extrinsic_is_failed(mock_subtensor, mock_wallet, mo ) assert result == mocked_sign_and_send_extrinsic.return_value + + +@pytest.mark.parametrize( + "subnet_exists, neuron_is_null, recycle_success, is_registered, expected_result, test_id", + [ + # Happy paths + (True, False, None, None, True, "neuron-not-null"), + (True, True, True, True, True, "happy-path-wallet-registered"), + # Error paths + (True, True, True, False, False, "error-path-not-registered"), + (False, True, False, None, False, "subnet-non-existence"), + (True, True, False, False, False, "error-path-recycling-failed"), + ], +) +def test_register_limit_extrinsic( + mock_subtensor, + mock_wallet, + subnet_exists, + neuron_is_null, + recycle_success, + is_registered, + expected_result, + test_id, + mocker, +): + # Arrange + mock_substrate_ = mocker.MagicMock( + **{"get_payment_info.return_value": {"partial_fee": 10}} + ) + mocker.patch.object(mock_subtensor, "substrate", mock_substrate_) + mocker.patch.object(mock_subtensor, "subnet_exists", return_value=subnet_exists) + mocker.patch.object( + mock_subtensor, + "get_neuron_for_pubkey_and_subnet", + return_value=mocker.MagicMock(is_null=neuron_is_null), + ) + mocker.patch.object( + mock_subtensor, + "sign_and_send_extrinsic", + return_value=ExtrinsicResponse(recycle_success, "Mock error message"), + ) + mocker.patch.object( + mock_subtensor, "is_hotkey_registered", return_value=is_registered + ) + + # Act + result = registration.register_limit_extrinsic( + subtensor=mock_subtensor, + wallet=mock_wallet, + netuid=123, + limit_price=Balance.from_rao(1000000000), + ) + # Assert + assert result.success == expected_result, f"Test failed for test_id: {test_id}" diff --git a/tests/unit_tests/extrinsics/test_transfer.py b/tests/unit_tests/extrinsics/test_transfer.py index e6f560c388..4ddd3031bd 100644 --- a/tests/unit_tests/extrinsics/test_transfer.py +++ b/tests/unit_tests/extrinsics/test_transfer.py @@ -1,4 +1,3 @@ -import pytest from bittensor.core.extrinsics import transfer from bittensor.core.settings import DEFAULT_MEV_PROTECTION from bittensor.utils.balance import Balance diff --git a/tests/unit_tests/test_async_subtensor.py b/tests/unit_tests/test_async_subtensor.py index 072be4b650..aba3085fc2 100644 --- a/tests/unit_tests/test_async_subtensor.py +++ b/tests/unit_tests/test_async_subtensor.py @@ -2,9 +2,10 @@ import unittest.mock as mock import pytest -from async_substrate_interface.types import Runtime, ScaleObj +from async_substrate_interface.types import Runtime from bittensor_wallet import Wallet from scalecodec import GenericCall +from scalecodec.base import ScaleType from bittensor import u64_normalized_float from bittensor.core import async_subtensor, settings @@ -14,7 +15,6 @@ NeuronInfo, SelectiveMetagraphIndex, StakeInfo, - proposal_vote_data, ) from bittensor.core.errors import BalanceTypeError from bittensor.core.settings import DEFAULT_MEV_PROTECTION, DEFAULT_PERIOD @@ -39,35 +39,6 @@ def subtensor(mock_substrate): return async_subtensor.AsyncSubtensor() -def test_decode_ss58_tuples_in_proposal_vote_data(mocker): - """Tests that ProposalVoteData instance instantiation works properly,""" - # Preps - mocked_decode_account_id = mocker.patch.object( - proposal_vote_data, "decode_account_id" - ) - fake_proposal_dict = { - "index": "0", - "threshold": 1, - "ayes": ("0 line", "1 line"), - "nays": ("2 line", "3 line"), - "end": 123, - } - - # Call - async_subtensor.ProposalVoteData.from_dict(fake_proposal_dict) - - # Asserts - assert mocked_decode_account_id.call_count == len(fake_proposal_dict["ayes"]) + len( - fake_proposal_dict["nays"] - ) - assert mocked_decode_account_id.mock_calls == [ - mocker.call("0 line"), - mocker.call("1 line"), - mocker.call("2 line"), - mocker.call("3 line"), - ] - - def test_decode_hex_identity_dict_with_non_tuple_value(): """Tests _decode_hex_identity_dict when value is not a tuple.""" info_dict = {"info": "regular_string"} @@ -335,7 +306,6 @@ async def test_get_total_subnets(subtensor, mocker): storage_function="TotalNetworks", params=[], block_hash=fake_block_hash, - reuse_block_hash=False, ) @@ -368,7 +338,6 @@ async def test_get_subnets(subtensor, mocker, records, response): module="SubtensorModule", storage_function="NetworksAdded", block_hash=fake_block_hash, - reuse_block_hash=False, ) assert result == response @@ -397,7 +366,9 @@ async def test_is_hotkey_delegate(subtensor, mocker, hotkey_ss58_in_result): # Asserts assert result == hotkey_ss58_in_result - mocked_get_delegates.assert_called_once_with(block_hash=None, reuse_block=True) + mocked_get_delegates.assert_called_once_with( + block_hash=subtensor.substrate.last_block_hash, reuse_block=True + ) @pytest.mark.parametrize( @@ -576,7 +547,7 @@ async def test_query_runtime_api(subtensor, mocker): fake_block_hash, ) - assert result == mocked_runtime_call.return_value.value + assert result == mocked_runtime_call.return_value @pytest.mark.asyncio @@ -610,10 +581,9 @@ async def test_get_balance(subtensor, mocker): storage_function="Account", params=[fake_address], block_hash=mocked_determine_block_hash.return_value, - reuse_block_hash=reuse_block, ) mocked_balance.assert_called_once_with( - subtensor.substrate.query.return_value.__getitem__.return_value.__getitem__.return_value + subtensor.substrate.query.return_value.value.__getitem__.return_value.__getitem__.return_value ) assert result == mocked_balance.return_value @@ -690,11 +660,11 @@ async def test_get_netuids_for_hotkey_with_records(subtensor, mocker): subtensor.substrate.query_map = mocked_substrate_query_map fake_hotkey_ss58 = "hotkey_58" - fake_block_hash = None + fake_block_hash = subtensor.substrate.last_block_hash # Call result = await subtensor.get_netuids_for_hotkey( - hotkey_ss58=fake_hotkey_ss58, block_hash=fake_block_hash, reuse_block=True + hotkey_ss58=fake_hotkey_ss58, reuse_block=True ) # Assertions @@ -703,7 +673,6 @@ async def test_get_netuids_for_hotkey_with_records(subtensor, mocker): storage_function="IsNetworkMember", params=[fake_hotkey_ss58], block_hash=fake_block_hash, - reuse_block_hash=True, ) assert result == expected_response @@ -725,11 +694,11 @@ async def test_get_netuids_for_hotkey_without_records(subtensor, mocker): subtensor.substrate.query_map = mocked_substrate_query_map fake_hotkey_ss58 = "hotkey_58" - fake_block_hash = None + fake_block_hash = subtensor.substrate.last_block_hash # Call result = await subtensor.get_netuids_for_hotkey( - hotkey_ss58=fake_hotkey_ss58, block_hash=fake_block_hash, reuse_block=True + hotkey_ss58=fake_hotkey_ss58, block_hash=None, reuse_block=True ) # Assertions @@ -738,7 +707,6 @@ async def test_get_netuids_for_hotkey_without_records(subtensor, mocker): storage_function="IsNetworkMember", params=[fake_hotkey_ss58], block_hash=fake_block_hash, - reuse_block_hash=True, ) assert result == expected_response @@ -769,7 +737,6 @@ async def test_subnet_exists(subtensor, mocker): storage_function="NetworksAdded", params=[fake_netuid], block_hash=fake_block_hash, - reuse_block_hash=fake_reuse_block_hash, ) assert result == mocked_substrate_query.return_value.value @@ -807,7 +774,6 @@ async def test_get_hyperparameter_happy_path(subtensor, mocker): storage_function=fake_param_name, params=[fake_netuid], block_hash=fake_block_hash, - reuse_block_hash=fake_reuse_block_hash, ) assert result == mocked_substrate_query.return_value.value @@ -912,7 +878,6 @@ async def test_get_existential_deposit_happy_path(subtensor, mocker): module_name="Balances", constant_name="ExistentialDeposit", block_hash=fake_block_hash, - reuse_block_hash=fake_reuse_block_hash, ) spy_balance_from_rao.assert_called_once_with( mocked_substrate_get_constant.return_value.value @@ -946,7 +911,6 @@ async def test_get_existential_deposit_raise_exception(subtensor, mocker): module_name="Balances", constant_name="ExistentialDeposit", block_hash=fake_block_hash, - reuse_block_hash=fake_reuse_block_hash, ) spy_balance_from_rao.assert_not_called() @@ -1046,7 +1010,7 @@ async def test_get_neuron_for_pubkey_and_subnet_success(subtensor, mocker): mocker.patch.object( subtensor.substrate, "runtime_call", - return_value=mocker.Mock(value=fake_result), + return_value=fake_result, ) mocked_neuron_info = mocker.patch.object( async_subtensor.NeuronInfo, "from_dict", return_value="fake_neuron_info" @@ -1064,7 +1028,6 @@ async def test_get_neuron_for_pubkey_and_subnet_success(subtensor, mocker): storage_function="Uids", params=[fake_netuid, fake_hotkey], block_hash=None, - reuse_block_hash=False, ) subtensor.substrate.runtime_call.assert_awaited_once() subtensor.substrate.runtime_call.assert_called_once_with( @@ -1087,7 +1050,7 @@ async def test_get_neuron_for_pubkey_and_subnet_uid_not_found(subtensor, mocker) mocker.patch.object( subtensor.substrate, "query", - return_value=None, + return_value=mocker.Mock(spec=ScaleType, value=None), ) mocked_get_null_neuron = mocker.patch.object( async_subtensor.NeuronInfo, "get_null_neuron", return_value="null_neuron" @@ -1104,7 +1067,6 @@ async def test_get_neuron_for_pubkey_and_subnet_uid_not_found(subtensor, mocker) storage_function="Uids", params=[fake_netuid, fake_hotkey], block_hash=None, - reuse_block_hash=False, ) mocked_get_null_neuron.assert_called_once() assert result == "null_neuron" @@ -1126,7 +1088,7 @@ async def test_get_neuron_for_pubkey_and_subnet_rpc_result_empty(subtensor, mock mocker.patch.object( subtensor.substrate, "runtime_call", - return_value=mocker.Mock(value=None), + return_value=None, ) mocked_get_null_neuron = mocker.patch.object( async_subtensor.NeuronInfo, "get_null_neuron", return_value="null_neuron" @@ -1143,7 +1105,6 @@ async def test_get_neuron_for_pubkey_and_subnet_rpc_result_empty(subtensor, mock storage_function="Uids", params=[fake_netuid, fake_hotkey], block_hash=None, - reuse_block_hash=False, ) subtensor.substrate.runtime_call.assert_called_once_with( "NeuronInfoRuntimeApi", @@ -1180,7 +1141,7 @@ async def test_neuron_for_uid_happy_path(subtensor, mocker): # Asserts mocked_null_neuron.assert_not_called() mocked_neuron_info_from_dict.assert_called_once_with( - subtensor.substrate.runtime_call.return_value.value + subtensor.substrate.runtime_call.return_value ) assert result == mocked_neuron_info_from_dict.return_value @@ -1222,11 +1183,7 @@ async def test_neuron_for_uid(subtensor, mocker): ) # no result in response - mocked_substrate_runtime_call = mocker.AsyncMock( - return_value=mocker.Mock( - value=None, - ), - ) + mocked_substrate_runtime_call = mocker.AsyncMock(return_value=None) subtensor.substrate.runtime_call = mocked_substrate_runtime_call mocked_neuron_info_from_dict = mocker.patch.object( @@ -1267,7 +1224,7 @@ async def test_get_delegated_no_block_hash_no_reuse(subtensor, mocker): None, ) mocked_delegated_list_from_dicts.assert_called_once_with( - subtensor.substrate.runtime_call.return_value.value + subtensor.substrate.runtime_call.return_value ) assert result == mocked_delegated_list_from_dicts.return_value @@ -1297,7 +1254,7 @@ async def test_get_delegated_with_block_hash(subtensor, mocker): fake_block_hash, ) mocked_delegated_list_from_dicts.assert_called_once_with( - subtensor.substrate.runtime_call.return_value.value + subtensor.substrate.runtime_call.return_value ) assert result == mocked_delegated_list_from_dicts.return_value @@ -1327,7 +1284,7 @@ async def test_get_delegated_with_reuse_block(subtensor, mocker): subtensor.substrate.last_block_hash, ) mocked_delegated_list_from_dicts.assert_called_once_with( - subtensor.substrate.runtime_call.return_value.value + subtensor.substrate.runtime_call.return_value ) assert result == mocked_delegated_list_from_dicts.return_value @@ -1338,11 +1295,7 @@ async def test_get_delegated_with_empty_result(subtensor, mocker): # Preps fake_coldkey_ss58 = "fake_ss58_address" - mocked_runtime_call = mocker.AsyncMock( - return_value=mocker.Mock( - value=None, - ), - ) + mocked_runtime_call = mocker.AsyncMock(return_value=None) subtensor.substrate.runtime_call = mocked_runtime_call # Call @@ -1364,15 +1317,17 @@ async def test_query_identity_successful(subtensor, mocker): # Preps fake_coldkey_ss58 = "test_key" fake_block_hash = "block_hash" - fake_identity_info = { - "additional": "Additional", - "description": "Description", - "discord": "", - "github_repo": "https://github.com/opentensor/bittensor", - "image": "", - "name": "Name", - "url": "https://www.example.com", - } + fake_identity_info = mocker.MagicMock( + value={ + "additional": "Additional", + "description": "Description", + "discord": "", + "github_repo": "https://github.com/opentensor/bittensor", + "image": "", + "name": "Name", + "url": "https://www.example.com", + } + ) mocked_query = mocker.AsyncMock(return_value=fake_identity_info) subtensor.substrate.query = mocked_query @@ -1388,7 +1343,6 @@ async def test_query_identity_successful(subtensor, mocker): storage_function="IdentitiesV2", params=[fake_coldkey_ss58], block_hash=fake_block_hash, - reuse_block_hash=False, ) assert result == ChainIdentity( additional="Additional", @@ -1407,39 +1361,11 @@ async def test_query_identity_no_info(subtensor, mocker): # Preps fake_coldkey_ss58 = "test_key" - mocked_query = mocker.AsyncMock(return_value=None) - subtensor.substrate.query = mocked_query - - # Call - result = await subtensor.query_identity(coldkey_ss58=fake_coldkey_ss58) - - # Asserts - mocked_query.assert_called_once_with( - module="SubtensorModule", - storage_function="IdentitiesV2", - params=[fake_coldkey_ss58], - block_hash=None, - reuse_block_hash=False, + mocked_query = mocker.AsyncMock( + return_value=mocker.Mock(spec=ScaleType, value=None) ) - assert result is None - - -@pytest.mark.asyncio -async def test_query_identity_type_error(subtensor, mocker): - """Tests query_identity method when a TypeError occurs during decoding.""" - # Preps - fake_coldkey_ss58 = "test_key" - fake_identity_info = {"info": {"rank": (b"\xff\xfe",)}} - - mocked_query = mocker.AsyncMock(return_value=fake_identity_info) subtensor.substrate.query = mocked_query - mocker.patch.object( - async_subtensor, - "decode_hex_identity_dict", - side_effect=TypeError, - ) - # Call result = await subtensor.query_identity(coldkey_ss58=fake_coldkey_ss58) @@ -1449,7 +1375,6 @@ async def test_query_identity_type_error(subtensor, mocker): storage_function="IdentitiesV2", params=[fake_coldkey_ss58], block_hash=None, - reuse_block_hash=False, ) assert result is None @@ -1461,8 +1386,8 @@ async def test_weights_successful(subtensor, mocker): fake_netuid = 1 fake_block_hash = "block_hash" fake_weights = [ - (0, mocker.AsyncMock(value=[(1, 10), (2, 20)])), - (1, mocker.AsyncMock(value=[(0, 15), (2, 25)])), + (0, [(1, 10), (2, 20)]), + (1, [(0, 15), (2, 25)]), ] async def mock_query_map(**_): @@ -1480,7 +1405,6 @@ async def mock_query_map(**_): storage_function="Weights", params=[fake_netuid], block_hash=fake_block_hash, - reuse_block_hash=False, ) assert result == [(0, [(1, 10), (2, 20)]), (1, [(0, 15), (2, 25)])] @@ -1492,8 +1416,8 @@ async def test_bonds(subtensor, mocker): fake_netuid = 1 fake_block_hash = "block_hash" fake_bonds = [ - (0, mocker.Mock(value=[(1, 100), (2, 200)])), - (1, mocker.Mock(value=[(0, 150), (2, 250)])), + (0, [(1, 100), (2, 200)]), + (1, [(0, 150), (2, 250)]), ] async def mock_query_map(**_): @@ -1511,7 +1435,6 @@ async def mock_query_map(**_): storage_function="Bonds", params=[fake_netuid], block_hash=fake_block_hash, - reuse_block_hash=False, ) assert result == [(0, [(1, 100), (2, 200)]), (1, [(0, 150), (2, 250)])] @@ -1538,7 +1461,6 @@ async def test_does_hotkey_exist_true(subtensor, mocker): storage_function="Owner", params=[fake_hotkey_ss58], block_hash=fake_block_hash, - reuse_block_hash=False, ) assert result is True @@ -1548,7 +1470,9 @@ async def test_does_hotkey_exist_false_for_specific_account(subtensor, mocker): """Tests does_hotkey_exist method when the hotkey exists but matches the specific account ID to ignore.""" # Preps fake_hotkey_ss58 = "fake_hotkey" - fake_query_result = "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM" + fake_query_result = mocker.Mock( + spec=ScaleType, value="5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM" + ) mocked_query = mocker.patch.object( subtensor.substrate, "query", return_value=fake_query_result @@ -1563,7 +1487,6 @@ async def test_does_hotkey_exist_false_for_specific_account(subtensor, mocker): storage_function="Owner", params=[fake_hotkey_ss58], block_hash=None, - reuse_block_hash=False, ) assert result is False @@ -1575,7 +1498,9 @@ async def test_get_hotkey_owner_successful(subtensor, mocker): fake_hotkey_ss58 = "valid_hotkey" fake_block_hash = "block_hash" - mocked_query = mocker.AsyncMock(return_value="decoded_owner_account_id") + mocked_query = mocker.AsyncMock( + return_value=mocker.Mock(spec=ScaleType, value="decoded_owner_account_id") + ) subtensor.substrate.query = mocked_query mocked_does_hotkey_exist = mocker.AsyncMock(return_value=True) @@ -1592,7 +1517,6 @@ async def test_get_hotkey_owner_successful(subtensor, mocker): storage_function="Owner", params=[fake_hotkey_ss58], block_hash=fake_block_hash, - reuse_block_hash=False, ) mocked_does_hotkey_exist.assert_awaited_once_with( fake_hotkey_ss58, block_hash=fake_block_hash @@ -1607,7 +1531,11 @@ async def test_get_hotkey_owner_non_existent_hotkey(subtensor, mocker): fake_hotkey_ss58 = "non_existent_hotkey" fake_block_hash = "block_hash" - mocked_query = mocker.AsyncMock(return_value=None) + mocked_query = mocker.AsyncMock( + return_value=mocker.Mock( + spec=ScaleType, value="5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM" + ) + ) subtensor.substrate.query = mocked_query # Call @@ -1621,7 +1549,6 @@ async def test_get_hotkey_owner_non_existent_hotkey(subtensor, mocker): storage_function="Owner", params=[fake_hotkey_ss58], block_hash=fake_block_hash, - reuse_block_hash=False, ) assert result is None @@ -1871,19 +1798,14 @@ async def test_get_children_success(subtensor, mocker): fake_netuid = 1 fake_children = mocker.Mock( value=[ - (1000, ["child_key_1"]), - (2000, ["child_key_2"]), + (1000, "decoded_child_key_1"), + (2000, "decoded_child_key_2"), ] ) mocked_query = mocker.AsyncMock(return_value=fake_children) subtensor.substrate.query = mocked_query - mocked_decode_account_id = mocker.Mock( - side_effect=["decoded_child_key_1", "decoded_child_key_2"] - ) - mocker.patch.object(async_subtensor, "decode_account_id", mocked_decode_account_id) - expected_formatted_children = [ (u64_normalized_float(1000), "decoded_child_key_1"), (u64_normalized_float(2000), "decoded_child_key_2"), @@ -1898,10 +1820,6 @@ async def test_get_children_success(subtensor, mocker): module="SubtensorModule", storage_function="ChildKeys", params=[fake_hotkey, fake_netuid], - reuse_block_hash=False, - ) - mocked_decode_account_id.assert_has_calls( - [mocker.call("child_key_1"), mocker.call("child_key_2")] ) assert result == (True, expected_formatted_children, "") @@ -1926,7 +1844,6 @@ async def test_get_children_no_children(subtensor, mocker): module="SubtensorModule", storage_function="ChildKeys", params=[fake_hotkey, fake_netuid], - reuse_block_hash=False, ) assert result == (True, [], "") @@ -1956,7 +1873,6 @@ async def test_get_children_substrate_request_exception(subtensor, mocker): module="SubtensorModule", storage_function="ChildKeys", params=[fake_hotkey, fake_netuid], - reuse_block_hash=False, ) mocked_format_error_message.assert_called_once_with(fake_exception) assert result == (False, [], "Formatted error message") @@ -1970,19 +1886,14 @@ async def test_get_parents_success(subtensor, mocker): fake_netuid = 1 fake_parents = mocker.Mock( value=[ - (1000, ["parent_key_1"]), - (2000, ["parent_key_2"]), + (1000, "decoded_parent_key_1"), + (2000, "decoded_parent_key_2"), ] ) mocked_query = mocker.AsyncMock(return_value=fake_parents) subtensor.substrate.query = mocked_query - mocked_decode_account_id = mocker.Mock( - side_effect=["decoded_parent_key_1", "decoded_parent_key_2"] - ) - mocker.patch.object(async_subtensor, "decode_account_id", mocked_decode_account_id) - expected_formatted_parents = [ (u64_normalized_float(1000), "decoded_parent_key_1"), (u64_normalized_float(2000), "decoded_parent_key_2"), @@ -1997,10 +1908,6 @@ async def test_get_parents_success(subtensor, mocker): module="SubtensorModule", storage_function="ParentKeys", params=[fake_hotkey, fake_netuid], - reuse_block_hash=False, - ) - mocked_decode_account_id.assert_has_calls( - [mocker.call("parent_key_1"), mocker.call("parent_key_2")] ) assert result == expected_formatted_parents @@ -2013,7 +1920,9 @@ async def test_get_parents_no_parents(subtensor, mocker): fake_netuid = 1 fake_parents = [] - mocked_query = mocker.AsyncMock(return_value=fake_parents) + mocked_query = mocker.AsyncMock( + return_value=mocker.Mock(spec=ScaleType, value=fake_parents) + ) subtensor.substrate.query = mocked_query # Call @@ -2025,7 +1934,6 @@ async def test_get_parents_no_parents(subtensor, mocker): module="SubtensorModule", storage_function="ParentKeys", params=[fake_hotkey, fake_netuid], - reuse_block_hash=False, ) assert result == [] @@ -2052,7 +1960,7 @@ async def test_get_children_pending(mock_substrate, subtensor): [ ( U64_MAX, - (tuple(bytearray(32)),), + "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", ), ], 123, @@ -2076,7 +1984,6 @@ async def test_get_children_pending(mock_substrate, subtensor): storage_function="PendingChildKeys", params=[1, "hotkey_ss58"], block_hash=None, - reuse_block_hash=False, ) @@ -2176,7 +2083,9 @@ async def test_get_vote_data_success(subtensor, mocker): fake_block_hash = "block_hash" fake_vote_data = {"ayes": ["senate_member_1"], "nays": ["senate_member_2"]} - mocked_query = mocker.AsyncMock(return_value=fake_vote_data) + mocked_query = mocker.AsyncMock( + return_value=mocker.Mock(spec=ScaleType, value=fake_vote_data) + ) subtensor.substrate.query = mocked_query mocked_proposal_vote_data = mocker.Mock() @@ -2197,7 +2106,6 @@ async def test_get_vote_data_success(subtensor, mocker): storage_function="Voting", params=[fake_proposal_hash], block_hash=fake_block_hash, - reuse_block_hash=False, ) assert result == mocked_proposal_vote_data @@ -2209,7 +2117,9 @@ async def test_get_vote_data_no_data(subtensor, mocker): fake_proposal_hash = "invalid_proposal_hash" fake_block_hash = "block_hash" - mocked_query = mocker.AsyncMock(return_value=None) + mocked_query = mocker.AsyncMock( + return_value=mocker.Mock(spec=ScaleType, value=None) + ) subtensor.substrate.query = mocked_query # Call @@ -2223,7 +2133,6 @@ async def test_get_vote_data_no_data(subtensor, mocker): storage_function="Voting", params=[fake_proposal_hash], block_hash=fake_block_hash, - reuse_block_hash=False, ) assert result is None @@ -2235,32 +2144,28 @@ async def test_get_delegate_identities(subtensor, mocker): fake_block_hash = "block_hash" fake_chain_data = [ ( - ["delegate1_ss58"], - mocker.Mock( - value={ - "additional": "", - "description": "", - "discord": "", - "github_repo": "", - "image": "", - "name": "Chain Delegate 1", - "url": "", - }, - ), + "delegate1_ss58", + { + "additional": "", + "description": "", + "discord": "", + "github_repo": "", + "image": "", + "name": "Chain Delegate 1", + "url": "", + }, ), ( - ["delegate2_ss58"], - mocker.Mock( - value={ - "additional": "", - "description": "", - "discord": "", - "github_repo": "", - "image": "", - "name": "Chain Delegate 2", - "url": "", - }, - ), + "delegate2_ss58", + { + "additional": "", + "description": "", + "discord": "", + "github_repo": "", + "image": "", + "name": "Chain Delegate 2", + "url": "", + }, ), ] @@ -2269,9 +2174,6 @@ async def test_get_delegate_identities(subtensor, mocker): ) subtensor.substrate.query_map = mocked_query_map - mocked_decode_account_id = mocker.Mock(side_effect=lambda ss58: ss58) - mocker.patch.object(async_subtensor, "decode_account_id", mocked_decode_account_id) - mocked_decode_hex_identity_dict = mocker.Mock(side_effect=lambda data: data) mocker.patch.object( async_subtensor, "decode_hex_identity_dict", mocked_decode_hex_identity_dict @@ -2285,7 +2187,6 @@ async def test_get_delegate_identities(subtensor, mocker): module="SubtensorModule", storage_function="IdentitiesV2", block_hash=fake_block_hash, - reuse_block_hash=False, ) assert result["delegate1_ss58"].name == "Chain Delegate 1" @@ -2313,7 +2214,6 @@ async def test_is_hotkey_registered_true(subtensor, mocker): storage_function="Uids", params=[fake_netuid, fake_hotkey_ss58], block_hash=None, - reuse_block_hash=False, ) assert result is True @@ -2340,7 +2240,6 @@ async def test_is_hotkey_registered_false(subtensor, mocker): storage_function="Uids", params=[fake_netuid, fake_hotkey_ss58], block_hash=None, - reuse_block_hash=False, ) assert result is False @@ -2368,7 +2267,6 @@ async def test_get_uid_for_hotkey_on_subnet_registered(subtensor, mocker): storage_function="Uids", params=[fake_netuid, fake_hotkey_ss58], block_hash=fake_block_hash, - reuse_block_hash=False, ) assert result == fake_uid @@ -2396,7 +2294,6 @@ async def test_get_uid_for_hotkey_on_subnet_not_registered(subtensor, mocker): storage_function="Uids", params=[fake_netuid, fake_hotkey_ss58], block_hash=fake_block_hash, - reuse_block_hash=False, ) assert result is None @@ -2494,7 +2391,7 @@ async def test_blocks_since_last_update_no_last_update(subtensor, mocker): # Preps fake_netuid = 1 fake_uid = 5 - fake_result = None + fake_result = [] mocked_get_hyperparameter = mocker.patch.object( subtensor, @@ -2526,7 +2423,9 @@ async def test_commit_reveal_enabled(subtensor, mocker): netuid = 1 block_hash = "block_hash" mocked_get_hyperparameter = mocker.patch.object( - subtensor, "get_hyperparameter", return_value=mocker.AsyncMock() + subtensor, + "get_hyperparameter", + return_value=False, ) # Call @@ -2605,39 +2504,53 @@ async def test_transfer_success(subtensor, fake_wallet, mocker): @pytest.mark.asyncio async def test_register_success(subtensor, fake_wallet, mocker): - """Tests register when there is enough balance and registration succeeds.""" + """Tests register with auto-calculated limit_price from recycle.""" # Preps fake_netuid = 1 - mocked_register_extrinsic = mocker.AsyncMock() + mocked_register_limit_extrinsic = mocker.AsyncMock() + mocker.patch.object( + async_subtensor, "register_limit_extrinsic", mocked_register_limit_extrinsic + ) mocker.patch.object( - async_subtensor, "register_extrinsic", mocked_register_extrinsic + subtensor, "recycle", return_value=Balance.from_rao(1_000_000_000) ) # Call result = await subtensor.register(wallet=fake_wallet, netuid=fake_netuid) # Asserts - mocked_register_extrinsic.assert_awaited_once_with( + mocked_register_limit_extrinsic.assert_awaited_once_with( + subtensor=subtensor, wallet=fake_wallet, - cuda=False, - dev_id=0, - log_verbose=False, - max_allowed_attempts=3, netuid=1, - num_processes=None, - output_in_place=False, - subtensor=subtensor, - tpb=256, - update_interval=None, + limit_price=Balance.from_rao(1_005_000_000), mev_protection=DEFAULT_MEV_PROTECTION, period=DEFAULT_PERIOD, raise_error=False, - wait_for_finalization=True, wait_for_inclusion=True, + wait_for_finalization=True, wait_for_revealed_execution=True, ) - assert result == mocked_register_extrinsic.return_value + assert result == mocked_register_limit_extrinsic.return_value + + +@pytest.mark.asyncio +async def test_register_on_root(mock_substrate, subtensor, fake_wallet, mocker): + mock_substrate.submit_extrinsic.return_value = mocker.AsyncMock( + is_success=mocker.AsyncMock(return_value=True)(), + ) + mocked_root_register_extrinsic = mocker.patch.object( + async_subtensor, + "root_register_extrinsic", + ) + + response = await subtensor.register( + wallet=fake_wallet, + netuid=0, + ) + + assert response == mocked_root_register_extrinsic.return_value @pytest.mark.asyncio @@ -2822,14 +2735,15 @@ async def test_get_all_neuron_certificates(mocker, subtensor): storage_function="NeuronCertificates", params=[fake_netuid], block_hash=None, - reuse_block_hash=False, ) @pytest.mark.asyncio async def test_get_timestamp(mocker, subtensor): fake_block = 1000 - mocked_query = mocker.AsyncMock(return_value=ScaleObj(1740586018 * 1000)) + mock_return = mocker.MagicMock(spec=ScaleType) + mock_return.value = 1740586018 * 1000 + mocked_query = mocker.AsyncMock(return_value=mock_return) mocker.patch.object(subtensor.substrate, "query", mocked_query) expected_result = datetime.datetime( 2025, 2, 26, 16, 6, 58, tzinfo=datetime.timezone.utc @@ -2844,18 +2758,11 @@ async def test_get_owned_hotkeys_happy_path(subtensor, mocker): # Prep fake_coldkey = "fake_hotkey" fake_hotkey = "fake_hotkey" - fake_hotkeys = [ - [ - fake_hotkey, - ] - ] - mocked_subtensor = mocker.AsyncMock(return_value=fake_hotkeys) - mocker.patch.object(subtensor.substrate, "query", new=mocked_subtensor) - - mocked_decode_account_id = mocker.Mock() - mocker.patch.object( - async_subtensor, "decode_account_id", new=mocked_decode_account_id + fake_hotkeys = [fake_hotkey] + mocked_subtensor = mocker.AsyncMock( + return_value=mocker.Mock(spec=ScaleType, value=fake_hotkeys) ) + mocker.patch.object(subtensor.substrate, "query", new=mocked_subtensor) # Call result = await subtensor.get_owned_hotkeys(fake_coldkey) @@ -2866,10 +2773,8 @@ async def test_get_owned_hotkeys_happy_path(subtensor, mocker): storage_function="OwnedHotkeys", params=[fake_coldkey], block_hash=None, - reuse_block_hash=False, ) - assert result == [mocked_decode_account_id.return_value] - mocked_decode_account_id.assert_called_once_with(fake_hotkey) + assert result == [fake_hotkey] @pytest.mark.asyncio @@ -2877,7 +2782,9 @@ async def test_get_owned_hotkeys_return_empty(subtensor, mocker): """Tests that the output of get_owned_hotkeys is empty.""" # Prep fake_coldkey = "fake_hotkey" - mocked_subtensor = mocker.AsyncMock(return_value=[]) + mocked_subtensor = mocker.AsyncMock( + return_value=mocker.Mock(spec=ScaleType, value=[]) + ) mocker.patch.object(subtensor.substrate, "query", new=mocked_subtensor) # Call @@ -2889,7 +2796,6 @@ async def test_get_owned_hotkeys_return_empty(subtensor, mocker): storage_function="OwnedHotkeys", params=[fake_coldkey], block_hash=None, - reuse_block_hash=False, ) assert result == [] @@ -2996,7 +2902,7 @@ async def test_get_metagraph_info_all_fields(subtensor, mocker): mock_runtime_call = mocker.patch.object( subtensor.substrate, "runtime_call", - return_value=mocker.AsyncMock(value=mock_value), + return_value=mock_value, ) mock_chain_head = mocker.patch.object( subtensor.substrate, @@ -3007,21 +2913,15 @@ async def test_get_metagraph_info_all_fields(subtensor, mocker): async_subtensor.MetagraphInfo, "from_dict", return_value="parsed_metagraph" ) mocked_runtime_metadata_v15 = { - "apis": [ - { - "name": "SubnetInfoRuntimeApi", - "methods": [ - {"name": "get_selective_metagraph"}, - {"name": "get_metagraph"}, - {"name": "get_selective_mechagraph"}, - ], - }, - ] + "SubnetInfoRuntimeApi": { + "get_selective_metagraph": {"name": "get_selective_metagraph"}, + "get_metagraph": {"name": "get_metagraph"}, + "get_selective_mechagraph": {"name": "get_selective_mechagraph"}, + }, } mocked_runtime = mocker.Mock(spec=Runtime) - mocked_metadata = mocker.Mock() - mocked_metadata.value.return_value = mocked_runtime_metadata_v15 - mocked_runtime.metadata_v15 = mocked_metadata + mocked_runtime.metadata_v15 = mocker.Mock() + mocked_runtime.runtime_api_map = mocked_runtime_metadata_v15 mocker.patch.object( subtensor.substrate, "init_runtime", @@ -3056,7 +2956,7 @@ async def test_get_metagraph_info_specific_fields(subtensor, mocker): mock_runtime_call = mocker.patch.object( subtensor.substrate, "runtime_call", - return_value=mocker.AsyncMock(value=mock_value), + return_value=mock_value, ) mock_chain_head = mocker.patch.object( subtensor.substrate, @@ -3067,21 +2967,15 @@ async def test_get_metagraph_info_specific_fields(subtensor, mocker): async_subtensor.MetagraphInfo, "from_dict", return_value="parsed_metagraph" ) mocked_runtime_metadata_v15 = { - "apis": [ - { - "name": "SubnetInfoRuntimeApi", - "methods": [ - {"name": "get_selective_metagraph"}, - {"name": "get_metagraph"}, - {"name": "get_selective_mechagraph"}, - ], - }, - ] + "SubnetInfoRuntimeApi": { + "get_selective_metagraph": {"name": "get_selective_metagraph"}, + "get_metagraph": {"name": "get_metagraph"}, + "get_selective_mechagraph": {"name": "get_selective_mechagraph"}, + }, } mocked_runtime = mocker.Mock(spec=Runtime) - mocked_metadata = mocker.Mock() - mocked_metadata.value.return_value = mocked_runtime_metadata_v15 - mocked_runtime.metadata_v15 = mocked_metadata + mocked_runtime.metadata_v15 = mocker.Mock() + mocked_runtime.runtime_api_map = mocked_runtime_metadata_v15 mocker.patch.object( subtensor.substrate, "init_runtime", @@ -3120,21 +3014,15 @@ async def test_get_metagraph_info_subnet_not_exist(subtensor, mocker): return_value=None, ) mocked_runtime_metadata_v15 = { - "apis": [ - { - "name": "SubnetInfoRuntimeApi", - "methods": [ - {"name": "get_selective_metagraph"}, - {"name": "get_metagraph"}, - {"name": "get_selective_mechagraph"}, - ], - }, - ] + "SubnetInfoRuntimeApi": { + "get_selective_metagraph": {"name": "get_selective_metagraph"}, + "get_metagraph": {"name": "get_metagraph"}, + "get_selective_mechagraph": {"name": "get_selective_mechagraph"}, + }, } mocked_runtime = mocker.Mock(spec=Runtime) - mocked_metadata = mocker.Mock() - mocked_metadata.value.return_value = mocked_runtime_metadata_v15 - mocked_runtime.metadata_v15 = mocked_metadata + mocked_runtime.metadata_v15 = mocker.Mock() + mocked_runtime.runtime_api_map = mocked_runtime_metadata_v15 mocker.patch.object( subtensor.substrate, "init_runtime", @@ -3179,25 +3067,19 @@ async def test_get_metagraph_info_older_runtime_version( "runtime_call", ) mocked_runtime_metadata_v15 = { - "apis": [ - { - "name": "SubnetInfoRuntimeApi", - "methods": [ - {"name": "get_selective_metagraph"}, - {"name": "get_metagraph"}, - ], - }, - ] + "SubnetInfoRuntimeApi": { + "get_selective_metagraph": {"name": "get_selective_metagraph"}, + "get_metagraph": {"name": "get_metagraph"}, + }, } if block == 6_800_000: # only the newer block should have 'mechagraph' runtime - mocked_runtime_metadata_v15["apis"][0]["methods"].append( - {"name": "get_selective_mechagraph"} - ) + mocked_runtime_metadata_v15["SubnetInfoRuntimeApi"][ + "get_selective_mechagraph" + ] = {"name": "get_selective_mechagraph"} mocked_runtime = mocker.Mock(spec=Runtime) - mocked_metadata = mocker.Mock() - mocked_metadata.value.return_value = mocked_runtime_metadata_v15 - mocked_runtime.metadata_v15 = mocked_metadata + mocked_runtime.metadata_v15 = mocker.Mock() + mocked_runtime.runtime_api_map = mocked_runtime_metadata_v15 mocker.patch.object( subtensor.substrate, "init_runtime", @@ -3245,7 +3127,9 @@ async def test_blocks_since_last_step_is_none(subtensor, mocker): # preps netuid = 1 block = 123 - mocked_query_subtensor = mocker.AsyncMock(return_value=None) + mocked_query_subtensor = mocker.AsyncMock( + return_value=mocker.Mock(spec=ScaleType, value=None) + ) subtensor.query_subtensor = mocked_query_subtensor # call @@ -3604,45 +3488,39 @@ async def test_get_liquidity_list_happy_path(subtensor, fake_wallet, mocker): fake_positions = [ [ (2,), - mocker.Mock( - value={ - "id": (2,), - "netuid": 2, - "tick_low": (206189,), - "tick_high": (208196,), - "liquidity": 1000000000000, - "fees_tao": {"bits": 0}, - "fees_alpha": {"bits": 0}, - } - ), + { + "id": 2, + "netuid": 2, + "tick_low": 206189, + "tick_high": 208196, + "liquidity": 1000000000000, + "fees_tao": {"bits": 0}, + "fees_alpha": {"bits": 0}, + }, ], [ - (2,), - mocker.Mock( - value={ - "id": (2,), - "netuid": 2, - "tick_low": (216189,), - "tick_high": (198196,), - "liquidity": 2000000000000, - "fees_tao": {"bits": 0}, - "fees_alpha": {"bits": 0}, - } - ), + 2, + { + "id": 2, + "netuid": 2, + "tick_low": 216189, + "tick_high": 198196, + "liquidity": 2000000000000, + "fees_tao": {"bits": 0}, + "fees_alpha": {"bits": 0}, + }, ], [ - (2,), - mocker.Mock( - value={ - "id": (2,), - "netuid": 2, - "tick_low": (226189,), - "tick_high": (188196,), - "liquidity": 3000000000000, - "fees_tao": {"bits": 0}, - "fees_alpha": {"bits": 0}, - } - ), + 2, + { + "id": 2, + "netuid": 2, + "tick_low": 226189, + "tick_high": 188196, + "liquidity": 3000000000000, + "fees_tao": {"bits": 0}, + "fees_alpha": {"bits": 0}, + }, ], ] @@ -3822,7 +3700,7 @@ async def test_get_subnet_price(subtensor, mocker): fake_price = 29258617 expected_price = Balance.from_tao(0.029258617) mocked_query = mocker.patch.object( - subtensor.substrate, "runtime_call", return_value=mocker.Mock(value=fake_price) + subtensor.substrate, "runtime_call", return_value=fake_price ) # Call @@ -3887,11 +3765,8 @@ async def test_all_subnets(subtensor, mocker): "get_subnet_prices", return_value={0: Balance.from_tao(1), 1: Balance.from_tao(0.029258617)}, ) - mocked_decode = mocker.Mock(return_value=[{"netuid": 0}, {"netuid": 1}]) - mocked_runtime_call = mocker.Mock(decode=mocked_decode) - mocker.patch.object( - subtensor.substrate, "runtime_call", return_value=mocked_runtime_call - ) + mocked_decode = [{"netuid": 0}, {"netuid": 1}] + mocker.patch.object(subtensor.substrate, "runtime_call", return_value=mocked_decode) # Call result = await subtensor.all_subnets() @@ -3925,8 +3800,7 @@ async def test_subnet(subtensor, mocker): mocked_get_subnet_price = mocker.patch.object( subtensor, "get_subnet_price", return_value=Balance.from_tao(100.0) ) - mocked_decode = mocker.Mock(return_value={"netuid": netuid}) - mocked_runtime_call = mocker.Mock(decode=mocked_decode) + mocked_runtime_call = {"netuid": netuid} mocker.patch.object( subtensor.substrate, "runtime_call", return_value=mocked_runtime_call ) @@ -4245,8 +4119,8 @@ async def test_get_auto_stakes(subtensor, mocker): fake_hk_1 = mocker.Mock() fake_hk_2 = mocker.Mock() - dest_value_1 = mocker.Mock(value=[fake_hk_1]) - dest_value_2 = mocker.Mock(value=[fake_hk_2]) + dest_value_1 = fake_hk_1 + dest_value_2 = fake_hk_2 mock_result = mocker.MagicMock() mock_result.__aiter__.return_value = iter([(0, dest_value_1), (1, dest_value_2)]) @@ -4254,12 +4128,6 @@ async def test_get_auto_stakes(subtensor, mocker): subtensor.substrate, "query_map", return_value=mock_result ) - mocked_decode_account_id = mocker.patch.object( - async_subtensor, - "decode_account_id", - side_effect=[fake_hk_1, fake_hk_2], - ) - # Call result = await subtensor.get_auto_stakes(coldkey_ss58=fake_coldkey) @@ -4271,9 +4139,6 @@ async def test_get_auto_stakes(subtensor, mocker): params=[fake_coldkey], block_hash=mock_determine_block_hash.return_value, ) - mocked_decode_account_id.assert_has_calls( - [mocker.call(dest_value_1.value[0]), mocker.call(dest_value_2.value[0])] - ) assert result == {0: fake_hk_1, 1: fake_hk_2} @@ -4657,12 +4522,12 @@ async def test_get_crowdloan_constants(mocker, subtensor): async def test_get_crowdloan_contributions(mocker, subtensor): """Tests subtensor `get_crowdloan_contributions` method.""" # Preps - fake_hk_array = mocker.Mock(spec=list) + fake_hk = mocker.Mock(spec=str) fake_contribution = mocker.Mock(value=mocker.Mock(spec=Balance)) fake_crowdloan_id = mocker.Mock(spec=int) mocked_determine_block_hash = mocker.patch.object(subtensor, "determine_block_hash") - records = [(fake_hk_array, fake_contribution)] + records = [(fake_hk, fake_contribution)] fake_result = mocker.AsyncMock(autospec=list) fake_result.records = records fake_result.__aiter__.return_value = iter(records) @@ -4671,7 +4536,6 @@ async def test_get_crowdloan_contributions(mocker, subtensor): subtensor.substrate, "query_map", return_value=fake_result ) - mocked_decode_account_id = mocker.patch.object(async_subtensor, "decode_account_id") mocked_from_rao = mocker.patch.object(async_subtensor.Balance, "from_rao") # Call @@ -4685,9 +4549,7 @@ async def test_get_crowdloan_contributions(mocker, subtensor): params=[fake_crowdloan_id], block_hash=mocked_determine_block_hash.return_value, ) - assert result == { - mocked_decode_account_id.return_value: mocked_from_rao.return_value - } + assert result == {fake_hk: mocked_from_rao.return_value} @pytest.mark.parametrize( @@ -4697,12 +4559,10 @@ async def test_get_crowdloan_contributions(mocker, subtensor): async def test_get_crowdloan_by_id(mocker, subtensor, query_return, expected_result): """Tests subtensor `get_crowdloan_by_id` method.""" # Preps - fake_crowdloan_id = mocker.Mock(spec=int) + fake_crowdloan_id = mocker.Mock(spec=ScaleType, value=mocker.Mock(spec=int)) mocked_determine_block_hash = mocker.patch.object(subtensor, "determine_block_hash") - mocked_query_return = ( - None if query_return is None else mocker.Mock(value=query_return) - ) + mocked_query_return = mocker.Mock(value=query_return) mocked_query = mocker.patch.object( subtensor.substrate, "query", return_value=mocked_query_return ) @@ -4756,7 +4616,7 @@ async def test_get_crowdloans(mocker, subtensor): """Tests subtensor `get_crowdloans` method.""" # Preps fake_id = mocker.Mock(spec=int) - fake_crowdloan = mocker.Mock(value=mocker.Mock(spec=dict)) + fake_crowdloan = mocker.Mock(spec=dict) mocked_determine_block_hash = mocker.patch.object(subtensor, "determine_block_hash") records = [(fake_id, fake_crowdloan)] @@ -4785,7 +4645,7 @@ async def test_get_crowdloans(mocker, subtensor): ) mocked_decode_crowdloan_entry.assert_awaited_once_with( crowdloan_id=fake_id, - data=fake_crowdloan.value, + data=fake_crowdloan, block_hash=mocked_determine_block_hash.return_value, ) assert result == [mocked_decode_crowdloan_entry.return_value] @@ -4841,23 +4701,21 @@ async def test_commit_weights_with_zero_max_attempts( @pytest.mark.parametrize( "fake_result, expected_result", [ - ({"Swap": ()}, "Swap"), - ({"Keep": ()}, "Keep"), + ("Swap", "Swap"), + ("Keep", "Keep"), ( { "KeepSubnets": { - "subnets": ( - ( - 2, - 3, - ), - ) + "subnets": [ + 2, + 3, + ], } }, {"KeepSubnets": {"subnets": [2, 3]}}, ), ( - {"KeepSubnets": {"subnets": ((2,),)}}, + {"KeepSubnets": {"subnets": [2]}}, { "KeepSubnets": { "subnets": [ @@ -4877,7 +4735,9 @@ async def test_get_root_claim_type(mocker, subtensor, fake_result, expected_resu fake_coldkey_ss58 = mocker.Mock(spec=str) mocked_determine_block_hash = mocker.patch.object(subtensor, "determine_block_hash") mocked_map = mocker.patch.object( - subtensor.substrate, "query", return_value=fake_result + subtensor.substrate, + "query", + return_value=mocker.Mock(spec=ScaleType, value=fake_result), ) # call @@ -4890,7 +4750,6 @@ async def test_get_root_claim_type(mocker, subtensor, fake_result, expected_resu storage_function="RootClaimType", params=[fake_coldkey_ss58], block_hash=mocked_determine_block_hash.return_value, - reuse_block_hash=False, ) assert result == expected_result @@ -4926,8 +4785,8 @@ async def test_get_root_claimable_all_rates(mocker, subtensor): # Preps hotkey_ss58 = mocker.Mock(spec=str) mocked_determine_block_hash = mocker.patch.object(subtensor, "determine_block_hash") - fake_value = [((14, {"bits": 6520190}),)] - fake_result = mocker.MagicMock(value=fake_value) + fake_value = [(14, {"bits": 6520190})] + fake_result = mocker.MagicMock(spec=ScaleType, value=fake_value) fake_result.__iter__ = fake_value mocked_query = mocker.patch.object( subtensor.substrate, "query", return_value=fake_result @@ -4946,7 +4805,6 @@ async def test_get_root_claimable_all_rates(mocker, subtensor): storage_function="RootClaimable", params=[hotkey_ss58], block_hash=mocked_determine_block_hash.return_value, - reuse_block_hash=False, ) mocked_fixed_to_float.assert_called_once_with({"bits": 6520190}, frac_bits=32) assert result == {14: mocked_fixed_to_float.return_value} @@ -5030,7 +4888,6 @@ async def test_get_root_claimed(mocker, subtensor): storage_function="RootClaimed", params=[netuid, hotkey_ss58, coldkey_ss58], block_hash=mocked_determine_block_hash.return_value, - reuse_block_hash=False, ) assert result == Balance.from_rao(1).set_unit(netuid) @@ -5208,7 +5065,6 @@ async def test_get_proxies(subtensor, mocker): module="Proxy", storage_function="Proxies", block_hash=mocked_determine_block_hash.return_value, - reuse_block_hash=False, ) mocked_from_query_map_record.assert_called_once_with(fake_record) assert result == {fake_real_account: [fake_proxy_list]} @@ -5242,7 +5098,6 @@ async def test_get_proxies_for_real_account(subtensor, mocker): storage_function="Proxies", params=[fake_real_account_ss58], block_hash=mocked_determine_block_hash.return_value, - reuse_block_hash=False, ) mocked_from_query.assert_called_once_with(mocked_query.return_value) assert result == mocked_from_query.return_value @@ -5275,9 +5130,8 @@ async def test_get_proxy_announcement(subtensor, mocker): storage_function="Announcements", params=[fake_delegate_account_ss58], block_hash=mocked_determine_block_hash.return_value, - reuse_block_hash=False, ) - mocked_from_dict.assert_called_once_with(mocked_query.return_value.value[0]) + mocked_from_dict.assert_called_once_with(mocked_query.return_value.value) assert result == mocked_from_dict.return_value @@ -5316,7 +5170,6 @@ async def test_get_proxy_announcements(subtensor, mocker): module="Proxy", storage_function="Announcements", block_hash=mocked_determine_block_hash.return_value, - reuse_block_hash=False, ) mocked_from_query_map_record.assert_called_once_with(fake_record) assert result == {fake_delegate: fake_proxies_list} @@ -5846,10 +5699,10 @@ async def test_get_stake_info_for_coldkeys_success(subtensor, mocker): fake_block_hash = None fake_reuse_block = False - fake_ck1 = b"\x16:\xech\r\xde,g\x03R1\xb9\x88q\xe79\xb8\x88\x93\xae\xd2)?*\rp\xb2\xe62\xads\x1c" - fake_ck2 = b"\x17:\xech\r\xde,g\x03R1\xb9\x88q\xe79\xb8\x88\x93\xae\xd2)?*\rp\xb2\xe62\xads\x1d" - fake_decoded_ck1 = "decoded_coldkey1" - fake_decoded_ck2 = "decoded_coldkey2" + fake_ck1 = fake_coldkey_ss58s[0] + fake_ck2 = fake_coldkey_ss58s[1] + fake_decoded_ck1 = fake_coldkey_ss58s[0] + fake_decoded_ck2 = fake_coldkey_ss58s[1] stake_info_dict_1 = { "netuid": 1, @@ -5882,12 +5735,6 @@ async def test_get_stake_info_for_coldkeys_success(subtensor, mocker): ) subtensor.query_runtime_api = mocked_query_runtime_api - mocked_decode_account_id = mocker.patch.object( - async_subtensor, - "decode_account_id", - side_effect=[fake_decoded_ck1, fake_decoded_ck2], - ) - mock_stake_info_1 = mocker.Mock(spec=StakeInfo) mock_stake_info_2 = mocker.Mock(spec=StakeInfo) mocked_stake_info_list_from_dicts = mocker.patch.object( @@ -5917,9 +5764,6 @@ async def test_get_stake_info_for_coldkeys_success(subtensor, mocker): block_hash=fake_block_hash, reuse_block=fake_reuse_block, ) - mocked_decode_account_id.assert_has_calls( - [mocker.call(fake_ck1), mocker.call(fake_ck2)] - ) mocked_stake_info_list_from_dicts.assert_has_calls( [mocker.call([stake_info_dict_1]), mocker.call([stake_info_dict_2])] ) @@ -5931,12 +5775,13 @@ async def test_get_mev_shield_current_key_success(subtensor, mocker): # Prep fake_block = 123 fake_block_hash = "0x123abc" - fake_public_key_bytes = b"\x00" * 1184 # ML-KEM-768 public key size + fake_public_key_bytes = bytearray(b"\x00" * 1184) # ML-KEM-768 public key size mocked_determine_block_hash = mocker.AsyncMock(return_value=fake_block_hash) mocker.patch.object(subtensor, "determine_block_hash", mocked_determine_block_hash) - mocked_query = mocker.AsyncMock() - mocked_query.return_value = iter([fake_public_key_bytes]) + mocked_query = mocker.AsyncMock( + return_value=mocker.Mock(spec=ScaleType, value_object=fake_public_key_bytes) + ) subtensor.substrate.query = mocked_query # Call @@ -5961,7 +5806,9 @@ async def test_get_mev_shield_current_key_none(subtensor, mocker): mocked_determine_block_hash = mocker.AsyncMock(return_value=fake_block_hash) mocker.patch.object(subtensor, "determine_block_hash", mocked_determine_block_hash) - mocked_query = mocker.AsyncMock(return_value=None) + mocked_query = mocker.AsyncMock( + return_value=mocker.Mock(spec=ScaleType, value_object=None) + ) subtensor.substrate.query = mocked_query # Call @@ -5983,12 +5830,13 @@ async def test_get_mev_shield_current_key_invalid_size(subtensor, mocker): # Prep fake_block = 123 fake_block_hash = "0x123abc" - fake_public_key_bytes = b"\x00" * 1000 # Invalid size + fake_public_key_bytes = bytearray(b"\x00" * 1000) # Invalid size mocked_determine_block_hash = mocker.AsyncMock(return_value=fake_block_hash) mocker.patch.object(subtensor, "determine_block_hash", mocked_determine_block_hash) - mocked_query = mocker.AsyncMock() - mocked_query.return_value = iter([fake_public_key_bytes]) + mocked_query = mocker.AsyncMock( + return_value=mocker.Mock(spec=ScaleType, value_object=fake_public_key_bytes) + ) subtensor.substrate.query = mocked_query # Call & Assert @@ -6010,12 +5858,13 @@ async def test_get_mev_shield_next_key_success(subtensor, mocker): # Prep fake_block = 123 fake_block_hash = "0x123abc" - fake_public_key_bytes = b"\x00" * 1184 # ML-KEM-768 public key size + fake_public_key_bytes = bytearray(b"\x00" * 1184) # ML-KEM-768 public key size mocked_determine_block_hash = mocker.AsyncMock(return_value=fake_block_hash) mocker.patch.object(subtensor, "determine_block_hash", mocked_determine_block_hash) - mocked_query = mocker.AsyncMock() - mocked_query.return_value = iter([fake_public_key_bytes]) + mocked_query = mocker.AsyncMock( + return_value=mocker.Mock(spec=ScaleType, value_object=fake_public_key_bytes) + ) subtensor.substrate.query = mocked_query # Call @@ -6040,7 +5889,9 @@ async def test_get_mev_shield_next_key_none(subtensor, mocker): mocked_determine_block_hash = mocker.AsyncMock(return_value=fake_block_hash) mocker.patch.object(subtensor, "determine_block_hash", mocked_determine_block_hash) - mocked_query = mocker.AsyncMock(return_value=None) + mocked_query = mocker.AsyncMock( + return_value=mocker.Mock(spec=ScaleType, value_object=None) + ) subtensor.substrate.query = mocked_query # Call @@ -6062,12 +5913,13 @@ async def test_get_mev_shield_next_key_invalid_size(subtensor, mocker): # Prep fake_block = 123 fake_block_hash = "0x123abc" - fake_public_key_bytes = b"\x00" * 1000 # Invalid size + fake_public_key_bytes = bytearray(b"\x00" * 1000) # Invalid size mocked_determine_block_hash = mocker.AsyncMock(return_value=fake_block_hash) mocker.patch.object(subtensor, "determine_block_hash", mocked_determine_block_hash) - mocked_query = mocker.AsyncMock() - mocked_query.return_value = iter([fake_public_key_bytes]) + mocked_query = mocker.AsyncMock( + return_value=mocker.Mock(spec=ScaleType, value_object=fake_public_key_bytes) + ) subtensor.substrate.query = mocked_query # Call & Assert @@ -6198,7 +6050,7 @@ async def test_get_start_call_delay(subtensor, mocker): block_hash=None, reuse_block=False, ) - assert result == mocked_query_subtensor.return_value + assert result == mocked_query_subtensor.return_value.value @pytest.mark.asyncio @@ -6225,7 +6077,6 @@ async def test_get_coldkey_swap_announcement(subtensor, mocker): storage_function="ColdkeySwapAnnouncements", params=[fake_coldkey_ss58], block_hash=mocked_determine_block_hash.return_value, - reuse_block_hash=False, ) mocked_from_query.assert_called_once_with( coldkey_ss58=fake_coldkey_ss58, query=mocked_query.return_value @@ -6257,7 +6108,6 @@ async def test_get_coldkey_swap_announcements(subtensor, mocker): module="SubtensorModule", storage_function="ColdkeySwapAnnouncements", block_hash=mocked_determine_block_hash.return_value, - reuse_block_hash=False, ) mocked_from_record.assert_called_once_with(fake_record) assert result == [mocked_from_record.return_value] @@ -6279,7 +6129,6 @@ async def test_get_coldkey_swap_announcement_delay(subtensor, mocker): module="SubtensorModule", storage_function="ColdkeySwapAnnouncementDelay", block_hash=mocked_determine_block_hash.return_value, - reuse_block_hash=False, ) assert result == mocked_query.return_value.value @@ -6300,7 +6149,6 @@ async def test_get_coldkey_swap_reannouncement_delay(subtensor, mocker): module="SubtensorModule", storage_function="ColdkeySwapReannouncementDelay", block_hash=mocked_determine_block_hash.return_value, - reuse_block_hash=False, ) assert result == mocked_query.return_value.value @@ -6425,7 +6273,6 @@ async def test_get_coldkey_swap_dispute(subtensor, mocker): storage_function="ColdkeySwapDisputes", params=[fake_coldkey_ss58], block_hash=mocked_determine_block_hash.return_value, - reuse_block_hash=False, ) mocked_from_query.assert_called_once_with( coldkey_ss58=fake_coldkey_ss58, query=mocked_query.return_value @@ -6457,7 +6304,6 @@ async def test_get_coldkey_swap_disputes(subtensor, mocker): module="SubtensorModule", storage_function="ColdkeySwapDisputes", block_hash=mocked_determine_block_hash.return_value, - reuse_block_hash=False, ) mocked_from_record.assert_called_once_with(fake_record) assert result == [mocked_from_record.return_value] diff --git a/tests/unit_tests/test_config.py b/tests/unit_tests/test_config.py index 18b8ee36d5..b7bdbc61df 100644 --- a/tests/unit_tests/test_config.py +++ b/tests/unit_tests/test_config.py @@ -1,5 +1,7 @@ +import copy import bittensor import argparse +from bittensor.core.config import Config, DefaultMunch def test_py_config_parsed_successfully_rust_wallet(): @@ -29,3 +31,45 @@ def test_py_config_parsed_successfully_rust_wallet(): assert wallet_two.name == config.wallet.name assert wallet_two.hotkey_str == config.wallet.hotkey assert wallet_two.path == config.wallet.path + + +def test_deepcopy_default_munch(): + """ + deepcopy of a nested DefaultMunch must not fail via __reduce_ex__. + + In Python 3.10, copy.deepcopy falls back to getattr(x, "__reduce_ex__") for + dict subclasses that are not in _deepcopy_dispatch. DefaultMunch.__getattr__ + intercepts that lookup and returns None (the configured default value) instead + of the real method, causing TypeError: 'NoneType' object is not callable. + DefaultMunch.__deepcopy__ prevents this by giving deepcopy an explicit path. + """ + original = DefaultMunch.fromDict( + {"port": 8091, "ip": "[::]", "external_port": None, "nested": {"x": 1}} + ) + cloned = copy.deepcopy(original) + + assert cloned.port == 8091 + assert cloned.ip == "[::]" + assert cloned.external_port is None + assert cloned.nested.x == 1 + + # Mutations to the clone must not affect the original + cloned.port = 9999 + cloned.nested.x = 42 + assert original.port == 8091 + assert original.nested.x == 1 + + +def test_deepcopy_config_with_nested_defaults(): + """deepcopy of a full Config (including nested DefaultMunch values) works.""" + parser = argparse.ArgumentParser() + bittensor.Subtensor.add_args(parser) + config = Config(parser) + + cloned = copy.deepcopy(config) + + assert cloned.subtensor.network == config.subtensor.network + + # Mutations to the clone must not affect the original + cloned.subtensor.network = "mutated" + assert config.subtensor.network != "mutated" diff --git a/tests/unit_tests/test_stream.py b/tests/unit_tests/test_stream.py index be333d59b3..b40a51b373 100644 --- a/tests/unit_tests/test_stream.py +++ b/tests/unit_tests/test_stream.py @@ -5,9 +5,7 @@ """ import pytest -from abc import ABC -from typing import Optional -from unittest.mock import AsyncMock, Mock, MagicMock, patch +from unittest.mock import AsyncMock, Mock from aiohttp import ClientResponse from starlette.types import Send, Receive, Scope diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index 0a55814fd7..d2324d435d 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -6,9 +6,10 @@ import pytest import websockets from async_substrate_interface import sync_substrate -from async_substrate_interface.types import Runtime, ScaleObj +from async_substrate_interface.types import Runtime from bittensor_wallet import Wallet from scalecodec import GenericCall +from scalecodec.base import ScaleType from bittensor import StakeInfo from bittensor.core import settings @@ -40,6 +41,16 @@ def fake_call_params(): return call_params() +@pytest.fixture +def scale_type_none(mocker): + return mocker.Mock(spec=ScaleType, value=None) + + +@pytest.fixture +def scale_type(mocker): + return mocker.Mock(spec=ScaleType) + + def call_params(): return AxonServeCallParams( version=settings.version_as_int, @@ -290,10 +301,10 @@ def test_hyperparameter_subnet_does_not_exist(subtensor, mocker): subtensor.subnet_exists.assert_called_once_with(1, block=None) -def test_hyperparameter_result_is_none(subtensor, mocker): +def test_hyperparameter_result_is_none(subtensor, mocker, scale_type_none): """Tests when query_subtensor returns None.""" subtensor.subnet_exists = mocker.MagicMock(return_value=True) - subtensor.substrate.query = mocker.MagicMock(return_value=None) + subtensor.substrate.query = mocker.MagicMock(return_value=scale_type_none) assert subtensor.get_hyperparameter("Difficulty", 1, None) is None subtensor.subnet_exists.assert_called_once_with(1, block=None) subtensor.substrate.query.assert_called_once_with( @@ -304,10 +315,10 @@ def test_hyperparameter_result_is_none(subtensor, mocker): ) -def test_hyperparameter_result_has_no_value(subtensor, mocker): +def test_hyperparameter_result_has_no_value(subtensor, mocker, scale_type_none): """Test when the result has no 'value' attribute.""" subtensor.subnet_exists = mocker.MagicMock(return_value=True) - subtensor.substrate.query = mocker.MagicMock(return_value=None) + subtensor.substrate.query = mocker.MagicMock(return_value=scale_type_none) assert subtensor.get_hyperparameter("Difficulty", 1, None) is None subtensor.subnet_exists.assert_called_once_with(1, block=None) subtensor.substrate.query.assert_called_once_with( @@ -543,7 +554,9 @@ def test_commit_reveal_enabled(subtensor, mocker): # Preps netuid = 1 block = 123 - mocked_get_hyperparameter = mocker.patch.object(subtensor, "get_hyperparameter") + mocked_get_hyperparameter = mocker.patch.object( + subtensor, "get_hyperparameter", return_value=False + ) # Call result = subtensor.commit_reveal_enabled(netuid, block) @@ -911,10 +924,6 @@ def test_query_runtime_api(subtensor, mocker): subtensor, "determine_block_hash", ) - # mock_runtime_call = mocker.patch.object( - # subtensor.substrate, - # "runtime_call", - # ) # Call result = subtensor.query_runtime_api(fake_runtime_api, fake_method, None) @@ -928,7 +937,7 @@ def test_query_runtime_api(subtensor, mocker): ) mock_determine_block_hash.assert_called_once_with(None) - assert result == subtensor.substrate.runtime_call.return_value.value + assert result == subtensor.substrate.runtime_call.return_value def test_query_map_subtensor(subtensor, mocker): @@ -1371,7 +1380,7 @@ def test_neuron_for_uid_response_none(subtensor, mocker): subtensor_module.NeuronInfo, "get_null_neuron" ) - subtensor.substrate.runtime_call.return_value.value = None + subtensor.substrate.runtime_call.return_value = None # Call result = subtensor.neuron_for_uid( @@ -1501,10 +1510,7 @@ def test_get_commitment(subtensor, mocker): fake_uid = 2 fake_block = 3 fake_hotkey = "hotkey" - expected_result = ( - "{'peer_id': '12D3KooWFWnHBmUFxvfL6PfZ5eGHdhgsEqNnsxuN1HE9EtfW8THi', " - "'model_huggingface_id': 'kmfoda/gpt2-1b-miner-3'}" - ) + expected_result = "{'peer_id': '12D3KooWRNw54AW4zrQWeZL2buhU8P71gfo9PXQQAHUwEAe4hA34', 'model_huggingface_id': None}" mocked_metagraph = mocker.MagicMock() subtensor.metagraph = mocked_metagraph @@ -1515,133 +1521,11 @@ def test_get_commitment(subtensor, mocker): "deposit": 0, "block": 3843930, "info": { - "fields": ( - ( - { - "Raw117": ( - ( - 123, - 39, - 112, - 101, - 101, - 114, - 95, - 105, - 100, - 39, - 58, - 32, - 39, - 49, - 50, - 68, - 51, - 75, - 111, - 111, - 87, - 70, - 87, - 110, - 72, - 66, - 109, - 85, - 70, - 120, - 118, - 102, - 76, - 54, - 80, - 102, - 90, - 53, - 101, - 71, - 72, - 100, - 104, - 103, - 115, - 69, - 113, - 78, - 110, - 115, - 120, - 117, - 78, - 49, - 72, - 69, - 57, - 69, - 116, - 102, - 87, - 56, - 84, - 72, - 105, - 39, - 44, - 32, - 39, - 109, - 111, - 100, - 101, - 108, - 95, - 104, - 117, - 103, - 103, - 105, - 110, - 103, - 102, - 97, - 99, - 101, - 95, - 105, - 100, - 39, - 58, - 32, - 39, - 107, - 109, - 102, - 111, - 100, - 97, - 47, - 103, - 112, - 116, - 50, - 45, - 49, - 98, - 45, - 109, - 105, - 110, - 101, - 114, - 45, - 51, - 39, - 125, - ), - ) - }, - ), - ) + "fields": [ + { + "Raw97": "0x7b27706565725f6964273a2027313244334b6f6f57524e7735344157347a725157655a4c32627568553850373167666f3950585151414855774541653468413334272c20276d6f64656c5f68756767696e67666163655f6964273a204e6f6e657d" + } + ], }, } @@ -1663,7 +1547,6 @@ def test_get_last_commitment_bonds_reset_block(subtensor, mocker): fake_hotkey = "hotkey" mocked_get_last_bonds_reset = mocker.patch.object(subtensor, "get_last_bonds_reset") - mocked_decode_block = mocker.patch.object(subtensor_module, "decode_block") mocked_metagraph = mocker.MagicMock() subtensor.metagraph = mocked_metagraph @@ -1677,10 +1560,6 @@ def test_get_last_commitment_bonds_reset_block(subtensor, mocker): # Assertions mocked_metagraph.assert_called_once_with(fake_netuid, block=None) mocked_get_last_bonds_reset.assert_called_once_with(fake_netuid, fake_hotkey, None) - mocked_decode_block.assert_called_once_with( - mocked_get_last_bonds_reset.return_value - ) - assert result == mocked_decode_block.return_value def test_min_allowed_weights(subtensor, mocker): @@ -1922,28 +1801,6 @@ def test_difficulty_success(subtensor, mocker): assert result == int(mocked_get_hyperparameter.return_value) -def test_difficulty_none(subtensor, mocker): - """Tests difficulty method with None result.""" - # Preps - mocked_get_hyperparameter = mocker.patch.object( - subtensor, "get_hyperparameter", return_value=None - ) - fake_netuid = 1 - fake_block = 2 - - # Call - result = subtensor.difficulty(fake_netuid, fake_block) - - # Asserts - mocked_get_hyperparameter.assert_called_once_with( - param_name="Difficulty", - netuid=fake_netuid, - block=fake_block, - ) - - assert result is None - - def test_recycle_success(subtensor, mocker): """Tests recycle method with successfully result.""" # Preps @@ -2142,11 +1999,6 @@ def test_does_hotkey_exist_true(mocker, subtensor): "query", return_value=mocker.Mock(value=[fake_owner]), ) - mocker.patch.object( - subtensor_module, - "decode_account_id", - return_value=fake_owner, - ) # Call result = subtensor.does_hotkey_exist(fake_hotkey_ss58, block=fake_block) @@ -2162,50 +2014,20 @@ def test_does_hotkey_exist_true(mocker, subtensor): assert result is True -def test_does_hotkey_exist_no_value(mocker, subtensor): - """Test when query_subtensor returns no value.""" - # Mock data - fake_hotkey_ss58 = "fake_hotkey" - fake_block = 123 - - # Mocks - mock_query_subtensor = mocker.patch.object( - subtensor.substrate, "query", return_value=None - ) - - # Call - result = subtensor.does_hotkey_exist(fake_hotkey_ss58, block=fake_block) - - # Assertions - mock_query_subtensor.assert_called_once_with( - module="SubtensorModule", - storage_function="Owner", - params=[fake_hotkey_ss58], - block_hash=subtensor.substrate.get_block_hash.return_value, - ) - subtensor.substrate.get_block_hash.assert_called_once_with(fake_block) - assert result is False - - -def test_does_hotkey_exist_special_id(mocker, subtensor): +def test_does_hotkey_exist_special_id(mocker, subtensor, scale_type): """Test when query_subtensor returns the special invalid owner identifier.""" # Mock data fake_hotkey_ss58 = "fake_hotkey" fake_owner = "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM" fake_block = 123 + scale_type.value = fake_owner # Mocks mock_query_subtensor = mocker.patch.object( subtensor.substrate, "query", - return_value=fake_owner, - ) - mocker.patch.object( - subtensor_module, - "decode_account_id", - return_value=fake_owner, + return_value=scale_type, ) - # Call result = subtensor.does_hotkey_exist(fake_hotkey_ss58, block=fake_block) @@ -2232,11 +2054,6 @@ def test_does_hotkey_exist_latest_block(mocker, subtensor): "query", return_value=mocker.Mock(value=[fake_owner]), ) - mocker.patch.object( - subtensor_module, - "decode_account_id", - return_value=fake_owner, - ) # Call result = subtensor.does_hotkey_exist(fake_hotkey_ss58) @@ -2251,16 +2068,17 @@ def test_does_hotkey_exist_latest_block(mocker, subtensor): assert result is True -def test_get_hotkey_owner_success(mocker, subtensor): +def test_get_hotkey_owner_success(mocker, subtensor, scale_type): """Test when hotkey exists and owner is found.""" # Mock data fake_hotkey_ss58 = "fake_hotkey" fake_coldkey_ss58 = "fake_coldkey" fake_block = 123 + scale_type.value = fake_coldkey_ss58 # Mocks mock_query_subtensor = mocker.patch.object( - subtensor.substrate, "query", return_value=fake_coldkey_ss58 + subtensor.substrate, "query", return_value=scale_type ) mock_does_hotkey_exist = mocker.patch.object( subtensor, "does_hotkey_exist", return_value=True @@ -2281,37 +2099,6 @@ def test_get_hotkey_owner_success(mocker, subtensor): assert result == fake_coldkey_ss58 -def test_get_hotkey_owner_no_value(mocker, subtensor): - """Test when query_subtensor returns no value.""" - # Mock data - fake_hotkey_ss58 = "fake_hotkey" - fake_block = 123 - - # Mocks - mock_query_subtensor = mocker.patch.object( - subtensor.substrate, - "query", - return_value=None, - ) - mock_does_hotkey_exist = mocker.patch.object( - subtensor, "does_hotkey_exist", return_value=True - ) - - # Call - result = subtensor.get_hotkey_owner(fake_hotkey_ss58, block=fake_block) - - # Assertions - mock_query_subtensor.assert_called_once_with( - module="SubtensorModule", - storage_function="Owner", - params=[fake_hotkey_ss58], - block_hash=subtensor.substrate.get_block_hash.return_value, - ) - mock_does_hotkey_exist.assert_not_called() - subtensor.substrate.get_block_hash.assert_called_once_with(fake_block) - assert result is None - - def test_get_hotkey_owner_does_not_exist(mocker, subtensor): """Test when hotkey does not exist.""" # Mock data @@ -2327,11 +2114,6 @@ def test_get_hotkey_owner_does_not_exist(mocker, subtensor): mock_does_hotkey_exist = mocker.patch.object( subtensor, "does_hotkey_exist", return_value=False ) - mocker.patch.object( - subtensor_module, - "decode_account_id", - return_value=fake_hotkey_ss58, - ) # Call result = subtensor.get_hotkey_owner(fake_hotkey_ss58, block=fake_block) @@ -2348,15 +2130,16 @@ def test_get_hotkey_owner_does_not_exist(mocker, subtensor): assert result is None -def test_get_hotkey_owner_latest_block(mocker, subtensor): +def test_get_hotkey_owner_latest_block(mocker, subtensor, scale_type): """Test when no block is provided (latest block).""" # Mock data fake_hotkey_ss58 = "fake_hotkey" fake_coldkey_ss58 = "fake_coldkey" + scale_type.value = fake_coldkey_ss58 # Mocks mock_query_subtensor = mocker.patch.object( - subtensor.substrate, "query", return_value=fake_coldkey_ss58 + subtensor.substrate, "query", return_value=scale_type ) mock_does_hotkey_exist = mocker.patch.object( subtensor, "does_hotkey_exist", return_value=True @@ -2437,7 +2220,7 @@ def test_get_minimum_required_stake_invalid_result(mocker, subtensor): mock_query.assert_called_once_with( module="SubtensorModule", storage_function="NominatorMinRequiredStake" ) - mock_balance_from_rao.assert_called_once_with(fake_invalid_stake) + mock_balance_from_rao.assert_called_once_with(0) assert result == mock_balance_from_rao.return_value @@ -3096,7 +2879,9 @@ def test_get_all_neuron_certificates(mocker, subtensor): def test_get_timestamp(mocker, subtensor): fake_block = 1000 - mocked_query = mocker.MagicMock(return_value=ScaleObj(1740586018 * 1000)) + mock_return = mocker.MagicMock(spec=ScaleType) + mock_return.value = 1740586018 * 1000 + mocked_query = mocker.MagicMock(return_value=mock_return) mocker.patch.object(subtensor.substrate, "query", mocked_query) expected_result = datetime.datetime( 2025, 2, 26, 16, 6, 58, tzinfo=datetime.timezone.utc @@ -3110,19 +2895,10 @@ def test_get_owned_hotkeys_happy_path(subtensor, mocker): # Prep fake_coldkey = "fake_hotkey" fake_hotkey = "fake_hotkey" - fake_hotkeys = [ - [ - fake_hotkey, - ] - ] + fake_hotkeys = mocker.MagicMock(spec=ScaleType, value=[fake_hotkey]) mocked_subtensor = mocker.Mock(return_value=fake_hotkeys) mocker.patch.object(subtensor.substrate, "query", new=mocked_subtensor) - mocked_decode_account_id = mocker.Mock() - mocker.patch.object( - subtensor_module, "decode_account_id", new=mocked_decode_account_id - ) - # Call result = subtensor.get_owned_hotkeys(fake_coldkey) @@ -3133,16 +2909,17 @@ def test_get_owned_hotkeys_happy_path(subtensor, mocker): params=[fake_coldkey], block_hash=None, ) - assert result == [mocked_decode_account_id.return_value] - mocked_decode_account_id.assert_called_once_with(fake_hotkey) + assert result == fake_hotkeys.value def test_get_owned_hotkeys_return_empty(subtensor, mocker): """Tests that the output of get_owned_hotkeys is empty.""" # Prep fake_coldkey = "fake_hotkey" - mocked_subtensor = mocker.Mock(return_value=[]) - mocker.patch.object(subtensor.substrate, "query", new=mocked_subtensor) + mocked_return = mocker.MagicMock(spec=ScaleType, value=[]) + mocked_subtensor = mocker.patch.object( + subtensor.substrate, "query", return_value=mocked_return + ) # Call result = subtensor.get_owned_hotkeys(fake_coldkey) @@ -3262,7 +3039,7 @@ def test_get_metagraph_info_all_fields(subtensor, mocker): mock_runtime_call = mocker.patch.object( subtensor.substrate, "runtime_call", - return_value=mocker.Mock(value=mock_value), + return_value=mock_value, ) mock_chain_head = mocker.patch.object( subtensor.substrate, @@ -3273,21 +3050,15 @@ def test_get_metagraph_info_all_fields(subtensor, mocker): subtensor_module.MetagraphInfo, "from_dict", return_value="parsed_metagraph" ) mocked_runtime_metadata_v15 = { - "apis": [ - { - "name": "SubnetInfoRuntimeApi", - "methods": [ - {"name": "get_selective_metagraph"}, - {"name": "get_metagraph"}, - {"name": "get_selective_mechagraph"}, - ], - }, - ] + "SubnetInfoRuntimeApi": { + "get_selective_metagraph": {"name": "get_selective_metagraph"}, + "get_metagraph": {"name": "get_metagraph"}, + "get_selective_mechagraph": {"name": "get_selective_mechagraph"}, + }, } mocked_runtime = mocker.Mock(spec=Runtime) - mocked_metadata = mocker.Mock() - mocked_metadata.value.return_value = mocked_runtime_metadata_v15 - mocked_runtime.metadata_v15 = mocked_metadata + mocked_runtime.metadata_v15 = mocker.Mock() + mocked_runtime.runtime_api_map = mocked_runtime_metadata_v15 mocker.patch.object( subtensor.substrate, "init_runtime", @@ -3321,7 +3092,7 @@ def test_get_metagraph_info_specific_fields(subtensor, mocker): mock_runtime_call = mocker.patch.object( subtensor.substrate, "runtime_call", - return_value=mocker.Mock(value=mock_value), + return_value=mock_value, ) mock_chain_head = mocker.patch.object( subtensor.substrate, @@ -3329,21 +3100,15 @@ def test_get_metagraph_info_specific_fields(subtensor, mocker): return_value="0xfakechainhead", ) mocked_runtime_metadata_v15 = { - "apis": [ - { - "name": "SubnetInfoRuntimeApi", - "methods": [ - {"name": "get_selective_metagraph"}, - {"name": "get_metagraph"}, - {"name": "get_selective_mechagraph"}, - ], - }, - ] + "SubnetInfoRuntimeApi": { + "get_selective_metagraph": {"name": "get_selective_metagraph"}, + "get_metagraph": {"name": "get_metagraph"}, + "get_selective_mechagraph": {"name": "get_selective_mechagraph"}, + }, } mocked_runtime = mocker.Mock(spec=Runtime) - mocked_metadata = mocker.Mock() - mocked_metadata.value.return_value = mocked_runtime_metadata_v15 - mocked_runtime.metadata_v15 = mocked_metadata + mocked_runtime.metadata_v15 = mocker.Mock() + mocked_runtime.runtime_api_map = mocked_runtime_metadata_v15 mocker.patch.object( subtensor.substrate, "init_runtime", @@ -3384,21 +3149,15 @@ def test_get_metagraph_info_subnet_not_exist(subtensor, mocker): return_value=None, ) mocked_runtime_metadata_v15 = { - "apis": [ - { - "name": "SubnetInfoRuntimeApi", - "methods": [ - {"name": "get_selective_metagraph"}, - {"name": "get_metagraph"}, - {"name": "get_selective_mechagraph"}, - ], - }, - ] + "SubnetInfoRuntimeApi": { + "get_selective_metagraph": {"name": "get_selective_metagraph"}, + "get_metagraph": {"name": "get_metagraph"}, + "get_selective_mechagraph": {"name": "get_selective_mechagraph"}, + }, } mocked_runtime = mocker.Mock(spec=Runtime) - mocked_metadata = mocker.Mock() - mocked_metadata.value.return_value = mocked_runtime_metadata_v15 - mocked_runtime.metadata_v15 = mocked_metadata + mocked_runtime.metadata_v15 = mocker.Mock() + mocked_runtime.runtime_api_map = mocked_runtime_metadata_v15 mocker.patch.object( subtensor.substrate, "init_runtime", @@ -3442,25 +3201,19 @@ def test_get_metagraph_info_older_runtime_version( "runtime_call", ) mocked_runtime_metadata_v15 = { - "apis": [ - { - "name": "SubnetInfoRuntimeApi", - "methods": [ - {"name": "get_selective_metagraph"}, - {"name": "get_metagraph"}, - ], - }, - ] + "SubnetInfoRuntimeApi": { + "get_selective_metagraph": {"name": "get_selective_metagraph"}, + "get_metagraph": {"name": "get_metagraph"}, + }, } if block == 6_800_000: # only the newer block should have 'mechagraph' runtime - mocked_runtime_metadata_v15["apis"][0]["methods"].append( - {"name": "get_selective_mechagraph"} - ) + mocked_runtime_metadata_v15["SubnetInfoRuntimeApi"][ + "get_selective_mechagraph" + ] = {"name": "get_selective_mechagraph"} mocked_runtime = mocker.Mock(spec=Runtime) - mocked_metadata = mocker.Mock() - mocked_metadata.value.return_value = mocked_runtime_metadata_v15 - mocked_runtime.metadata_v15 = mocked_metadata + mocked_runtime.metadata_v15 = mocker.Mock() + mocked_runtime.runtime_api_map = mocked_runtime_metadata_v15 mocker.patch.object( subtensor.substrate, "init_runtime", @@ -3499,12 +3252,12 @@ def test_blocks_since_last_step_with_value(subtensor, mocker): assert result == mocked_query_subtensor.return_value.value -def test_blocks_since_last_step_is_none(subtensor, mocker): +def test_blocks_since_last_step_is_none(subtensor, mocker, scale_type_none): """Test blocks_since_last_step returns None correctly.""" # preps netuid = 1 block = 123 - mocked_query_subtensor = mocker.MagicMock(return_value=None) + mocked_query_subtensor = mocker.MagicMock(return_value=scale_type_none) subtensor.query_subtensor = mocked_query_subtensor # call @@ -3717,19 +3470,14 @@ def test_get_parents_success(subtensor, mocker): fake_netuid = 1 fake_parents = mocker.Mock( value=[ - (1000, ["parent_key_1"]), - (2000, ["parent_key_2"]), + (1000, "decoded_parent_key_1"), + (2000, "decoded_parent_key_2"), ] ) mocked_query = mocker.MagicMock(return_value=fake_parents) subtensor.substrate.query = mocked_query - mocked_decode_account_id = mocker.Mock( - side_effect=["decoded_parent_key_1", "decoded_parent_key_2"] - ) - mocker.patch.object(subtensor_module, "decode_account_id", mocked_decode_account_id) - expected_formatted_parents = [ (u64_normalized_float(1000), "decoded_parent_key_1"), (u64_normalized_float(2000), "decoded_parent_key_2"), @@ -3745,20 +3493,18 @@ def test_get_parents_success(subtensor, mocker): storage_function="ParentKeys", params=[fake_hotkey, fake_netuid], ) - mocked_decode_account_id.assert_has_calls( - [mocker.call("parent_key_1"), mocker.call("parent_key_2")] - ) assert result == expected_formatted_parents -def test_get_parents_no_parents(subtensor, mocker): +def test_get_parents_no_parents(subtensor, mocker, scale_type): """Tests get_parents when there are no parents to retrieve.""" # Preps fake_hotkey = "valid_hotkey" fake_netuid = 1 fake_parents = [] + scale_type.value = fake_parents - mocked_query = mocker.MagicMock(return_value=fake_parents) + mocked_query = mocker.MagicMock(return_value=scale_type) subtensor.substrate.query = mocked_query # Call @@ -3892,18 +3638,16 @@ def test_get_liquidity_list_happy_path(subtensor, fake_wallet, mocker): # Fake positions to return from query_map fake_positions = [ [ - (2,), - mocker.Mock( - value={ - "id": (2,), - "netuid": 2, - "tick_low": (206189,), - "tick_high": (208196,), - "liquidity": 1000000000000, - "fees_tao": {"bits": 0}, - "fees_alpha": {"bits": 0}, - } - ), + 2, + { + "id": 2, + "netuid": 2, + "tick_low": 206189, + "tick_high": 208196, + "liquidity": 1000000000000, + "fees_tao": {"bits": 0}, + "fees_alpha": {"bits": 0}, + }, ], ] fake_result = mocker.MagicMock(records=fake_positions, autospec=list) @@ -3916,10 +3660,9 @@ def test_get_liquidity_list_happy_path(subtensor, fake_wallet, mocker): mocker.patch.object( subtensor.substrate, "create_storage_key", - side_effect=lambda pallet, - storage_function, - params, - block_hash=None: f"{pallet}:{storage_function}:{params}", + side_effect=lambda pallet, storage_function, params, block_hash=None: ( + f"{pallet}:{storage_function}:{params}" + ), ) # Mock query_multi for fee + sqrt_price + tick data @@ -4104,7 +3847,7 @@ def test_get_subnet_price(subtensor, mocker): fake_price = 29258617 expected_price = Balance.from_tao(0.029258617) mocked_query = mocker.patch.object( - subtensor.substrate, "runtime_call", return_value=mocker.Mock(value=fake_price) + subtensor.substrate, "runtime_call", return_value=fake_price ) # Call @@ -4163,10 +3906,8 @@ def test_all_subnets(subtensor, mocker): "get_subnet_prices", return_value={0: Balance.from_tao(1), 1: Balance.from_tao(0.029258617)}, ) - mocked_decode = mocker.Mock(return_value=[{"netuid": 0}, {"netuid": 1}]) - mocked_runtime_call = mocker.Mock(decode=mocked_decode) mocker.patch.object( - subtensor.substrate, "runtime_call", return_value=mocked_runtime_call + subtensor.substrate, "runtime_call", return_value=[{"netuid": 0}, {"netuid": 1}] ) # Call @@ -4198,8 +3939,7 @@ def test_subnet(subtensor, mocker): mocked_get_subnet_price = mocker.patch.object( subtensor, "get_subnet_price", return_value=Balance.from_tao(100.0) ) - mocked_decode = mocker.Mock(return_value={"netuid": netuid}) - mocked_runtime_call = mocker.Mock(decode=mocked_decode) + mocked_runtime_call = {"netuid": netuid} mocker.patch.object( subtensor.substrate, "runtime_call", return_value=mocked_runtime_call ) @@ -4359,6 +4099,7 @@ def test_get_timelocked_weight_commits(subtensor, mocker): storage_function="TimelockedWeightCommits", params=[netuid], block_hash=mock_determine_block_hash.return_value, + page_size=1, ) assert result == [] @@ -4366,7 +4107,7 @@ def test_get_timelocked_weight_commits(subtensor, mocker): @pytest.mark.parametrize( "query_return, expected_result", ( - ["value", [10, 90]], + [[6553, 58982], [10, 90]], [None, None], ), ) @@ -4374,9 +4115,7 @@ def test_get_mechanism_emission_split(subtensor, mocker, query_return, expected_ """Verify that get_mechanism_emission_split calls the correct methods.""" # Preps netuid = mocker.Mock() - query_return = ( - mocker.Mock(value=[6553, 58982]) if query_return == "value" else query_return - ) + query_return = mocker.Mock(value=query_return) mocked_determine_block_hash = mocker.patch.object(subtensor, "determine_block_hash") mocked_query = mocker.patch.object( subtensor.substrate, "query", return_value=query_return @@ -4490,21 +4229,12 @@ def test_get_auto_stakes(subtensor, mocker): fake_hk_1 = mocker.Mock() fake_hk_2 = mocker.Mock() - dest_value_1 = mocker.Mock(value=[fake_hk_1]) - dest_value_2 = mocker.Mock(value=[fake_hk_2]) - mock_result = mocker.MagicMock() - mock_result.__iter__.return_value = iter([(0, dest_value_1), (1, dest_value_2)]) + mock_result.__iter__.return_value = iter([(0, fake_hk_1), (1, fake_hk_2)]) mocked_query_map = mocker.patch.object( subtensor.substrate, "query_map", return_value=mock_result ) - mocked_decode_account_id = mocker.patch.object( - subtensor_module, - "decode_account_id", - side_effect=[fake_hk_1, fake_hk_2], - ) - # Call result = subtensor.get_auto_stakes(coldkey_ss58=fake_coldkey) @@ -4516,9 +4246,6 @@ def test_get_auto_stakes(subtensor, mocker): params=[fake_coldkey], block_hash=mock_determine_block_hash.return_value, ) - mocked_decode_account_id.assert_has_calls( - [mocker.call(dest_value_1.value[0]), mocker.call(dest_value_2.value[0])] - ) assert result == {0: fake_hk_1, 1: fake_hk_2} @@ -4857,15 +4584,17 @@ def test_get_crowdloan_constants(mocker, subtensor): def test_get_crowdloan_contributions(mocker, subtensor): """Tests subtensor `get_crowdloan_contributions` method.""" # Preps - fake_hk_array = mocker.Mock(spec=list) - fake_contribution = mocker.Mock(value=mocker.Mock(spec=Balance)) + fake_hk = mocker.Mock(spec=str) + fake_contribution = mocker.Mock(spec=int) fake_crowdloan_id = mocker.Mock(spec=int) + records = [(fake_hk, fake_contribution)] + fake_result = mocker.MagicMock(autospec=list) + fake_result.records = records + fake_result.__iter__.return_value = iter(records) mocked_determine_block_hash = mocker.patch.object(subtensor, "determine_block_hash") - mocked_query_map = mocker.patch.object(subtensor.substrate, "query_map") - mocked_query_map.return_value.records = [(fake_hk_array, fake_contribution)] - mocked_decode_account_id = mocker.patch.object( - subtensor_module, "decode_account_id" + mocked_query_map = mocker.patch.object( + subtensor.substrate, "query_map", return_value=fake_result ) mocked_from_rao = mocker.patch.object(subtensor_module.Balance, "from_rao") @@ -4874,9 +4603,7 @@ def test_get_crowdloan_contributions(mocker, subtensor): # Asserts mocked_determine_block_hash.assert_called_once() - assert result == { - mocked_decode_account_id.return_value: mocked_from_rao.return_value - } + assert result == {fake_hk: mocked_from_rao.return_value} @pytest.mark.parametrize( @@ -4888,9 +4615,7 @@ def test_get_crowdloan_by_id(mocker, subtensor, query_return, expected_result): fake_crowdloan_id = mocker.Mock(spec=int) mocked_determine_block_hash = mocker.patch.object(subtensor, "determine_block_hash") - mocked_query_return = ( - None if query_return is None else mocker.Mock(value=query_return) - ) + mocked_query_return = mocker.Mock(value=query_return) mocked_query = mocker.patch.object( subtensor.substrate, "query", return_value=mocked_query_return ) @@ -4942,13 +4667,17 @@ def test_get_crowdloans(mocker, subtensor): """Tests subtensor `get_crowdloans` method.""" # Preps fake_id = mocker.Mock(spec=int) - fake_crowdloan = mocker.Mock(value=mocker.Mock(spec=dict)) + fake_crowdloan = mocker.Mock(spec=dict) mocked_determine_block_hash = mocker.patch.object(subtensor, "determine_block_hash") + records = [(fake_id, fake_crowdloan)] + fake_result = mocker.MagicMock(autospec=list) + fake_result.records = records + fake_result.__iter__.return_value = iter(records) mocked_query_map = mocker.patch.object( subtensor.substrate, "query_map", - return_value=mocker.Mock(records=[(fake_id, fake_crowdloan)]), + return_value=fake_result, ) mocked_decode_crowdloan_entry = mocker.patch.object( subtensor, "_decode_crowdloan_entry" @@ -4966,7 +4695,7 @@ def test_get_crowdloans(mocker, subtensor): ) mocked_decode_crowdloan_entry.assert_called_once_with( crowdloan_id=fake_id, - data=fake_crowdloan.value, + data=fake_crowdloan, block_hash=mocked_determine_block_hash.return_value, ) assert result == [mocked_decode_crowdloan_entry.return_value] @@ -5021,23 +4750,14 @@ def test_commit_weights_with_zero_max_attempts( @pytest.mark.parametrize( "fake_result, expected_result", [ - ({"Swap": ()}, "Swap"), - ({"Keep": ()}, "Keep"), + ("Swap", "Swap"), + ("Keep", "Keep"), ( - { - "KeepSubnets": { - "subnets": ( - ( - 2, - 3, - ), - ) - } - }, + {"KeepSubnets": {"subnets": [2, 3]}}, {"KeepSubnets": {"subnets": [2, 3]}}, ), ( - {"KeepSubnets": {"subnets": ((2,),)}}, + {"KeepSubnets": {"subnets": [2]}}, { "KeepSubnets": { "subnets": [ @@ -5048,13 +4768,16 @@ def test_commit_weights_with_zero_max_attempts( ), ], ) -def test_get_root_claim_type(mocker, subtensor, fake_result, expected_result): +def test_get_root_claim_type( + mocker, subtensor, scale_type, fake_result, expected_result +): """Tests that `get_root_claim_type` calls proper methods and returns the correct value.""" # Preps fake_coldkey_ss58 = mocker.Mock(spec=str) mocked_determine_block_hash = mocker.patch.object(subtensor, "determine_block_hash") + scale_type.value = fake_result mocked_map = mocker.patch.object( - subtensor.substrate, "query", return_value=fake_result + subtensor.substrate, "query", return_value=scale_type ) # call @@ -5103,7 +4826,7 @@ def test_get_root_claimable_all_rates(mocker, subtensor): # Preps hotkey_ss58 = mocker.Mock(spec=str) mocked_determine_block_hash = mocker.patch.object(subtensor, "determine_block_hash") - fake_value = [((14, {"bits": 6520190}),)] + fake_value = [(14, {"bits": 6520190})] fake_result = mocker.MagicMock(value=fake_value) fake_result.__iter__ = fake_value mocked_query = mocker.patch.object( @@ -5443,7 +5166,7 @@ def test_get_proxy_announcement(subtensor, mocker): params=[fake_delegate_account_ss58], block_hash=mocked_determine_block_hash.return_value, ) - mocked_from_dict.assert_called_once_with(mocked_query.return_value.value[0]) + mocked_from_dict.assert_called_once_with(mocked_query.return_value.value) assert result == mocked_from_dict.return_value @@ -5978,14 +5701,14 @@ def test_get_stake_info_for_coldkeys_success(subtensor, mocker): fake_coldkey_ss58s = ["coldkey1", "coldkey2"] fake_block = 123 - fake_ck1 = b"\x16:\xech\r\xde,g\x03R1\xb9\x88q\xe79\xb8\x88\x93\xae\xd2)?*\rp\xb2\xe62\xads\x1c" - fake_ck2 = b"\x17:\xech\r\xde,g\x03R1\xb9\x88q\xe79\xb8\x88\x93\xae\xd2)?*\rp\xb2\xe62\xads\x1d" - fake_decoded_ck1 = "decoded_coldkey1" - fake_decoded_ck2 = "decoded_coldkey2" + fake_ck1 = "decoded_coldkey1" + fake_ck2 = "decoded_coldkey2" + fake_decoded_ck1 = fake_ck1 + fake_decoded_ck2 = fake_ck2 stake_info_dict_1 = { "netuid": 5, - "hotkey": b"\x16:\xech\r\xde,g\x03R1\xb9\x88q\xe79\xb8\x88\x93\xae\xd2)?*\rp\xb2\xe62\xads\x1c", + "hotkey": "fake_hk", "coldkey": fake_ck1, "stake": 1000, "locked": 0, @@ -5995,7 +5718,7 @@ def test_get_stake_info_for_coldkeys_success(subtensor, mocker): } stake_info_dict_2 = { "netuid": 14, - "hotkey": b"\x17:\xech\r\xde,g\x03R1\xb9\x88q\xe79\xb8\x88\x93\xae\xd2)?*\rp\xb2\xe62\xads\x1d", + "hotkey": "fake_hk", "coldkey": fake_ck2, "stake": 2000, "locked": 0, @@ -6013,12 +5736,6 @@ def test_get_stake_info_for_coldkeys_success(subtensor, mocker): subtensor, "query_runtime_api", return_value=fake_query_result ) - mocked_decode_account_id = mocker.patch.object( - subtensor_module, - "decode_account_id", - side_effect=[fake_decoded_ck1, fake_decoded_ck2], - ) - mock_stake_info_1 = mocker.Mock(spec=StakeInfo) mock_stake_info_2 = mocker.Mock(spec=StakeInfo) mocked_stake_info_list_from_dicts = mocker.patch.object( @@ -6043,9 +5760,6 @@ def test_get_stake_info_for_coldkeys_success(subtensor, mocker): params=[fake_coldkey_ss58s], block=fake_block, ) - mocked_decode_account_id.assert_has_calls( - [mocker.call(fake_ck1), mocker.call(fake_ck2)] - ) mocked_stake_info_list_from_dicts.assert_has_calls( [mocker.call([stake_info_dict_1]), mocker.call([stake_info_dict_2])] ) @@ -6056,13 +5770,15 @@ def test_get_mev_shield_current_key_success(subtensor, mocker): # Prep fake_block = 123 fake_block_hash = "0x123abc" - fake_public_key_bytes = b"\x00" * 1184 # ML-KEM-768 public key size + fake_public_key_bytes = bytearray(b"\x00" * 1184) # ML-KEM-768 public key size mocked_determine_block_hash = mocker.patch.object( subtensor, "determine_block_hash", return_value=fake_block_hash ) mocked_query = mocker.patch.object(subtensor.substrate, "query") - mocked_query.return_value = iter([fake_public_key_bytes]) + mocked_query.return_value = mocker.Mock( + spec=ScaleType, value_object=fake_public_key_bytes + ) # Call result = subtensor.get_mev_shield_current_key(block=fake_block) @@ -6086,7 +5802,11 @@ def test_get_mev_shield_current_key_none(subtensor, mocker): mocked_determine_block_hash = mocker.patch.object( subtensor, "determine_block_hash", return_value=fake_block_hash ) - mocked_query = mocker.patch.object(subtensor.substrate, "query", return_value=None) + mocked_query = mocker.patch.object( + subtensor.substrate, + "query", + return_value=mocker.Mock(spec=ScaleType, value_object=None), + ) # Call result = subtensor.get_mev_shield_current_key(block=fake_block) @@ -6106,13 +5826,15 @@ def test_get_mev_shield_current_key_invalid_size(subtensor, mocker): # Prep fake_block = 123 fake_block_hash = "0x123abc" - fake_public_key_bytes = b"\x00" * 1000 # Invalid size + fake_public_key_bytes = bytearray(b"\x00" * 1000) # Invalid size mocked_determine_block_hash = mocker.patch.object( subtensor, "determine_block_hash", return_value=fake_block_hash ) mocked_query = mocker.patch.object(subtensor.substrate, "query") - mocked_query.return_value = iter([fake_public_key_bytes]) + mocked_query.return_value = mocker.Mock( + spec=ScaleType, value_object=fake_public_key_bytes + ) # Call & Assert with pytest.raises(ValueError, match="Invalid ML-KEM-768 public key size"): @@ -6132,13 +5854,15 @@ def test_get_mev_shield_next_key_success(subtensor, mocker): # Prep fake_block = 123 fake_block_hash = "0x123abc" - fake_public_key_bytes = b"\x00" * 1184 # ML-KEM-768 public key size + fake_public_key_bytes = bytearray(b"\x00" * 1184) # ML-KEM-768 public key size mocked_determine_block_hash = mocker.patch.object( subtensor, "determine_block_hash", return_value=fake_block_hash ) mocked_query = mocker.patch.object(subtensor.substrate, "query") - mocked_query.return_value = iter([fake_public_key_bytes]) + mocked_query.return_value = mocker.Mock( + spec=ScaleType, value_object=fake_public_key_bytes + ) # Call result = subtensor.get_mev_shield_next_key(block=fake_block) @@ -6162,7 +5886,11 @@ def test_get_mev_shield_next_key_none(subtensor, mocker): mocked_determine_block_hash = mocker.patch.object( subtensor, "determine_block_hash", return_value=fake_block_hash ) - mocked_query = mocker.patch.object(subtensor.substrate, "query", return_value=None) + mocked_query = mocker.patch.object( + subtensor.substrate, + "query", + return_value=mocker.Mock(spec=ScaleType, value_object=None), + ) # Call result = subtensor.get_mev_shield_next_key(block=fake_block) @@ -6182,13 +5910,15 @@ def test_get_mev_shield_next_key_invalid_size(subtensor, mocker): # Prep fake_block = 123 fake_block_hash = "0x123abc" - fake_public_key_bytes = b"\x00" * 1000 # Invalid size + fake_public_key_bytes = bytearray(b"\x00" * 1000) # Invalid size mocked_determine_block_hash = mocker.patch.object( subtensor, "determine_block_hash", return_value=fake_block_hash ) mocked_query = mocker.patch.object(subtensor.substrate, "query") - mocked_query.return_value = iter([fake_public_key_bytes]) + mocked_query.return_value = mocker.Mock( + spec=ScaleType, value_object=fake_public_key_bytes + ) # Call & Assert with pytest.raises(ValueError, match="Invalid ML-KEM-768 public key size"): @@ -6307,7 +6037,7 @@ def test_get_start_call_delay(subtensor, mocker): # Asserts mocked_query_subtensor.assert_called_once_with(name="StartCallDelay", block=None) - assert result == mocked_query_subtensor.return_value + assert result == mocked_query_subtensor.return_value.value def test_get_coldkey_swap_announcement(subtensor, mocker): @@ -6562,3 +6292,51 @@ def test_dispute_coldkey_swap(mocker, subtensor): wait_for_revealed_execution=True, ) assert response == mocked_dispute_coldkey_swap_extrinsic.return_value + + +def test_register_success(subtensor, fake_wallet, mocker): + """Tests register with auto-calculated limit_price from recycle.""" + # Preps + fake_netuid = 1 + + mocked_register_limit_extrinsic = mocker.patch.object( + subtensor_module, "register_limit_extrinsic" + ) + mocker.patch.object( + subtensor, "recycle", return_value=Balance.from_rao(1_000_000_000) + ) + + # Call + result = subtensor.register(wallet=fake_wallet, netuid=fake_netuid) + + # Asserts + mocked_register_limit_extrinsic.assert_called_once_with( + subtensor=subtensor, + wallet=fake_wallet, + netuid=1, + limit_price=Balance.from_rao(1_005_000_000), + mev_protection=DEFAULT_MEV_PROTECTION, + period=DEFAULT_PERIOD, + raise_error=False, + wait_for_inclusion=True, + wait_for_finalization=True, + wait_for_revealed_execution=True, + ) + assert result == mocked_register_limit_extrinsic.return_value + + +def test_register_on_root(mock_substrate, subtensor, fake_wallet, mocker): + mock_substrate.submit_extrinsic.return_value = MagicMock( + is_success=True, + ) + mocked_root_register_extrinsic = mocker.patch.object( + subtensor_module, + "root_register_extrinsic", + ) + + response = subtensor.register( + wallet=fake_wallet, + netuid=0, + ) + + assert response == mocked_root_register_extrinsic.return_value diff --git a/tests/unit_tests/utils/test_registration.py b/tests/unit_tests/utils/test_registration.py index a4ec066279..4b9f6ab3eb 100644 --- a/tests/unit_tests/utils/test_registration.py +++ b/tests/unit_tests/utils/test_registration.py @@ -14,7 +14,7 @@ def error(self, message): @pytest.fixture def mock_bittensor_logging(monkeypatch): mock_logger = MockBittensorLogging() - monkeypatch.setattr("bittensor.utils.registration.pow.logging", mock_logger) + monkeypatch.setattr("bittensor.utils.registration.torch_utils.logging", mock_logger) return mock_logger @@ -32,7 +32,7 @@ def test_lazy_loaded_torch__torch_installed(monkeypatch, mock_bittensor_logging) def test_lazy_loaded_torch__no_torch(monkeypatch, mock_bittensor_logging): monkeypatch.setattr( - "bittensor.utils.registration.pow._get_real_torch", lambda: None + "bittensor.utils.registration.torch_utils._get_real_torch", lambda: None ) torch = LazyLoadedTorch()